@grafema/cli 0.2.4-beta → 0.2.6-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/README.md +85 -0
- package/dist/cli.js +7 -2
- package/dist/cli.js.map +1 -0
- package/dist/commands/analyze.d.ts +3 -1
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +8 -266
- package/dist/commands/analyze.js.map +1 -0
- 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.d.ts +2 -6
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +34 -48
- package/dist/commands/check.js.map +1 -0
- 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/coverage.js +1 -0
- package/dist/commands/coverage.js.map +1 -0
- package/dist/commands/doctor/checks.d.ts.map +1 -1
- package/dist/commands/doctor/checks.js +10 -6
- package/dist/commands/doctor/checks.js.map +1 -0
- package/dist/commands/doctor/output.js +1 -0
- package/dist/commands/doctor/output.js.map +1 -0
- package/dist/commands/doctor/types.js +1 -0
- package/dist/commands/doctor/types.js.map +1 -0
- package/dist/commands/doctor.js +1 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/explain.d.ts.map +1 -1
- package/dist/commands/explain.js +5 -3
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/explore.d.ts.map +1 -1
- package/dist/commands/explore.js +9 -4
- package/dist/commands/explore.js.map +1 -0
- 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/get.d.ts.map +1 -1
- package/dist/commands/get.js +7 -0
- package/dist/commands/get.js.map +1 -0
- package/dist/commands/impact.d.ts.map +1 -1
- package/dist/commands/impact.js +3 -3
- package/dist/commands/impact.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +20 -2
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +10 -2
- package/dist/commands/ls.js.map +1 -0
- package/dist/commands/overview.d.ts.map +1 -1
- package/dist/commands/overview.js +1 -0
- package/dist/commands/overview.js.map +1 -0
- package/dist/commands/query.d.ts +8 -0
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +217 -43
- package/dist/commands/query.js.map +1 -0
- package/dist/commands/schema.d.ts.map +1 -1
- package/dist/commands/schema.js +4 -2
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/server.d.ts +2 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +76 -14
- package/dist/commands/server.js.map +1 -0
- 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/stats.js +1 -0
- package/dist/commands/stats.js.map +1 -0
- package/dist/commands/trace.d.ts.map +1 -1
- package/dist/commands/trace.js +21 -10
- package/dist/commands/trace.js.map +1 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands/types.js.map +1 -0
- 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 +6 -3
- package/dist/utils/codePreview.js.map +1 -0
- package/dist/utils/errorFormatter.js +1 -0
- package/dist/utils/errorFormatter.js.map +1 -0
- package/dist/utils/formatNode.d.ts +1 -1
- package/dist/utils/formatNode.d.ts.map +1 -1
- package/dist/utils/formatNode.js +3 -2
- package/dist/utils/formatNode.js.map +1 -0
- 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 +119 -0
- package/dist/utils/progressRenderer.d.ts.map +1 -0
- package/dist/utils/progressRenderer.js +245 -0
- package/dist/utils/progressRenderer.js.map +1 -0
- package/dist/utils/spinner.d.ts +39 -0
- package/dist/utils/spinner.d.ts.map +1 -0
- package/dist/utils/spinner.js +84 -0
- package/dist/utils/spinner.js.map +1 -0
- package/package.json +8 -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 +7 -342
- package/src/commands/analyzeAction.ts +284 -0
- package/src/commands/check.ts +38 -70
- package/src/commands/context.ts +309 -0
- package/src/commands/doctor/checks.ts +9 -6
- package/src/commands/explain.ts +4 -3
- package/src/commands/explore.tsx +15 -9
- package/src/commands/file.ts +179 -0
- package/src/commands/get.ts +8 -0
- package/src/commands/impact.ts +3 -4
- package/src/commands/init.ts +19 -3
- package/src/commands/ls.ts +11 -2
- package/src/commands/overview.ts +0 -4
- package/src/commands/query.ts +235 -44
- package/src/commands/schema.ts +3 -2
- package/src/commands/server.ts +85 -15
- 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 +288 -0
- package/src/utils/spinner.ts +94 -0
|
@@ -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
|
+
}
|
|
@@ -159,8 +159,8 @@ export async function checkConfigValidity(
|
|
|
159
159
|
return {
|
|
160
160
|
name: 'config',
|
|
161
161
|
status: 'warn',
|
|
162
|
-
message: `
|
|
163
|
-
recommendation: 'Check plugin names for typos. Run: grafema doctor --verbose for available plugins',
|
|
162
|
+
message: `Plugin(s) not found: ${unknownPlugins.join(', ')} (will be skipped during analysis)`,
|
|
163
|
+
recommendation: 'Check plugin names for typos or add custom plugins to .grafema/plugins/. Run: grafema doctor --verbose for available plugins',
|
|
164
164
|
details: { unknownPlugins },
|
|
165
165
|
};
|
|
166
166
|
}
|
|
@@ -357,10 +357,11 @@ export async function checkGraphStats(
|
|
|
357
357
|
},
|
|
358
358
|
};
|
|
359
359
|
} catch (err) {
|
|
360
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
360
361
|
return {
|
|
361
362
|
name: 'graph_stats',
|
|
362
363
|
status: 'warn',
|
|
363
|
-
message: `Could not read graph stats: ${
|
|
364
|
+
message: `Could not read graph stats: ${message}`,
|
|
364
365
|
};
|
|
365
366
|
}
|
|
366
367
|
}
|
|
@@ -496,10 +497,11 @@ export async function checkConnectivity(
|
|
|
496
497
|
details: { unreachableCount, percentage, byType },
|
|
497
498
|
};
|
|
498
499
|
} catch (err) {
|
|
500
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
499
501
|
return {
|
|
500
502
|
name: 'connectivity',
|
|
501
503
|
status: 'warn',
|
|
502
|
-
message: `Could not check connectivity: ${
|
|
504
|
+
message: `Could not check connectivity: ${message}`,
|
|
503
505
|
};
|
|
504
506
|
}
|
|
505
507
|
}
|
|
@@ -525,7 +527,7 @@ export async function checkFreshness(
|
|
|
525
527
|
try {
|
|
526
528
|
await backend.connect();
|
|
527
529
|
const freshnessChecker = new GraphFreshnessChecker();
|
|
528
|
-
const result = await freshnessChecker.checkFreshness(backend);
|
|
530
|
+
const result = await freshnessChecker.checkFreshness(backend, projectPath);
|
|
529
531
|
await backend.close();
|
|
530
532
|
|
|
531
533
|
if (result.isFresh) {
|
|
@@ -547,10 +549,11 @@ export async function checkFreshness(
|
|
|
547
549
|
},
|
|
548
550
|
};
|
|
549
551
|
} catch (err) {
|
|
552
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
550
553
|
return {
|
|
551
554
|
name: 'freshness',
|
|
552
555
|
status: 'warn',
|
|
553
|
-
message: `Could not check freshness: ${
|
|
556
|
+
message: `Could not check freshness: ${message}`,
|
|
554
557
|
};
|
|
555
558
|
}
|
|
556
559
|
}
|
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';
|
|
@@ -160,9 +161,10 @@ function Explorer({ backend, startNode, projectPath }: ExplorerProps) {
|
|
|
160
161
|
}));
|
|
161
162
|
}
|
|
162
163
|
} catch (err) {
|
|
164
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
163
165
|
setState(s => ({
|
|
164
166
|
...s,
|
|
165
|
-
error:
|
|
167
|
+
error: message,
|
|
166
168
|
loading: false,
|
|
167
169
|
}));
|
|
168
170
|
}
|
|
@@ -233,6 +235,7 @@ function Explorer({ backend, startNode, projectPath }: ExplorerProps) {
|
|
|
233
235
|
const preview = getCodePreview({
|
|
234
236
|
file: state.currentNode.file,
|
|
235
237
|
line: state.currentNode.line,
|
|
238
|
+
projectPath,
|
|
236
239
|
});
|
|
237
240
|
if (preview) {
|
|
238
241
|
const formatted = formatCodePreview(preview, state.currentNode.line);
|
|
@@ -387,9 +390,10 @@ function Explorer({ backend, startNode, projectPath }: ExplorerProps) {
|
|
|
387
390
|
error: results.length === 0 ? `No results for "${query}"` : null,
|
|
388
391
|
}));
|
|
389
392
|
} catch (err) {
|
|
393
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
390
394
|
setState(s => ({
|
|
391
395
|
...s,
|
|
392
|
-
error:
|
|
396
|
+
error: message,
|
|
393
397
|
loading: false,
|
|
394
398
|
}));
|
|
395
399
|
}
|
|
@@ -408,9 +412,10 @@ function Explorer({ backend, startNode, projectPath }: ExplorerProps) {
|
|
|
408
412
|
loading: false,
|
|
409
413
|
}));
|
|
410
414
|
} catch (err) {
|
|
415
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
411
416
|
setState(s => ({
|
|
412
417
|
...s,
|
|
413
|
-
error:
|
|
418
|
+
error: message,
|
|
414
419
|
loading: false,
|
|
415
420
|
}));
|
|
416
421
|
}
|
|
@@ -418,7 +423,7 @@ function Explorer({ backend, startNode, projectPath }: ExplorerProps) {
|
|
|
418
423
|
|
|
419
424
|
const formatLoc = (node: NodeInfo) => {
|
|
420
425
|
if (!node.file) return '';
|
|
421
|
-
const rel =
|
|
426
|
+
const rel = toRelativeDisplay(node.file, projectPath);
|
|
422
427
|
return node.line ? `${rel}:${node.line}` : rel;
|
|
423
428
|
};
|
|
424
429
|
|
|
@@ -1010,7 +1015,8 @@ async function runBatchExplore(
|
|
|
1010
1015
|
outputResults(callees, 'callees', useJson, projectPath, target);
|
|
1011
1016
|
}
|
|
1012
1017
|
} catch (err) {
|
|
1013
|
-
|
|
1018
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1019
|
+
exitWithError(`Explore failed: ${message}`);
|
|
1014
1020
|
}
|
|
1015
1021
|
}
|
|
1016
1022
|
|
|
@@ -1036,7 +1042,7 @@ function outputResults(
|
|
|
1036
1042
|
// Text format
|
|
1037
1043
|
if (target) {
|
|
1038
1044
|
console.log(`${mode === 'callers' ? 'Callers of' : 'Callees of'}: ${target.name}`);
|
|
1039
|
-
console.log(`File: ${
|
|
1045
|
+
console.log(`File: ${toRelativeDisplay(target.file, projectPath)}${target.line ? `:${target.line}` : ''}`);
|
|
1040
1046
|
console.log('');
|
|
1041
1047
|
}
|
|
1042
1048
|
|
|
@@ -1044,7 +1050,7 @@ function outputResults(
|
|
|
1044
1050
|
console.log(` (no ${mode} found)`);
|
|
1045
1051
|
} else {
|
|
1046
1052
|
for (const node of nodes) {
|
|
1047
|
-
const loc =
|
|
1053
|
+
const loc = toRelativeDisplay(node.file, projectPath);
|
|
1048
1054
|
console.log(` ${node.type} ${node.name} (${loc}${node.line ? `:${node.line}` : ''})`);
|
|
1049
1055
|
}
|
|
1050
1056
|
}
|
|
@@ -1059,7 +1065,7 @@ function formatNodeForJson(node: NodeInfo, projectPath: string): object {
|
|
|
1059
1065
|
id: node.id,
|
|
1060
1066
|
type: node.type,
|
|
1061
1067
|
name: node.name,
|
|
1062
|
-
file:
|
|
1068
|
+
file: toRelativeDisplay(node.file, projectPath),
|
|
1063
1069
|
line: node.line,
|
|
1064
1070
|
async: node.async,
|
|
1065
1071
|
exported: node.exported,
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File command - Show structured overview of a file's entities and relationships
|
|
3
|
+
*
|
|
4
|
+
* Purpose: Give a file-level summary with imports, exports, classes, functions,
|
|
5
|
+
* and their key relationships (calls, extends, assigned-from).
|
|
6
|
+
*
|
|
7
|
+
* This fills the gap between:
|
|
8
|
+
* - explain (lists ALL nodes flat, no relationships)
|
|
9
|
+
* - context (shows ONE node's full neighborhood)
|
|
10
|
+
*
|
|
11
|
+
* @see REG-412
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Command } from 'commander';
|
|
15
|
+
import { resolve, join, relative, normalize } from 'path';
|
|
16
|
+
import { existsSync, realpathSync } from 'fs';
|
|
17
|
+
import { RFDBServerBackend, FileOverview } from '@grafema/core';
|
|
18
|
+
import type { FileOverviewResult, FunctionOverview } from '@grafema/core';
|
|
19
|
+
import { exitWithError } from '../utils/errorFormatter.js';
|
|
20
|
+
import { Spinner } from '../utils/spinner.js';
|
|
21
|
+
|
|
22
|
+
interface FileOptions {
|
|
23
|
+
project: string;
|
|
24
|
+
json?: boolean;
|
|
25
|
+
edges?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const fileCommand = new Command('file')
|
|
29
|
+
.description(
|
|
30
|
+
'Show structured overview of a file: imports, exports, classes, functions with relationships'
|
|
31
|
+
)
|
|
32
|
+
.argument('<path>', 'File path to analyze')
|
|
33
|
+
.option('-p, --project <path>', 'Project path', '.')
|
|
34
|
+
.option('-j, --json', 'Output as JSON')
|
|
35
|
+
.option('--no-edges', 'Skip edge resolution (faster, just list entities)')
|
|
36
|
+
.addHelpText('after', `
|
|
37
|
+
Examples:
|
|
38
|
+
grafema file src/app.ts Show file overview with relationships
|
|
39
|
+
grafema file src/app.ts --json Output as JSON for scripting
|
|
40
|
+
grafema file src/app.ts --no-edges Fast mode: just list entities
|
|
41
|
+
grafema file ./src/utils.js Works with relative paths
|
|
42
|
+
|
|
43
|
+
Output shows:
|
|
44
|
+
- Imports (module sources and specifiers)
|
|
45
|
+
- Exports (named and default)
|
|
46
|
+
- Classes with methods and their calls
|
|
47
|
+
- Functions with their calls
|
|
48
|
+
- Variables with assignment sources
|
|
49
|
+
|
|
50
|
+
Use 'grafema context <id>' to dive deeper into any specific entity.
|
|
51
|
+
`)
|
|
52
|
+
.action(async (file: string, options: FileOptions) => {
|
|
53
|
+
const projectPath = resolve(options.project);
|
|
54
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
55
|
+
const dbPath = join(grafemaDir, 'graph.rfdb');
|
|
56
|
+
|
|
57
|
+
if (!existsSync(dbPath)) {
|
|
58
|
+
exitWithError('No graph database found', [
|
|
59
|
+
'Run: grafema init && grafema analyze',
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Path resolution (same as explain command)
|
|
64
|
+
let filePath = file;
|
|
65
|
+
|
|
66
|
+
if (file.startsWith('./') || file.startsWith('../')) {
|
|
67
|
+
filePath = normalize(file).replace(/^\.\//, '');
|
|
68
|
+
} else if (resolve(file) === file) {
|
|
69
|
+
filePath = relative(projectPath, file);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const resolvedPath = resolve(projectPath, filePath);
|
|
73
|
+
if (!existsSync(resolvedPath)) {
|
|
74
|
+
exitWithError(`File not found: ${file}`, [
|
|
75
|
+
'Check the file path and try again',
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const absoluteFilePath = realpathSync(resolvedPath);
|
|
80
|
+
const relativeFilePath = relative(projectPath, absoluteFilePath);
|
|
81
|
+
|
|
82
|
+
const backend = new RFDBServerBackend({ dbPath });
|
|
83
|
+
await backend.connect();
|
|
84
|
+
|
|
85
|
+
const spinner = new Spinner('Loading file overview...');
|
|
86
|
+
spinner.start();
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const overview = new FileOverview(backend);
|
|
90
|
+
const result = await overview.getOverview(relativeFilePath, {
|
|
91
|
+
includeEdges: options.edges !== false,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
result.file = relativeFilePath;
|
|
95
|
+
|
|
96
|
+
spinner.stop();
|
|
97
|
+
|
|
98
|
+
if (options.json) {
|
|
99
|
+
console.log(JSON.stringify(result, null, 2));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
printFileOverview(result);
|
|
104
|
+
} finally {
|
|
105
|
+
spinner.stop();
|
|
106
|
+
await backend.close();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
function printFileOverview(result: FileOverviewResult): void {
|
|
111
|
+
console.log(`Module: ${result.file}`);
|
|
112
|
+
|
|
113
|
+
if (result.status === 'NOT_ANALYZED') {
|
|
114
|
+
console.log('Status: NOT_ANALYZED');
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log('This file has not been analyzed yet.');
|
|
117
|
+
console.log('Run: grafema analyze');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (result.imports.length > 0) {
|
|
122
|
+
const importSources = result.imports.map(i => i.source);
|
|
123
|
+
console.log(`Imports: ${importSources.join(', ')}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (result.exports.length > 0) {
|
|
127
|
+
const exportNames = result.exports.map(e =>
|
|
128
|
+
e.isDefault ? `${e.name} (default)` : e.name
|
|
129
|
+
);
|
|
130
|
+
console.log(`Exports: ${exportNames.join(', ')}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (result.classes.length > 0) {
|
|
134
|
+
console.log('');
|
|
135
|
+
console.log('Classes:');
|
|
136
|
+
for (const cls of result.classes) {
|
|
137
|
+
const extendsStr = cls.extends ? ` extends ${cls.extends}` : '';
|
|
138
|
+
const lineStr = cls.line ? ` (line ${cls.line})` : '';
|
|
139
|
+
console.log(` ${cls.name}${extendsStr}${lineStr}`);
|
|
140
|
+
|
|
141
|
+
for (const method of cls.methods) {
|
|
142
|
+
printFunctionLine(method, ' ');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (result.functions.length > 0) {
|
|
148
|
+
console.log('');
|
|
149
|
+
console.log('Functions:');
|
|
150
|
+
for (const fn of result.functions) {
|
|
151
|
+
printFunctionLine(fn, ' ');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (result.variables.length > 0) {
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log('Variables:');
|
|
158
|
+
for (const v of result.variables) {
|
|
159
|
+
const lineStr = v.line ? `(line ${v.line})` : '';
|
|
160
|
+
const assignStr = v.assignedFrom ? ` = ${v.assignedFrom}` : '';
|
|
161
|
+
console.log(` ${v.kind} ${v.name}${assignStr} ${lineStr}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function printFunctionLine(fn: FunctionOverview, indent: string): void {
|
|
167
|
+
const asyncStr = fn.async ? 'async ' : '';
|
|
168
|
+
const paramsStr = fn.params ? `(${fn.params.join(', ')})` : '()';
|
|
169
|
+
const lineStr = fn.line ? `(line ${fn.line})` : '';
|
|
170
|
+
|
|
171
|
+
let callsStr = '';
|
|
172
|
+
if (fn.calls.length > 0) {
|
|
173
|
+
callsStr = ` -> ${fn.calls.join(', ')}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(
|
|
177
|
+
`${indent}${asyncStr}${fn.name}${paramsStr}${callsStr} ${lineStr}`
|
|
178
|
+
);
|
|
179
|
+
}
|