@grafema/cli 0.1.1-alpha → 0.2.0-beta
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/dist/cli.js +10 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +28 -7
- package/dist/commands/check.d.ts +6 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +177 -1
- package/dist/commands/coverage.d.ts.map +1 -1
- package/dist/commands/coverage.js +7 -0
- package/dist/commands/doctor/checks.d.ts +55 -0
- package/dist/commands/doctor/checks.d.ts.map +1 -0
- package/dist/commands/doctor/checks.js +534 -0
- package/dist/commands/doctor/output.d.ts +20 -0
- package/dist/commands/doctor/output.d.ts.map +1 -0
- package/dist/commands/doctor/output.js +94 -0
- package/dist/commands/doctor/types.d.ts +42 -0
- package/dist/commands/doctor/types.d.ts.map +1 -0
- package/dist/commands/doctor/types.js +4 -0
- package/dist/commands/doctor.d.ts +17 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +80 -0
- package/dist/commands/explain.d.ts +16 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +145 -0
- package/dist/commands/explore.d.ts +7 -1
- package/dist/commands/explore.d.ts.map +1 -1
- package/dist/commands/explore.js +204 -85
- package/dist/commands/get.d.ts.map +1 -1
- package/dist/commands/get.js +16 -4
- package/dist/commands/impact.d.ts.map +1 -1
- package/dist/commands/impact.js +48 -50
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +93 -15
- package/dist/commands/ls.d.ts +14 -0
- package/dist/commands/ls.d.ts.map +1 -0
- package/dist/commands/ls.js +132 -0
- package/dist/commands/overview.d.ts.map +1 -1
- package/dist/commands/overview.js +15 -2
- package/dist/commands/query.d.ts +98 -0
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +549 -136
- package/dist/commands/schema.d.ts +13 -0
- package/dist/commands/schema.d.ts.map +1 -0
- package/dist/commands/schema.js +279 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +13 -6
- package/dist/commands/stats.d.ts.map +1 -1
- package/dist/commands/stats.js +7 -0
- package/dist/commands/trace.d.ts +73 -0
- package/dist/commands/trace.d.ts.map +1 -1
- package/dist/commands/trace.js +500 -5
- package/dist/commands/types.d.ts +12 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +79 -0
- package/dist/utils/formatNode.d.ts +13 -0
- package/dist/utils/formatNode.d.ts.map +1 -1
- package/dist/utils/formatNode.js +35 -2
- package/package.json +3 -3
- package/src/cli.ts +10 -0
- package/src/commands/analyze.ts +31 -5
- package/src/commands/check.ts +201 -0
- package/src/commands/coverage.ts +7 -0
- package/src/commands/doctor/checks.ts +612 -0
- package/src/commands/doctor/output.ts +115 -0
- package/src/commands/doctor/types.ts +45 -0
- package/src/commands/doctor.ts +106 -0
- package/src/commands/explain.ts +173 -0
- package/src/commands/explore.tsx +247 -97
- package/src/commands/get.ts +20 -6
- package/src/commands/impact.ts +55 -61
- package/src/commands/init.ts +101 -14
- package/src/commands/ls.ts +166 -0
- package/src/commands/overview.ts +15 -2
- package/src/commands/query.ts +643 -149
- package/src/commands/schema.ts +345 -0
- package/src/commands/server.ts +13 -6
- package/src/commands/stats.ts +7 -0
- package/src/commands/trace.ts +647 -6
- package/src/commands/types.ts +94 -0
- package/src/utils/formatNode.ts +42 -2
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostic check functions for `grafema doctor` command - REG-214
|
|
3
|
+
*
|
|
4
|
+
* Checks are organized in levels:
|
|
5
|
+
* - Level 1: Prerequisites (fail-fast) - checkGrafemaInitialized, checkServerStatus
|
|
6
|
+
* - Level 2: Configuration - checkConfigValidity, checkEntrypoints
|
|
7
|
+
* - Level 3: Graph Health - checkDatabaseExists, checkGraphStats, checkConnectivity, checkFreshness
|
|
8
|
+
* - Level 4: Informational - checkVersions
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync, statSync } from 'fs';
|
|
11
|
+
import { join, dirname } from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { createRequire } from 'module';
|
|
14
|
+
import { RFDBServerBackend, RFDBClient, loadConfig, GraphFreshnessChecker, } from '@grafema/core';
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
// Valid built-in plugin names (for config validation)
|
|
18
|
+
const VALID_PLUGIN_NAMES = new Set([
|
|
19
|
+
// Discovery
|
|
20
|
+
'SimpleProjectDiscovery', 'MonorepoServiceDiscovery', 'WorkspaceDiscovery',
|
|
21
|
+
// Indexing
|
|
22
|
+
'JSModuleIndexer', 'RustModuleIndexer',
|
|
23
|
+
// Analysis
|
|
24
|
+
'JSASTAnalyzer', 'ExpressRouteAnalyzer', 'SocketIOAnalyzer', 'DatabaseAnalyzer',
|
|
25
|
+
'FetchAnalyzer', 'ServiceLayerAnalyzer', 'ReactAnalyzer', 'RustAnalyzer',
|
|
26
|
+
// Enrichment
|
|
27
|
+
'MethodCallResolver', 'AliasTracker', 'ValueDomainAnalyzer', 'MountPointResolver',
|
|
28
|
+
'PrefixEvaluator', 'InstanceOfResolver', 'ImportExportLinker', 'HTTPConnectionEnricher',
|
|
29
|
+
'RustFFIEnricher',
|
|
30
|
+
// Validation
|
|
31
|
+
'CallResolverValidator', 'EvalBanValidator', 'SQLInjectionValidator', 'ShadowingDetector',
|
|
32
|
+
'GraphConnectivityValidator', 'DataFlowValidator', 'TypeScriptDeadCodeValidator',
|
|
33
|
+
]);
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Level 1: Prerequisites (fail-fast)
|
|
36
|
+
// =============================================================================
|
|
37
|
+
/**
|
|
38
|
+
* Check if .grafema directory exists with config file.
|
|
39
|
+
* FAIL if not initialized.
|
|
40
|
+
*/
|
|
41
|
+
export async function checkGrafemaInitialized(projectPath) {
|
|
42
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
43
|
+
const configYaml = join(grafemaDir, 'config.yaml');
|
|
44
|
+
const configJson = join(grafemaDir, 'config.json');
|
|
45
|
+
if (!existsSync(grafemaDir)) {
|
|
46
|
+
return {
|
|
47
|
+
name: 'initialization',
|
|
48
|
+
status: 'fail',
|
|
49
|
+
message: '.grafema directory not found',
|
|
50
|
+
recommendation: 'Run: grafema init',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (!existsSync(configYaml) && !existsSync(configJson)) {
|
|
54
|
+
return {
|
|
55
|
+
name: 'initialization',
|
|
56
|
+
status: 'fail',
|
|
57
|
+
message: 'Config file not found',
|
|
58
|
+
recommendation: 'Run: grafema init',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const configFile = existsSync(configYaml) ? 'config.yaml' : 'config.json';
|
|
62
|
+
const deprecated = configFile === 'config.json';
|
|
63
|
+
return {
|
|
64
|
+
name: 'initialization',
|
|
65
|
+
status: deprecated ? 'warn' : 'pass',
|
|
66
|
+
message: `Config file: .grafema/${configFile}`,
|
|
67
|
+
recommendation: deprecated ? 'Run: grafema init --force (migrate to YAML)' : undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check if RFDB server is running and responsive.
|
|
72
|
+
* WARN if not running (server starts on-demand during analyze).
|
|
73
|
+
*/
|
|
74
|
+
export async function checkServerStatus(projectPath) {
|
|
75
|
+
const socketPath = join(projectPath, '.grafema', 'rfdb.sock');
|
|
76
|
+
if (!existsSync(socketPath)) {
|
|
77
|
+
return {
|
|
78
|
+
name: 'server',
|
|
79
|
+
status: 'warn',
|
|
80
|
+
message: 'RFDB server not running',
|
|
81
|
+
recommendation: 'Run: grafema analyze (starts server automatically)',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const client = new RFDBClient(socketPath);
|
|
85
|
+
client.on('error', () => { }); // Suppress error events
|
|
86
|
+
try {
|
|
87
|
+
await client.connect();
|
|
88
|
+
const version = await client.ping();
|
|
89
|
+
await client.close();
|
|
90
|
+
return {
|
|
91
|
+
name: 'server',
|
|
92
|
+
status: 'pass',
|
|
93
|
+
message: `Server: connected (RFDB ${version || 'unknown'})`,
|
|
94
|
+
details: { version, socketPath },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return {
|
|
99
|
+
name: 'server',
|
|
100
|
+
status: 'warn',
|
|
101
|
+
message: 'Server socket exists but not responding (stale)',
|
|
102
|
+
recommendation: 'Run: grafema analyze (will restart server)',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// =============================================================================
|
|
107
|
+
// Level 2: Configuration Validity
|
|
108
|
+
// =============================================================================
|
|
109
|
+
/**
|
|
110
|
+
* Validate config file syntax and structure.
|
|
111
|
+
* Uses existing loadConfig() which throws on errors.
|
|
112
|
+
*/
|
|
113
|
+
export async function checkConfigValidity(projectPath) {
|
|
114
|
+
try {
|
|
115
|
+
// Silent logger to suppress warnings during validation
|
|
116
|
+
const config = loadConfig(projectPath, { warn: () => { } });
|
|
117
|
+
// Check for unknown plugins
|
|
118
|
+
const unknownPlugins = [];
|
|
119
|
+
const phases = ['discovery', 'indexing', 'analysis', 'enrichment', 'validation'];
|
|
120
|
+
for (const phase of phases) {
|
|
121
|
+
const plugins = config.plugins[phase] || [];
|
|
122
|
+
for (const name of plugins) {
|
|
123
|
+
if (!VALID_PLUGIN_NAMES.has(name)) {
|
|
124
|
+
unknownPlugins.push(name);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (unknownPlugins.length > 0) {
|
|
129
|
+
return {
|
|
130
|
+
name: 'config',
|
|
131
|
+
status: 'warn',
|
|
132
|
+
message: `Unknown plugin(s): ${unknownPlugins.join(', ')}`,
|
|
133
|
+
recommendation: 'Check plugin names for typos. Run: grafema doctor --verbose for available plugins',
|
|
134
|
+
details: { unknownPlugins },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const totalPlugins = phases.reduce((sum, phase) => sum + (config.plugins[phase]?.length || 0), 0);
|
|
138
|
+
return {
|
|
139
|
+
name: 'config',
|
|
140
|
+
status: 'pass',
|
|
141
|
+
message: `Config valid: ${totalPlugins} plugins configured`,
|
|
142
|
+
details: { pluginCount: totalPlugins, services: config.services.length },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
147
|
+
return {
|
|
148
|
+
name: 'config',
|
|
149
|
+
status: 'fail',
|
|
150
|
+
message: `Config error: ${error.message}`,
|
|
151
|
+
recommendation: 'Fix config.yaml syntax or run: grafema init --force',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Check that entrypoints can be resolved.
|
|
157
|
+
* For config-defined services, validates that entrypoint files exist.
|
|
158
|
+
*/
|
|
159
|
+
export async function checkEntrypoints(projectPath) {
|
|
160
|
+
let config;
|
|
161
|
+
try {
|
|
162
|
+
config = loadConfig(projectPath, { warn: () => { } });
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Config loading failed - already reported by checkConfigValidity
|
|
166
|
+
return {
|
|
167
|
+
name: 'entrypoints',
|
|
168
|
+
status: 'skip',
|
|
169
|
+
message: 'Skipped (config error)',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (config.services.length === 0) {
|
|
173
|
+
// Auto-discovery mode - check package.json exists
|
|
174
|
+
const pkgJson = join(projectPath, 'package.json');
|
|
175
|
+
if (!existsSync(pkgJson)) {
|
|
176
|
+
return {
|
|
177
|
+
name: 'entrypoints',
|
|
178
|
+
status: 'warn',
|
|
179
|
+
message: 'No package.json found for auto-discovery',
|
|
180
|
+
recommendation: 'Add package.json or configure services in config.yaml',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
name: 'entrypoints',
|
|
185
|
+
status: 'pass',
|
|
186
|
+
message: 'Using auto-discovery mode',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// Config-defined services - validate each
|
|
190
|
+
const issues = [];
|
|
191
|
+
const valid = [];
|
|
192
|
+
for (const svc of config.services) {
|
|
193
|
+
const svcPath = join(projectPath, svc.path);
|
|
194
|
+
let entrypoint;
|
|
195
|
+
if (svc.entryPoint) {
|
|
196
|
+
entrypoint = join(svcPath, svc.entryPoint);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
// Auto-detect from package.json
|
|
200
|
+
const pkgPath = join(svcPath, 'package.json');
|
|
201
|
+
if (existsSync(pkgPath)) {
|
|
202
|
+
try {
|
|
203
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
204
|
+
entrypoint = join(svcPath, pkg.main || 'index.js');
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
entrypoint = join(svcPath, 'index.js');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
entrypoint = join(svcPath, 'index.js');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (existsSync(entrypoint)) {
|
|
215
|
+
valid.push(svc.name);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
issues.push(`${svc.name}: ${entrypoint} not found`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (issues.length > 0) {
|
|
222
|
+
return {
|
|
223
|
+
name: 'entrypoints',
|
|
224
|
+
status: 'warn',
|
|
225
|
+
message: `${issues.length} service(s) with missing entrypoints`,
|
|
226
|
+
recommendation: 'Check service paths in config.yaml',
|
|
227
|
+
details: { issues, valid },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
name: 'entrypoints',
|
|
232
|
+
status: 'pass',
|
|
233
|
+
message: `Entrypoints: ${valid.length} service(s) found`,
|
|
234
|
+
details: { services: valid },
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
// =============================================================================
|
|
238
|
+
// Level 3: Graph Health
|
|
239
|
+
// =============================================================================
|
|
240
|
+
/**
|
|
241
|
+
* Check if database file exists and has data.
|
|
242
|
+
*/
|
|
243
|
+
export async function checkDatabaseExists(projectPath) {
|
|
244
|
+
const dbPath = join(projectPath, '.grafema', 'graph.rfdb');
|
|
245
|
+
if (!existsSync(dbPath)) {
|
|
246
|
+
return {
|
|
247
|
+
name: 'database',
|
|
248
|
+
status: 'fail',
|
|
249
|
+
message: 'Database not found',
|
|
250
|
+
recommendation: 'Run: grafema analyze',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// Check file size (empty DB is typically < 100 bytes)
|
|
254
|
+
const stats = statSync(dbPath);
|
|
255
|
+
if (stats.size < 100) {
|
|
256
|
+
return {
|
|
257
|
+
name: 'database',
|
|
258
|
+
status: 'warn',
|
|
259
|
+
message: 'Database appears empty',
|
|
260
|
+
recommendation: 'Run: grafema analyze',
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
name: 'database',
|
|
265
|
+
status: 'pass',
|
|
266
|
+
message: `Database: ${dbPath}`,
|
|
267
|
+
details: { size: stats.size },
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Get graph statistics (requires server running).
|
|
272
|
+
*/
|
|
273
|
+
export async function checkGraphStats(projectPath) {
|
|
274
|
+
const socketPath = join(projectPath, '.grafema', 'rfdb.sock');
|
|
275
|
+
const dbPath = join(projectPath, '.grafema', 'graph.rfdb');
|
|
276
|
+
if (!existsSync(socketPath)) {
|
|
277
|
+
return {
|
|
278
|
+
name: 'graph_stats',
|
|
279
|
+
status: 'skip',
|
|
280
|
+
message: 'Server not running (skipped stats check)',
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const backend = new RFDBServerBackend({ dbPath });
|
|
284
|
+
try {
|
|
285
|
+
await backend.connect();
|
|
286
|
+
const stats = await backend.getStats();
|
|
287
|
+
await backend.close();
|
|
288
|
+
if (stats.nodeCount === 0) {
|
|
289
|
+
return {
|
|
290
|
+
name: 'graph_stats',
|
|
291
|
+
status: 'fail',
|
|
292
|
+
message: 'Database is empty (0 nodes)',
|
|
293
|
+
recommendation: 'Run: grafema analyze',
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
name: 'graph_stats',
|
|
298
|
+
status: 'pass',
|
|
299
|
+
message: `Graph: ${stats.nodeCount.toLocaleString()} nodes, ${stats.edgeCount.toLocaleString()} edges`,
|
|
300
|
+
details: {
|
|
301
|
+
nodeCount: stats.nodeCount,
|
|
302
|
+
edgeCount: stats.edgeCount,
|
|
303
|
+
nodesByType: stats.nodesByType,
|
|
304
|
+
edgesByType: stats.edgesByType,
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
return {
|
|
310
|
+
name: 'graph_stats',
|
|
311
|
+
status: 'warn',
|
|
312
|
+
message: `Could not read graph stats: ${err.message}`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Check graph connectivity - find disconnected nodes.
|
|
318
|
+
* Thresholds:
|
|
319
|
+
* 0-5%: pass (normal for external modules)
|
|
320
|
+
* 5-20%: warn
|
|
321
|
+
* >20%: fail (critical issue)
|
|
322
|
+
*/
|
|
323
|
+
export async function checkConnectivity(projectPath) {
|
|
324
|
+
const socketPath = join(projectPath, '.grafema', 'rfdb.sock');
|
|
325
|
+
const dbPath = join(projectPath, '.grafema', 'graph.rfdb');
|
|
326
|
+
if (!existsSync(socketPath)) {
|
|
327
|
+
return {
|
|
328
|
+
name: 'connectivity',
|
|
329
|
+
status: 'skip',
|
|
330
|
+
message: 'Server not running (skipped connectivity check)',
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const backend = new RFDBServerBackend({ dbPath });
|
|
334
|
+
try {
|
|
335
|
+
await backend.connect();
|
|
336
|
+
// Get all nodes
|
|
337
|
+
const allNodes = [];
|
|
338
|
+
for await (const node of backend.queryNodes({})) {
|
|
339
|
+
allNodes.push({ id: node.id, type: node.type });
|
|
340
|
+
}
|
|
341
|
+
const totalCount = allNodes.length;
|
|
342
|
+
if (totalCount === 0) {
|
|
343
|
+
await backend.close();
|
|
344
|
+
return {
|
|
345
|
+
name: 'connectivity',
|
|
346
|
+
status: 'skip',
|
|
347
|
+
message: 'No nodes to check',
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
// Find root nodes (SERVICE, MODULE, PROJECT)
|
|
351
|
+
const rootTypes = ['SERVICE', 'MODULE', 'PROJECT'];
|
|
352
|
+
const rootNodes = allNodes.filter(n => rootTypes.includes(n.type));
|
|
353
|
+
if (rootNodes.length === 0) {
|
|
354
|
+
await backend.close();
|
|
355
|
+
return {
|
|
356
|
+
name: 'connectivity',
|
|
357
|
+
status: 'warn',
|
|
358
|
+
message: 'No root nodes found (SERVICE/MODULE/PROJECT)',
|
|
359
|
+
recommendation: 'Run: grafema analyze',
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
// Get all edges and build adjacency
|
|
363
|
+
const allEdges = await backend.getAllEdges();
|
|
364
|
+
const adjacencyOut = new Map();
|
|
365
|
+
const adjacencyIn = new Map();
|
|
366
|
+
for (const edge of allEdges) {
|
|
367
|
+
if (!adjacencyOut.has(edge.src))
|
|
368
|
+
adjacencyOut.set(edge.src, []);
|
|
369
|
+
adjacencyOut.get(edge.src).push(edge.dst);
|
|
370
|
+
if (!adjacencyIn.has(edge.dst))
|
|
371
|
+
adjacencyIn.set(edge.dst, []);
|
|
372
|
+
adjacencyIn.get(edge.dst).push(edge.src);
|
|
373
|
+
}
|
|
374
|
+
// BFS from roots
|
|
375
|
+
const reachable = new Set();
|
|
376
|
+
const queue = [...rootNodes.map(n => n.id)];
|
|
377
|
+
while (queue.length > 0) {
|
|
378
|
+
const nodeId = queue.shift();
|
|
379
|
+
if (reachable.has(nodeId))
|
|
380
|
+
continue;
|
|
381
|
+
reachable.add(nodeId);
|
|
382
|
+
const outgoing = adjacencyOut.get(nodeId) || [];
|
|
383
|
+
const incoming = adjacencyIn.get(nodeId) || [];
|
|
384
|
+
for (const targetId of [...outgoing, ...incoming]) {
|
|
385
|
+
if (!reachable.has(targetId))
|
|
386
|
+
queue.push(targetId);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
await backend.close();
|
|
390
|
+
const unreachableCount = totalCount - reachable.size;
|
|
391
|
+
const percentage = (unreachableCount / totalCount) * 100;
|
|
392
|
+
if (unreachableCount === 0) {
|
|
393
|
+
return {
|
|
394
|
+
name: 'connectivity',
|
|
395
|
+
status: 'pass',
|
|
396
|
+
message: 'All nodes connected',
|
|
397
|
+
details: { totalNodes: totalCount },
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
// Group unreachable by type
|
|
401
|
+
const unreachableNodes = allNodes.filter(n => !reachable.has(n.id));
|
|
402
|
+
const byType = {};
|
|
403
|
+
for (const node of unreachableNodes) {
|
|
404
|
+
byType[node.type] = (byType[node.type] || 0) + 1;
|
|
405
|
+
}
|
|
406
|
+
if (percentage > 20) {
|
|
407
|
+
return {
|
|
408
|
+
name: 'connectivity',
|
|
409
|
+
status: 'fail',
|
|
410
|
+
message: `Critical: ${unreachableCount} disconnected nodes (${percentage.toFixed(1)}%)`,
|
|
411
|
+
recommendation: 'Run: grafema analyze --clear (rebuild graph)',
|
|
412
|
+
details: { unreachableCount, percentage, byType },
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (percentage > 5) {
|
|
416
|
+
return {
|
|
417
|
+
name: 'connectivity',
|
|
418
|
+
status: 'warn',
|
|
419
|
+
message: `${unreachableCount} disconnected nodes (${percentage.toFixed(1)}%)`,
|
|
420
|
+
recommendation: 'Run: grafema analyze --clear (may fix)',
|
|
421
|
+
details: { unreachableCount, percentage, byType },
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
name: 'connectivity',
|
|
426
|
+
status: 'pass',
|
|
427
|
+
message: `${unreachableCount} disconnected nodes (${percentage.toFixed(1)}% - normal)`,
|
|
428
|
+
details: { unreachableCount, percentage, byType },
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
return {
|
|
433
|
+
name: 'connectivity',
|
|
434
|
+
status: 'warn',
|
|
435
|
+
message: `Could not check connectivity: ${err.message}`,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Check if graph is fresh (no stale modules).
|
|
441
|
+
*/
|
|
442
|
+
export async function checkFreshness(projectPath) {
|
|
443
|
+
const socketPath = join(projectPath, '.grafema', 'rfdb.sock');
|
|
444
|
+
const dbPath = join(projectPath, '.grafema', 'graph.rfdb');
|
|
445
|
+
if (!existsSync(socketPath)) {
|
|
446
|
+
return {
|
|
447
|
+
name: 'freshness',
|
|
448
|
+
status: 'skip',
|
|
449
|
+
message: 'Server not running (skipped freshness check)',
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
const backend = new RFDBServerBackend({ dbPath });
|
|
453
|
+
try {
|
|
454
|
+
await backend.connect();
|
|
455
|
+
const freshnessChecker = new GraphFreshnessChecker();
|
|
456
|
+
const result = await freshnessChecker.checkFreshness(backend);
|
|
457
|
+
await backend.close();
|
|
458
|
+
if (result.isFresh) {
|
|
459
|
+
return {
|
|
460
|
+
name: 'freshness',
|
|
461
|
+
status: 'pass',
|
|
462
|
+
message: 'Graph is up to date',
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
name: 'freshness',
|
|
467
|
+
status: 'warn',
|
|
468
|
+
message: `${result.staleCount} stale module(s) detected`,
|
|
469
|
+
recommendation: 'Run: grafema analyze (or grafema check for auto-reanalysis)',
|
|
470
|
+
details: {
|
|
471
|
+
staleCount: result.staleCount,
|
|
472
|
+
staleModules: result.staleModules.slice(0, 5).map(m => m.file),
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
return {
|
|
478
|
+
name: 'freshness',
|
|
479
|
+
status: 'warn',
|
|
480
|
+
message: `Could not check freshness: ${err.message}`,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// =============================================================================
|
|
485
|
+
// Level 4: Informational
|
|
486
|
+
// =============================================================================
|
|
487
|
+
/**
|
|
488
|
+
* Collect version information (always passes).
|
|
489
|
+
*/
|
|
490
|
+
export async function checkVersions(projectPath) {
|
|
491
|
+
let cliVersion = 'unknown';
|
|
492
|
+
let coreVersion = 'unknown';
|
|
493
|
+
let rfdbVersion;
|
|
494
|
+
// Read CLI version - from dist/commands/doctor/ go up 3 levels to cli/
|
|
495
|
+
try {
|
|
496
|
+
const cliPkgPath = join(__dirname, '../../../package.json');
|
|
497
|
+
const cliPkg = JSON.parse(readFileSync(cliPkgPath, 'utf-8'));
|
|
498
|
+
cliVersion = cliPkg.version;
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
// Ignore errors
|
|
502
|
+
}
|
|
503
|
+
// Read core version
|
|
504
|
+
try {
|
|
505
|
+
const require = createRequire(import.meta.url);
|
|
506
|
+
const corePkgPath = require.resolve('@grafema/core/package.json');
|
|
507
|
+
const corePkg = JSON.parse(readFileSync(corePkgPath, 'utf-8'));
|
|
508
|
+
coreVersion = corePkg.version;
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
// Ignore errors
|
|
512
|
+
}
|
|
513
|
+
// Get RFDB version from server if running
|
|
514
|
+
const socketPath = join(projectPath, '.grafema', 'rfdb.sock');
|
|
515
|
+
if (existsSync(socketPath)) {
|
|
516
|
+
const client = new RFDBClient(socketPath);
|
|
517
|
+
client.on('error', () => { });
|
|
518
|
+
try {
|
|
519
|
+
await client.connect();
|
|
520
|
+
const version = await client.ping();
|
|
521
|
+
rfdbVersion = version || undefined;
|
|
522
|
+
await client.close();
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
// Ignore errors
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
name: 'versions',
|
|
530
|
+
status: 'pass',
|
|
531
|
+
message: `CLI ${cliVersion}, Core ${coreVersion}${rfdbVersion ? `, RFDB ${rfdbVersion}` : ''}`,
|
|
532
|
+
details: { cli: cliVersion, core: coreVersion, rfdb: rfdbVersion },
|
|
533
|
+
};
|
|
534
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatting utilities for `grafema doctor` command - REG-214
|
|
3
|
+
*/
|
|
4
|
+
import type { DoctorCheckResult, DoctorReport } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Format a single check result for console output.
|
|
7
|
+
*/
|
|
8
|
+
export declare function formatCheck(result: DoctorCheckResult, verbose: boolean): string;
|
|
9
|
+
/**
|
|
10
|
+
* Format full report for console.
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatReport(checks: DoctorCheckResult[], options: {
|
|
13
|
+
quiet?: boolean;
|
|
14
|
+
verbose?: boolean;
|
|
15
|
+
}): string;
|
|
16
|
+
/**
|
|
17
|
+
* Build JSON report structure.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildJsonReport(checks: DoctorCheckResult[], projectPath: string): DoctorReport;
|
|
20
|
+
//# sourceMappingURL=output.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor/output.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAmBlE;;GAEG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAiB/E;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,iBAAiB,EAAE,EAC3B,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9C,MAAM,CA2BR;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,iBAAiB,EAAE,EAC3B,WAAW,EAAE,MAAM,GAClB,YAAY,CA4Bd"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatting utilities for `grafema doctor` command - REG-214
|
|
3
|
+
*/
|
|
4
|
+
// ANSI colors (matching existing CLI style)
|
|
5
|
+
const COLORS = {
|
|
6
|
+
green: '\x1b[32m',
|
|
7
|
+
red: '\x1b[31m',
|
|
8
|
+
yellow: '\x1b[33m',
|
|
9
|
+
cyan: '\x1b[36m',
|
|
10
|
+
dim: '\x1b[2m',
|
|
11
|
+
reset: '\x1b[0m',
|
|
12
|
+
};
|
|
13
|
+
const STATUS_ICONS = {
|
|
14
|
+
pass: `${COLORS.green}✓${COLORS.reset}`,
|
|
15
|
+
warn: `${COLORS.yellow}⚠${COLORS.reset}`,
|
|
16
|
+
fail: `${COLORS.red}✗${COLORS.reset}`,
|
|
17
|
+
skip: `${COLORS.dim}○${COLORS.reset}`,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Format a single check result for console output.
|
|
21
|
+
*/
|
|
22
|
+
export function formatCheck(result, verbose) {
|
|
23
|
+
const icon = STATUS_ICONS[result.status];
|
|
24
|
+
let output = `${icon} ${result.message}`;
|
|
25
|
+
if (result.recommendation) {
|
|
26
|
+
output += `\n ${COLORS.dim}→${COLORS.reset} ${result.recommendation}`;
|
|
27
|
+
}
|
|
28
|
+
if (verbose && result.details) {
|
|
29
|
+
const detailStr = JSON.stringify(result.details, null, 2)
|
|
30
|
+
.split('\n')
|
|
31
|
+
.map(line => ` ${COLORS.dim}${line}${COLORS.reset}`)
|
|
32
|
+
.join('\n');
|
|
33
|
+
output += `\n${detailStr}`;
|
|
34
|
+
}
|
|
35
|
+
return output;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Format full report for console.
|
|
39
|
+
*/
|
|
40
|
+
export function formatReport(checks, options) {
|
|
41
|
+
const lines = [];
|
|
42
|
+
if (!options.quiet) {
|
|
43
|
+
lines.push('Checking Grafema setup...');
|
|
44
|
+
lines.push('');
|
|
45
|
+
}
|
|
46
|
+
for (const check of checks) {
|
|
47
|
+
if (options.quiet && check.status === 'pass')
|
|
48
|
+
continue;
|
|
49
|
+
lines.push(formatCheck(check, options.verbose || false));
|
|
50
|
+
}
|
|
51
|
+
// Summary
|
|
52
|
+
const failCount = checks.filter(c => c.status === 'fail').length;
|
|
53
|
+
const warnCount = checks.filter(c => c.status === 'warn').length;
|
|
54
|
+
lines.push('');
|
|
55
|
+
if (failCount > 0) {
|
|
56
|
+
lines.push(`${COLORS.red}Status: ${failCount} error(s), ${warnCount} warning(s)${COLORS.reset}`);
|
|
57
|
+
}
|
|
58
|
+
else if (warnCount > 0) {
|
|
59
|
+
lines.push(`${COLORS.yellow}Status: ${warnCount} warning(s)${COLORS.reset}`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
lines.push(`${COLORS.green}Status: All checks passed${COLORS.reset}`);
|
|
63
|
+
}
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build JSON report structure.
|
|
68
|
+
*/
|
|
69
|
+
export function buildJsonReport(checks, projectPath) {
|
|
70
|
+
const failCount = checks.filter(c => c.status === 'fail').length;
|
|
71
|
+
const warnCount = checks.filter(c => c.status === 'warn').length;
|
|
72
|
+
const status = failCount > 0 ? 'error' : warnCount > 0 ? 'warning' : 'healthy';
|
|
73
|
+
const recommendations = checks
|
|
74
|
+
.filter(c => c.recommendation)
|
|
75
|
+
.map(c => c.recommendation);
|
|
76
|
+
// Extract versions from versions check
|
|
77
|
+
const versionsCheck = checks.find(c => c.name === 'versions');
|
|
78
|
+
const versions = versionsCheck?.details || {
|
|
79
|
+
cli: 'unknown',
|
|
80
|
+
core: 'unknown',
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
status,
|
|
84
|
+
timestamp: new Date().toISOString(),
|
|
85
|
+
project: projectPath,
|
|
86
|
+
checks,
|
|
87
|
+
recommendations,
|
|
88
|
+
versions: {
|
|
89
|
+
cli: versions.cli || 'unknown',
|
|
90
|
+
core: versions.core || 'unknown',
|
|
91
|
+
rfdb: versions.rfdb,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for `grafema doctor` command - REG-214
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Status of a single diagnostic check
|
|
6
|
+
*/
|
|
7
|
+
export type CheckStatus = 'pass' | 'warn' | 'fail' | 'skip';
|
|
8
|
+
/**
|
|
9
|
+
* Result of a single diagnostic check
|
|
10
|
+
*/
|
|
11
|
+
export interface DoctorCheckResult {
|
|
12
|
+
name: string;
|
|
13
|
+
status: CheckStatus;
|
|
14
|
+
message: string;
|
|
15
|
+
recommendation?: string;
|
|
16
|
+
details?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Options for the doctor command
|
|
20
|
+
*/
|
|
21
|
+
export interface DoctorOptions {
|
|
22
|
+
project: string;
|
|
23
|
+
json?: boolean;
|
|
24
|
+
quiet?: boolean;
|
|
25
|
+
verbose?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Overall doctor report (for JSON output)
|
|
29
|
+
*/
|
|
30
|
+
export interface DoctorReport {
|
|
31
|
+
status: 'healthy' | 'warning' | 'error';
|
|
32
|
+
timestamp: string;
|
|
33
|
+
project: string;
|
|
34
|
+
checks: DoctorCheckResult[];
|
|
35
|
+
recommendations: string[];
|
|
36
|
+
versions: {
|
|
37
|
+
cli: string;
|
|
38
|
+
core: string;
|
|
39
|
+
rfdb?: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5D;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,EAAE;QACR,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH"}
|