@grafema/cli 0.2.5-beta → 0.2.7
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/README.md +12 -0
- package/dist/cli.js +6 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyze.d.ts +3 -10
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +5 -347
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/analyzeAction.d.ts +28 -0
- package/dist/commands/analyzeAction.d.ts.map +1 -0
- package/dist/commands/analyzeAction.js +243 -0
- package/dist/commands/analyzeAction.js.map +1 -0
- package/dist/commands/check.js +2 -2
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/context.d.ts +16 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +238 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/doctor/checks.js +1 -1
- package/dist/commands/doctor/checks.js.map +1 -1
- package/dist/commands/explain.d.ts.map +1 -1
- package/dist/commands/explain.js +4 -3
- package/dist/commands/explain.js.map +1 -1
- package/dist/commands/file.d.ts +15 -0
- package/dist/commands/file.d.ts.map +1 -0
- package/dist/commands/file.js +144 -0
- package/dist/commands/file.js.map +1 -0
- package/dist/commands/impact.d.ts.map +1 -1
- package/dist/commands/impact.js +2 -3
- package/dist/commands/impact.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +13 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +3 -2
- package/dist/commands/ls.js.map +1 -1
- package/dist/commands/query.d.ts +8 -0
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +158 -51
- package/dist/commands/query.js.map +1 -1
- package/dist/commands/schema.d.ts.map +1 -1
- package/dist/commands/schema.js +3 -2
- package/dist/commands/schema.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +8 -59
- package/dist/commands/server.js.map +1 -1
- package/dist/commands/setup-skill.d.ts +17 -0
- package/dist/commands/setup-skill.d.ts.map +1 -0
- package/dist/commands/setup-skill.js +131 -0
- package/dist/commands/setup-skill.js.map +1 -0
- package/dist/commands/trace.d.ts.map +1 -1
- package/dist/commands/trace.js +20 -10
- package/dist/commands/trace.js.map +1 -1
- package/dist/plugins/builtinPlugins.d.ts +10 -0
- package/dist/plugins/builtinPlugins.d.ts.map +1 -0
- package/dist/plugins/builtinPlugins.js +68 -0
- package/dist/plugins/builtinPlugins.js.map +1 -0
- package/dist/plugins/pluginLoader.d.ts +16 -0
- package/dist/plugins/pluginLoader.d.ts.map +1 -0
- package/dist/plugins/pluginLoader.js +101 -0
- package/dist/plugins/pluginLoader.js.map +1 -0
- package/dist/plugins/pluginResolver.js +38 -0
- package/dist/utils/codePreview.d.ts +1 -0
- package/dist/utils/codePreview.d.ts.map +1 -1
- package/dist/utils/codePreview.js +5 -3
- package/dist/utils/codePreview.js.map +1 -1
- package/dist/utils/formatNode.d.ts +1 -1
- package/dist/utils/formatNode.d.ts.map +1 -1
- package/dist/utils/formatNode.js +2 -2
- package/dist/utils/formatNode.js.map +1 -1
- package/dist/utils/pathUtils.d.ts +2 -0
- package/dist/utils/pathUtils.d.ts.map +1 -0
- package/dist/utils/pathUtils.js +9 -0
- package/dist/utils/pathUtils.js.map +1 -0
- package/dist/utils/progressRenderer.d.ts +4 -0
- package/dist/utils/progressRenderer.d.ts.map +1 -1
- package/dist/utils/progressRenderer.js +23 -4
- package/dist/utils/progressRenderer.js.map +1 -1
- package/package.json +7 -9
- package/skills/grafema-codebase-analysis/SKILL.md +295 -0
- package/skills/grafema-codebase-analysis/references/node-edge-types.md +123 -0
- package/skills/grafema-codebase-analysis/references/query-patterns.md +205 -0
- package/src/cli.ts +8 -2
- package/src/commands/analyze.ts +5 -435
- package/src/commands/analyzeAction.ts +284 -0
- package/src/commands/check.ts +2 -2
- package/src/commands/context.ts +309 -0
- package/src/commands/doctor/checks.ts +1 -1
- package/src/commands/explain.ts +4 -3
- package/src/commands/explore.tsx +7 -5
- package/src/commands/file.ts +179 -0
- package/src/commands/impact.ts +2 -3
- package/src/commands/init.ts +13 -1
- package/src/commands/ls.ts +3 -2
- package/src/commands/query.ts +167 -52
- package/src/commands/schema.ts +3 -2
- package/src/commands/server.ts +8 -64
- package/src/commands/setup-skill.ts +162 -0
- package/src/commands/trace.ts +18 -9
- package/src/plugins/builtinPlugins.ts +108 -0
- package/src/plugins/pluginLoader.ts +123 -0
- package/src/plugins/pluginResolver.js +38 -0
- package/src/utils/codePreview.ts +7 -3
- package/src/utils/formatNode.ts +3 -3
- package/src/utils/pathUtils.ts +9 -0
- package/src/utils/progressRenderer.ts +25 -4
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze command action — connects to RFDB, loads plugins, runs Orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from analyze.ts (REG-435) to keep command definition separate
|
|
5
|
+
* from execution logic.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { resolve, join } from 'path';
|
|
9
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
10
|
+
import {
|
|
11
|
+
Orchestrator,
|
|
12
|
+
RFDBServerBackend,
|
|
13
|
+
DiagnosticReporter,
|
|
14
|
+
DiagnosticWriter,
|
|
15
|
+
createLogger,
|
|
16
|
+
loadConfig,
|
|
17
|
+
StrictModeFailure,
|
|
18
|
+
} from '@grafema/core';
|
|
19
|
+
import type { LogLevel, GraphBackend } from '@grafema/types';
|
|
20
|
+
import { ProgressRenderer } from '../utils/progressRenderer.js';
|
|
21
|
+
import { loadCustomPlugins, createPlugins } from '../plugins/pluginLoader.js';
|
|
22
|
+
|
|
23
|
+
export interface NodeEdgeCountBackend {
|
|
24
|
+
nodeCount: () => Promise<number>;
|
|
25
|
+
edgeCount: () => Promise<number>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function fetchNodeEdgeCounts(backend: NodeEdgeCountBackend): Promise<{ nodeCount: number; edgeCount: number }> {
|
|
29
|
+
const [nodeCount, edgeCount] = await Promise.all([backend.nodeCount(), backend.edgeCount()]);
|
|
30
|
+
return { nodeCount, edgeCount };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function exitWithCode(code: number, exitFn: (code: number) => void = process.exit): void {
|
|
34
|
+
exitFn(code);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Determine log level from CLI options.
|
|
39
|
+
* Priority: --log-level > --quiet > --verbose > default ('silent')
|
|
40
|
+
*
|
|
41
|
+
* By default, logs are silent to allow clean progress UI.
|
|
42
|
+
* Use --verbose to see detailed logs (disables interactive progress).
|
|
43
|
+
*/
|
|
44
|
+
function getLogLevel(options: { quiet?: boolean; verbose?: boolean; logLevel?: string }): LogLevel {
|
|
45
|
+
if (options.logLevel) {
|
|
46
|
+
const validLevels: LogLevel[] = ['silent', 'errors', 'warnings', 'info', 'debug'];
|
|
47
|
+
if (validLevels.includes(options.logLevel as LogLevel)) {
|
|
48
|
+
return options.logLevel as LogLevel;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (options.quiet) return 'silent';
|
|
52
|
+
if (options.verbose) return 'info'; // --verbose shows logs instead of progress UI
|
|
53
|
+
return 'silent'; // Default: silent logs, clean progress UI
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function analyzeAction(path: string, options: { service?: string; entrypoint?: string; clear?: boolean; quiet?: boolean; verbose?: boolean; debug?: boolean; logLevel?: string; logFile?: string; strict?: boolean; autoStart?: boolean }): Promise<void> {
|
|
57
|
+
const projectPath = resolve(path);
|
|
58
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
59
|
+
const dbPath = join(grafemaDir, 'graph.rfdb');
|
|
60
|
+
|
|
61
|
+
if (!existsSync(grafemaDir)) {
|
|
62
|
+
mkdirSync(grafemaDir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Two log levels for CLI output:
|
|
66
|
+
// - info: important results (shows unless --quiet)
|
|
67
|
+
// - debug: verbose details (shows only with --verbose)
|
|
68
|
+
const info = options.quiet ? () => {} : console.log;
|
|
69
|
+
const debug = options.verbose ? console.log : () => {};
|
|
70
|
+
|
|
71
|
+
// Create logger based on CLI flags
|
|
72
|
+
const logLevel = getLogLevel(options);
|
|
73
|
+
const logFile = options.logFile ? resolve(options.logFile) : undefined;
|
|
74
|
+
const logger = createLogger(logLevel, logFile ? { logFile } : undefined);
|
|
75
|
+
|
|
76
|
+
if (logFile) {
|
|
77
|
+
debug(`Log file: ${logFile}`);
|
|
78
|
+
}
|
|
79
|
+
debug(`Analyzing project: ${projectPath}`);
|
|
80
|
+
|
|
81
|
+
// Connect to RFDB server
|
|
82
|
+
// Default: require explicit `grafema server start`
|
|
83
|
+
// Use --auto-start for CI or backwards compatibility
|
|
84
|
+
// In normal mode (not verbose), suppress backend logs for clean progress UI
|
|
85
|
+
const backend = new RFDBServerBackend({
|
|
86
|
+
dbPath,
|
|
87
|
+
autoStart: options.autoStart ?? false,
|
|
88
|
+
silent: !options.verbose // Silent in normal mode (show progress), verbose shows logs
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await backend.connect();
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (!options.autoStart && err instanceof Error && err.message.includes('not running')) {
|
|
95
|
+
console.error('');
|
|
96
|
+
console.error('RFDB server is not running.');
|
|
97
|
+
console.error('');
|
|
98
|
+
console.error('Start the server first:');
|
|
99
|
+
console.error(' grafema server start');
|
|
100
|
+
console.error('');
|
|
101
|
+
console.error('Or use --auto-start flag:');
|
|
102
|
+
console.error(' grafema analyze --auto-start');
|
|
103
|
+
console.error('');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (options.clear) {
|
|
110
|
+
debug('Clearing existing database...');
|
|
111
|
+
await backend.clear();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const config = loadConfig(projectPath, logger);
|
|
115
|
+
|
|
116
|
+
// Extract services from config (REG-174)
|
|
117
|
+
if (config.services.length > 0) {
|
|
118
|
+
debug(`Loaded ${config.services.length} service(s) from config`);
|
|
119
|
+
for (const svc of config.services) {
|
|
120
|
+
const entry = svc.entryPoint ? ` (entry: ${svc.entryPoint})` : '';
|
|
121
|
+
debug(` - ${svc.name}: ${svc.path}${entry}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Load custom plugins from .grafema/plugins/
|
|
126
|
+
const customPlugins = await loadCustomPlugins(projectPath, debug);
|
|
127
|
+
const plugins = createPlugins(config.plugins, customPlugins, options.verbose);
|
|
128
|
+
|
|
129
|
+
debug(`Loaded ${plugins.length} plugins`);
|
|
130
|
+
|
|
131
|
+
// Resolve strict mode: CLI flag overrides config
|
|
132
|
+
const strictMode = options.strict ?? config.strict ?? false;
|
|
133
|
+
if (strictMode) {
|
|
134
|
+
debug('Strict mode enabled - analysis will fail on unresolved references');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const startTime = Date.now();
|
|
138
|
+
|
|
139
|
+
// Create progress renderer for CLI output
|
|
140
|
+
// In quiet mode, use a no-op renderer (skip rendering)
|
|
141
|
+
// In verbose mode, use non-interactive (newlines per update)
|
|
142
|
+
// In normal mode, use interactive (spinner with line overwrite)
|
|
143
|
+
const renderer = options.quiet
|
|
144
|
+
? null
|
|
145
|
+
: new ProgressRenderer({
|
|
146
|
+
isInteractive: !options.verbose && process.stdout.isTTY,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Poll graph stats periodically to show node/edge counts in progress
|
|
150
|
+
let statsInterval: NodeJS.Timeout | null = null;
|
|
151
|
+
if (renderer && !options.quiet) {
|
|
152
|
+
statsInterval = setInterval(async () => {
|
|
153
|
+
try {
|
|
154
|
+
const stats = await fetchNodeEdgeCounts(backend);
|
|
155
|
+
renderer.setStats(stats.nodeCount, stats.edgeCount);
|
|
156
|
+
} catch {
|
|
157
|
+
// Ignore stats errors during analysis
|
|
158
|
+
}
|
|
159
|
+
}, 500); // Poll every 500ms
|
|
160
|
+
statsInterval.unref?.();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const orchestrator = new Orchestrator({
|
|
164
|
+
graph: backend as unknown as GraphBackend,
|
|
165
|
+
plugins,
|
|
166
|
+
serviceFilter: options.service || null,
|
|
167
|
+
entrypoint: options.entrypoint,
|
|
168
|
+
forceAnalysis: options.clear || false,
|
|
169
|
+
logger,
|
|
170
|
+
services: config.services.length > 0 ? config.services : undefined, // Pass config services (REG-174)
|
|
171
|
+
strictMode, // REG-330: Pass strict mode flag
|
|
172
|
+
onProgress: (progress) => {
|
|
173
|
+
renderer?.update(progress);
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
let exitCode = 0;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await orchestrator.run(projectPath);
|
|
181
|
+
await backend.flush();
|
|
182
|
+
|
|
183
|
+
const elapsedSeconds = (Date.now() - startTime) / 1000;
|
|
184
|
+
const stats = await fetchNodeEdgeCounts(backend);
|
|
185
|
+
|
|
186
|
+
// Clear progress line in interactive mode, then show results
|
|
187
|
+
if (renderer && process.stdout.isTTY) {
|
|
188
|
+
process.stdout.write('\r\x1b[K'); // Clear line
|
|
189
|
+
}
|
|
190
|
+
info('');
|
|
191
|
+
info(renderer ? renderer.finish(elapsedSeconds) : `Analysis complete in ${elapsedSeconds.toFixed(2)}s`);
|
|
192
|
+
info(` Nodes: ${stats.nodeCount}`);
|
|
193
|
+
info(` Edges: ${stats.edgeCount}`);
|
|
194
|
+
|
|
195
|
+
// Get diagnostics and report summary
|
|
196
|
+
const diagnostics = orchestrator.getDiagnostics();
|
|
197
|
+
const reporter = new DiagnosticReporter(diagnostics);
|
|
198
|
+
|
|
199
|
+
// Print summary if there are any issues
|
|
200
|
+
if (diagnostics.count() > 0) {
|
|
201
|
+
info('');
|
|
202
|
+
info(reporter.categorizedSummary());
|
|
203
|
+
|
|
204
|
+
// In verbose mode, print full report
|
|
205
|
+
if (options.verbose) {
|
|
206
|
+
debug('');
|
|
207
|
+
debug(reporter.report({ format: 'text', includeSummary: false }));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Always write diagnostics.log (required for `grafema check` command)
|
|
212
|
+
const writer = new DiagnosticWriter();
|
|
213
|
+
await writer.write(diagnostics, grafemaDir);
|
|
214
|
+
if (options.debug) {
|
|
215
|
+
debug(`Diagnostics written to ${writer.getLogPath(grafemaDir)}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Determine exit code based on severity
|
|
219
|
+
if (diagnostics.hasFatal()) {
|
|
220
|
+
exitCode = 1;
|
|
221
|
+
} else if (diagnostics.hasErrors()) {
|
|
222
|
+
exitCode = 2; // Completed with errors
|
|
223
|
+
} else {
|
|
224
|
+
exitCode = 0; // Success (maybe warnings)
|
|
225
|
+
}
|
|
226
|
+
} catch (e) {
|
|
227
|
+
const diagnostics = orchestrator.getDiagnostics();
|
|
228
|
+
const reporter = new DiagnosticReporter(diagnostics);
|
|
229
|
+
|
|
230
|
+
// Clear progress line in interactive mode
|
|
231
|
+
if (renderer && process.stdout.isTTY) {
|
|
232
|
+
process.stdout.write('\r\x1b[K');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check if this is a strict mode failure (REG-332: structured output)
|
|
236
|
+
if (e instanceof StrictModeFailure) {
|
|
237
|
+
// Format ONLY from diagnostics, not from error.message
|
|
238
|
+
console.error('');
|
|
239
|
+
console.error(`✗ Strict mode: ${e.count} unresolved reference(s) found during ENRICHMENT.`);
|
|
240
|
+
console.error('');
|
|
241
|
+
console.error(reporter.formatStrict(e.diagnostics, {
|
|
242
|
+
verbose: options.verbose,
|
|
243
|
+
suppressedCount: e.suppressedCount, // REG-332
|
|
244
|
+
}));
|
|
245
|
+
console.error('');
|
|
246
|
+
console.error('Run without --strict for graceful degradation, or fix the underlying issues.');
|
|
247
|
+
} else {
|
|
248
|
+
// Generic error handling (non-strict)
|
|
249
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
250
|
+
console.error('');
|
|
251
|
+
console.error(`✗ Analysis failed: ${error.message}`);
|
|
252
|
+
console.error('');
|
|
253
|
+
console.error('→ Run with --debug for detailed diagnostics');
|
|
254
|
+
|
|
255
|
+
if (diagnostics.count() > 0) {
|
|
256
|
+
console.error('');
|
|
257
|
+
console.error(reporter.report({ format: 'text', includeSummary: true }));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Write diagnostics.log in debug mode even on failure
|
|
262
|
+
if (options.debug) {
|
|
263
|
+
const writer = new DiagnosticWriter();
|
|
264
|
+
await writer.write(diagnostics, grafemaDir);
|
|
265
|
+
console.error(`Diagnostics written to ${writer.getLogPath(grafemaDir)}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
exitCode = 1;
|
|
269
|
+
} finally {
|
|
270
|
+
// Stop stats polling
|
|
271
|
+
if (statsInterval) {
|
|
272
|
+
clearInterval(statsInterval);
|
|
273
|
+
statsInterval = null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (backend.connected) {
|
|
277
|
+
await backend.close();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Exit with appropriate code
|
|
281
|
+
// 0 = success, 1 = fatal, 2 = errors
|
|
282
|
+
exitWithCode(exitCode);
|
|
283
|
+
}
|
|
284
|
+
}
|
package/src/commands/check.ts
CHANGED
|
@@ -137,7 +137,7 @@ Examples:
|
|
|
137
137
|
|
|
138
138
|
// Check graph freshness
|
|
139
139
|
const freshnessChecker = new GraphFreshnessChecker();
|
|
140
|
-
const freshness = await freshnessChecker.checkFreshness(backend);
|
|
140
|
+
const freshness = await freshnessChecker.checkFreshness(backend, projectPath);
|
|
141
141
|
|
|
142
142
|
if (!freshness.isFresh) {
|
|
143
143
|
if (options.failOnStale) {
|
|
@@ -292,7 +292,7 @@ async function runBuiltInValidator(
|
|
|
292
292
|
|
|
293
293
|
// Check graph freshness
|
|
294
294
|
const freshnessChecker = new GraphFreshnessChecker();
|
|
295
|
-
const freshness = await freshnessChecker.checkFreshness(backend);
|
|
295
|
+
const freshness = await freshnessChecker.checkFreshness(backend, resolvedPath);
|
|
296
296
|
|
|
297
297
|
if (!freshness.isFresh) {
|
|
298
298
|
if (options.failOnStale) {
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context command — Show deep context for a graph node
|
|
3
|
+
*
|
|
4
|
+
* Displays the full graph neighborhood: source code + all incoming/outgoing edges
|
|
5
|
+
* with code context at each connected node's location.
|
|
6
|
+
*
|
|
7
|
+
* Works for ANY node type: FUNCTION, VARIABLE, MODULE, http:route, CALL, etc.
|
|
8
|
+
*
|
|
9
|
+
* Output is grep-friendly with stable prefixes:
|
|
10
|
+
* -> outgoing edges
|
|
11
|
+
* <- incoming edges
|
|
12
|
+
* > highlighted source lines
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Command } from 'commander';
|
|
16
|
+
import { resolve, join } from 'path';
|
|
17
|
+
import { existsSync } from 'fs';
|
|
18
|
+
import {
|
|
19
|
+
RFDBServerBackend,
|
|
20
|
+
buildNodeContext,
|
|
21
|
+
getNodeDisplayName,
|
|
22
|
+
formatEdgeMetadata,
|
|
23
|
+
STRUCTURAL_EDGE_TYPES,
|
|
24
|
+
} from '@grafema/core';
|
|
25
|
+
import type { NodeContext, EdgeGroup } from '@grafema/core';
|
|
26
|
+
import type { BaseNodeRecord } from '@grafema/types';
|
|
27
|
+
import { getCodePreview, formatCodePreview } from '../utils/codePreview.js';
|
|
28
|
+
import { formatLocation } from '../utils/formatNode.js';
|
|
29
|
+
import { exitWithError } from '../utils/errorFormatter.js';
|
|
30
|
+
import { Spinner } from '../utils/spinner.js';
|
|
31
|
+
|
|
32
|
+
interface ContextOptions {
|
|
33
|
+
project: string;
|
|
34
|
+
json?: boolean;
|
|
35
|
+
lines: string;
|
|
36
|
+
edgeType?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Extended context with CLASS member expansion (REG-411) */
|
|
40
|
+
interface ContextWithMembers extends NodeContext {
|
|
41
|
+
memberContexts?: NodeContext[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const contextCommand = new Command('context')
|
|
45
|
+
.description('Show deep context for a graph node: source code + graph neighborhood')
|
|
46
|
+
.argument('<semanticId>', 'Semantic ID of the node (exact match)')
|
|
47
|
+
.option('-p, --project <path>', 'Project path', '.')
|
|
48
|
+
.option('-j, --json', 'Output as JSON (full dump, no filtering)')
|
|
49
|
+
.option('-l, --lines <n>', 'Context lines around each code reference', '3')
|
|
50
|
+
.option(
|
|
51
|
+
'-e, --edge-type <type>',
|
|
52
|
+
`Filter edges by type (e.g., CALLS, ASSIGNED_FROM, DEPENDS_ON)
|
|
53
|
+
|
|
54
|
+
Multiple types can be comma-separated: --edge-type CALLS,ASSIGNED_FROM
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
grafema context <id> --edge-type CALLS
|
|
58
|
+
grafema context <id> -e DEPENDS_ON,IMPORTS_FROM`
|
|
59
|
+
)
|
|
60
|
+
.addHelpText('after', `
|
|
61
|
+
Output format (grep-friendly):
|
|
62
|
+
-> outgoing edge (this node points to)
|
|
63
|
+
<- incoming edge (points to this node)
|
|
64
|
+
> highlighted source line
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
grafema context "src/app.js->global->FUNCTION->main"
|
|
68
|
+
grafema context "http:route#POST#/api/users" --edge-type ROUTES_TO,HANDLED_BY
|
|
69
|
+
grafema context <id> --json
|
|
70
|
+
grafema context <id> | grep "CALLS"
|
|
71
|
+
grafema context <id> | grep "<-"
|
|
72
|
+
`)
|
|
73
|
+
.action(async (semanticId: string, options: ContextOptions) => {
|
|
74
|
+
const projectPath = resolve(options.project);
|
|
75
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
76
|
+
const dbPath = join(grafemaDir, 'graph.rfdb');
|
|
77
|
+
|
|
78
|
+
if (!existsSync(dbPath)) {
|
|
79
|
+
exitWithError('No graph database found', ['Run: grafema analyze']);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const backend = new RFDBServerBackend({ dbPath });
|
|
83
|
+
await backend.connect();
|
|
84
|
+
|
|
85
|
+
const spinner = new Spinner('Loading context...');
|
|
86
|
+
spinner.start();
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const contextLines = parseInt(options.lines, 10);
|
|
90
|
+
const edgeTypeFilter = options.edgeType
|
|
91
|
+
? new Set(options.edgeType.split(',').map(t => t.trim().toUpperCase()))
|
|
92
|
+
: null;
|
|
93
|
+
|
|
94
|
+
// 1. Look up node by exact semantic ID
|
|
95
|
+
const node = await backend.getNode(semanticId);
|
|
96
|
+
if (!node) {
|
|
97
|
+
spinner.stop();
|
|
98
|
+
exitWithError(`Node not found: "${semanticId}"`, [
|
|
99
|
+
'Use: grafema query "<name>" to find the correct semantic ID',
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 2. Build context (with CLASS member expansion)
|
|
104
|
+
const ctx = await buildContextWithMembers(backend, node, { contextLines, edgeTypeFilter });
|
|
105
|
+
|
|
106
|
+
spinner.stop();
|
|
107
|
+
|
|
108
|
+
// 3. Output
|
|
109
|
+
if (options.json) {
|
|
110
|
+
console.log(JSON.stringify(ctx, null, 2));
|
|
111
|
+
} else {
|
|
112
|
+
printContext(ctx, projectPath, contextLines);
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
spinner.stop();
|
|
116
|
+
await backend.close();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Build context with CLASS member expansion (REG-411)
|
|
122
|
+
*/
|
|
123
|
+
async function buildContextWithMembers(
|
|
124
|
+
backend: RFDBServerBackend,
|
|
125
|
+
node: BaseNodeRecord,
|
|
126
|
+
options: { contextLines: number; edgeTypeFilter: Set<string> | null },
|
|
127
|
+
): Promise<ContextWithMembers> {
|
|
128
|
+
const ctx = await buildNodeContext(backend, node, options);
|
|
129
|
+
|
|
130
|
+
let memberContexts: NodeContext[] | undefined;
|
|
131
|
+
if (node.type === 'CLASS') {
|
|
132
|
+
const outEdges = await backend.getOutgoingEdges(node.id);
|
|
133
|
+
const containsEdges = outEdges.filter(e => e.type === 'CONTAINS');
|
|
134
|
+
const members: NodeContext[] = [];
|
|
135
|
+
for (const edge of containsEdges) {
|
|
136
|
+
const memberNode = await backend.getNode(edge.dst);
|
|
137
|
+
if (memberNode && (memberNode.type === 'FUNCTION' || memberNode.type === 'METHOD')) {
|
|
138
|
+
const memberCtx = await buildNodeContext(backend, memberNode, options);
|
|
139
|
+
members.push(memberCtx);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
members.sort((a, b) => {
|
|
143
|
+
const lineA = (a.node.line as number | undefined) ?? 0;
|
|
144
|
+
const lineB = (b.node.line as number | undefined) ?? 0;
|
|
145
|
+
return lineA - lineB;
|
|
146
|
+
});
|
|
147
|
+
if (members.length > 0) {
|
|
148
|
+
memberContexts = members;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { ...ctx, memberContexts };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Print context to stdout in grep-friendly format
|
|
157
|
+
*/
|
|
158
|
+
function printContext(ctx: ContextWithMembers, projectPath: string, contextLines: number): void {
|
|
159
|
+
const { node, source, outgoing, incoming } = ctx;
|
|
160
|
+
|
|
161
|
+
// Node header
|
|
162
|
+
const displayName = getNodeDisplayName(node);
|
|
163
|
+
console.log(`[${node.type}] ${displayName}`);
|
|
164
|
+
console.log(` ID: ${node.id}`);
|
|
165
|
+
|
|
166
|
+
const loc = formatLocation(node.file, node.line as number | undefined, projectPath);
|
|
167
|
+
if (loc) {
|
|
168
|
+
console.log(` Location: ${loc}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Source code
|
|
172
|
+
if (source) {
|
|
173
|
+
console.log('');
|
|
174
|
+
console.log(` Source (lines ${source.startLine}-${source.endLine}):`);
|
|
175
|
+
const formatted = formatCodePreview(
|
|
176
|
+
{ lines: source.lines, startLine: source.startLine, endLine: source.endLine },
|
|
177
|
+
node.line as number | undefined,
|
|
178
|
+
);
|
|
179
|
+
for (const line of formatted) {
|
|
180
|
+
console.log(` ${line}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Outgoing edges
|
|
185
|
+
if (outgoing.length > 0) {
|
|
186
|
+
console.log('');
|
|
187
|
+
console.log(' Outgoing edges:');
|
|
188
|
+
for (const group of outgoing) {
|
|
189
|
+
printEdgeGroup(group, '->', projectPath, contextLines);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Incoming edges
|
|
194
|
+
if (incoming.length > 0) {
|
|
195
|
+
console.log('');
|
|
196
|
+
console.log(' Incoming edges:');
|
|
197
|
+
for (const group of incoming) {
|
|
198
|
+
printEdgeGroup(group, '<-', projectPath, contextLines);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Member methods (CLASS nodes)
|
|
203
|
+
if (ctx.memberContexts && ctx.memberContexts.length > 0) {
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(` Methods (${ctx.memberContexts.length}):`);
|
|
206
|
+
for (const memberCtx of ctx.memberContexts) {
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(` ── [${memberCtx.node.type}] ${getNodeDisplayName(memberCtx.node)}`);
|
|
209
|
+
const memberLoc = formatLocation(
|
|
210
|
+
memberCtx.node.file,
|
|
211
|
+
memberCtx.node.line as number | undefined,
|
|
212
|
+
projectPath,
|
|
213
|
+
);
|
|
214
|
+
if (memberLoc) {
|
|
215
|
+
console.log(` Location: ${memberLoc}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Method source code
|
|
219
|
+
if (memberCtx.source) {
|
|
220
|
+
console.log('');
|
|
221
|
+
console.log(` Source (lines ${memberCtx.source.startLine}-${memberCtx.source.endLine}):`);
|
|
222
|
+
const formatted = formatCodePreview(
|
|
223
|
+
{
|
|
224
|
+
lines: memberCtx.source.lines,
|
|
225
|
+
startLine: memberCtx.source.startLine,
|
|
226
|
+
endLine: memberCtx.source.endLine,
|
|
227
|
+
},
|
|
228
|
+
memberCtx.node.line as number | undefined,
|
|
229
|
+
);
|
|
230
|
+
for (const line of formatted) {
|
|
231
|
+
console.log(` ${line}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Method edges (non-structural only, to avoid clutter)
|
|
236
|
+
const methodOutgoing = memberCtx.outgoing.filter(g => !STRUCTURAL_EDGE_TYPES.has(g.edgeType));
|
|
237
|
+
const methodIncoming = memberCtx.incoming.filter(g => !STRUCTURAL_EDGE_TYPES.has(g.edgeType));
|
|
238
|
+
|
|
239
|
+
if (methodOutgoing.length > 0) {
|
|
240
|
+
console.log('');
|
|
241
|
+
console.log(' Outgoing edges:');
|
|
242
|
+
for (const group of methodOutgoing) {
|
|
243
|
+
printEdgeGroup(group, '->', projectPath, contextLines, ' ');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (methodIncoming.length > 0) {
|
|
247
|
+
console.log('');
|
|
248
|
+
console.log(' Incoming edges:');
|
|
249
|
+
for (const group of methodIncoming) {
|
|
250
|
+
printEdgeGroup(group, '<-', projectPath, contextLines, ' ');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Summary if no edges and no members
|
|
257
|
+
if (outgoing.length === 0 && incoming.length === 0 && !ctx.memberContexts?.length) {
|
|
258
|
+
console.log('');
|
|
259
|
+
console.log(' No edges found.');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Print a group of edges with the same type
|
|
265
|
+
*/
|
|
266
|
+
function printEdgeGroup(
|
|
267
|
+
group: EdgeGroup,
|
|
268
|
+
direction: '->' | '<-',
|
|
269
|
+
projectPath: string,
|
|
270
|
+
contextLines: number,
|
|
271
|
+
indent = ' ',
|
|
272
|
+
): void {
|
|
273
|
+
const isStructural = STRUCTURAL_EDGE_TYPES.has(group.edgeType);
|
|
274
|
+
|
|
275
|
+
console.log(`${indent}${group.edgeType} (${group.edges.length}):`);
|
|
276
|
+
|
|
277
|
+
for (const { edge, node } of group.edges) {
|
|
278
|
+
if (!node) {
|
|
279
|
+
const danglingId = direction === '->' ? edge.dst : edge.src;
|
|
280
|
+
console.log(`${indent} ${direction} [dangling] ${danglingId}`);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const displayName = getNodeDisplayName(node);
|
|
285
|
+
const loc = formatLocation(node.file, node.line as number | undefined, projectPath);
|
|
286
|
+
const locStr = loc ? ` (${loc})` : '';
|
|
287
|
+
|
|
288
|
+
// Edge metadata inline (if present and useful)
|
|
289
|
+
const metaStr = formatEdgeMetadata(edge);
|
|
290
|
+
|
|
291
|
+
console.log(`${indent} ${direction} [${node.type}] ${displayName}${locStr}${metaStr}`);
|
|
292
|
+
|
|
293
|
+
// Code context for non-structural edges
|
|
294
|
+
if (!isStructural && node.file && node.line && contextLines > 0) {
|
|
295
|
+
const preview = getCodePreview({
|
|
296
|
+
file: node.file,
|
|
297
|
+
line: node.line as number,
|
|
298
|
+
contextBefore: Math.min(contextLines, 2),
|
|
299
|
+
contextAfter: Math.min(contextLines, 2),
|
|
300
|
+
});
|
|
301
|
+
if (preview) {
|
|
302
|
+
const formatted = formatCodePreview(preview, node.line as number);
|
|
303
|
+
for (const line of formatted) {
|
|
304
|
+
console.log(`${indent} ${line}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -527,7 +527,7 @@ export async function checkFreshness(
|
|
|
527
527
|
try {
|
|
528
528
|
await backend.connect();
|
|
529
529
|
const freshnessChecker = new GraphFreshnessChecker();
|
|
530
|
-
const result = await freshnessChecker.checkFreshness(backend);
|
|
530
|
+
const result = await freshnessChecker.checkFreshness(backend, projectPath);
|
|
531
531
|
await backend.close();
|
|
532
532
|
|
|
533
533
|
if (result.isFresh) {
|
package/src/commands/explain.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { Command } from 'commander';
|
|
16
16
|
import { resolve, join, relative, normalize } from 'path';
|
|
17
17
|
import { existsSync, realpathSync } from 'fs';
|
|
18
|
+
import { toRelativeDisplay } from '../utils/pathUtils.js';
|
|
18
19
|
import { RFDBServerBackend, FileExplainer, type EnhancedNode } from '@grafema/core';
|
|
19
20
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
20
21
|
|
|
@@ -85,8 +86,8 @@ If a file shows NOT_ANALYZED:
|
|
|
85
86
|
|
|
86
87
|
try {
|
|
87
88
|
const explainer = new FileExplainer(backend);
|
|
88
|
-
// Query with
|
|
89
|
-
const result = await explainer.explain(
|
|
89
|
+
// Query with relative path since MODULE nodes store relative file paths
|
|
90
|
+
const result = await explainer.explain(relativeFilePath);
|
|
90
91
|
|
|
91
92
|
// Override file in result for display purposes (show relative path)
|
|
92
93
|
result.file = relativeFilePath;
|
|
@@ -166,7 +167,7 @@ function displayNode(node: EnhancedNode, type: string, projectPath: string): voi
|
|
|
166
167
|
|
|
167
168
|
// Line 3: Location
|
|
168
169
|
if (node.file) {
|
|
169
|
-
const relPath =
|
|
170
|
+
const relPath = toRelativeDisplay(node.file, projectPath);
|
|
170
171
|
const loc = node.line ? `${relPath}:${node.line}` : relPath;
|
|
171
172
|
console.log(` Location: ${loc}`);
|
|
172
173
|
}
|
package/src/commands/explore.tsx
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { Command } from 'commander';
|
|
12
|
-
import { resolve, join
|
|
12
|
+
import { resolve, join } from 'path';
|
|
13
|
+
import { toRelativeDisplay } from '../utils/pathUtils.js';
|
|
13
14
|
import { existsSync } from 'fs';
|
|
14
15
|
import { execSync } from 'child_process';
|
|
15
16
|
import React, { useState, useEffect } from 'react';
|
|
@@ -234,6 +235,7 @@ function Explorer({ backend, startNode, projectPath }: ExplorerProps) {
|
|
|
234
235
|
const preview = getCodePreview({
|
|
235
236
|
file: state.currentNode.file,
|
|
236
237
|
line: state.currentNode.line,
|
|
238
|
+
projectPath,
|
|
237
239
|
});
|
|
238
240
|
if (preview) {
|
|
239
241
|
const formatted = formatCodePreview(preview, state.currentNode.line);
|
|
@@ -421,7 +423,7 @@ function Explorer({ backend, startNode, projectPath }: ExplorerProps) {
|
|
|
421
423
|
|
|
422
424
|
const formatLoc = (node: NodeInfo) => {
|
|
423
425
|
if (!node.file) return '';
|
|
424
|
-
const rel =
|
|
426
|
+
const rel = toRelativeDisplay(node.file, projectPath);
|
|
425
427
|
return node.line ? `${rel}:${node.line}` : rel;
|
|
426
428
|
};
|
|
427
429
|
|
|
@@ -1040,7 +1042,7 @@ function outputResults(
|
|
|
1040
1042
|
// Text format
|
|
1041
1043
|
if (target) {
|
|
1042
1044
|
console.log(`${mode === 'callers' ? 'Callers of' : 'Callees of'}: ${target.name}`);
|
|
1043
|
-
console.log(`File: ${
|
|
1045
|
+
console.log(`File: ${toRelativeDisplay(target.file, projectPath)}${target.line ? `:${target.line}` : ''}`);
|
|
1044
1046
|
console.log('');
|
|
1045
1047
|
}
|
|
1046
1048
|
|
|
@@ -1048,7 +1050,7 @@ function outputResults(
|
|
|
1048
1050
|
console.log(` (no ${mode} found)`);
|
|
1049
1051
|
} else {
|
|
1050
1052
|
for (const node of nodes) {
|
|
1051
|
-
const loc =
|
|
1053
|
+
const loc = toRelativeDisplay(node.file, projectPath);
|
|
1052
1054
|
console.log(` ${node.type} ${node.name} (${loc}${node.line ? `:${node.line}` : ''})`);
|
|
1053
1055
|
}
|
|
1054
1056
|
}
|
|
@@ -1063,7 +1065,7 @@ function formatNodeForJson(node: NodeInfo, projectPath: string): object {
|
|
|
1063
1065
|
id: node.id,
|
|
1064
1066
|
type: node.type,
|
|
1065
1067
|
name: node.name,
|
|
1066
|
-
file:
|
|
1068
|
+
file: toRelativeDisplay(node.file, projectPath),
|
|
1067
1069
|
line: node.line,
|
|
1068
1070
|
async: node.async,
|
|
1069
1071
|
exported: node.exported,
|