@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.
Files changed (139) hide show
  1. package/README.md +85 -0
  2. package/dist/cli.js +7 -2
  3. package/dist/cli.js.map +1 -0
  4. package/dist/commands/analyze.d.ts +3 -1
  5. package/dist/commands/analyze.d.ts.map +1 -1
  6. package/dist/commands/analyze.js +8 -266
  7. package/dist/commands/analyze.js.map +1 -0
  8. package/dist/commands/analyzeAction.d.ts +28 -0
  9. package/dist/commands/analyzeAction.d.ts.map +1 -0
  10. package/dist/commands/analyzeAction.js +243 -0
  11. package/dist/commands/analyzeAction.js.map +1 -0
  12. package/dist/commands/check.d.ts +2 -6
  13. package/dist/commands/check.d.ts.map +1 -1
  14. package/dist/commands/check.js +34 -48
  15. package/dist/commands/check.js.map +1 -0
  16. package/dist/commands/context.d.ts +16 -0
  17. package/dist/commands/context.d.ts.map +1 -0
  18. package/dist/commands/context.js +238 -0
  19. package/dist/commands/context.js.map +1 -0
  20. package/dist/commands/coverage.js +1 -0
  21. package/dist/commands/coverage.js.map +1 -0
  22. package/dist/commands/doctor/checks.d.ts.map +1 -1
  23. package/dist/commands/doctor/checks.js +10 -6
  24. package/dist/commands/doctor/checks.js.map +1 -0
  25. package/dist/commands/doctor/output.js +1 -0
  26. package/dist/commands/doctor/output.js.map +1 -0
  27. package/dist/commands/doctor/types.js +1 -0
  28. package/dist/commands/doctor/types.js.map +1 -0
  29. package/dist/commands/doctor.js +1 -0
  30. package/dist/commands/doctor.js.map +1 -0
  31. package/dist/commands/explain.d.ts.map +1 -1
  32. package/dist/commands/explain.js +5 -3
  33. package/dist/commands/explain.js.map +1 -0
  34. package/dist/commands/explore.d.ts.map +1 -1
  35. package/dist/commands/explore.js +9 -4
  36. package/dist/commands/explore.js.map +1 -0
  37. package/dist/commands/file.d.ts +15 -0
  38. package/dist/commands/file.d.ts.map +1 -0
  39. package/dist/commands/file.js +144 -0
  40. package/dist/commands/file.js.map +1 -0
  41. package/dist/commands/get.d.ts.map +1 -1
  42. package/dist/commands/get.js +7 -0
  43. package/dist/commands/get.js.map +1 -0
  44. package/dist/commands/impact.d.ts.map +1 -1
  45. package/dist/commands/impact.js +3 -3
  46. package/dist/commands/impact.js.map +1 -0
  47. package/dist/commands/init.d.ts.map +1 -1
  48. package/dist/commands/init.js +20 -2
  49. package/dist/commands/init.js.map +1 -0
  50. package/dist/commands/ls.d.ts.map +1 -1
  51. package/dist/commands/ls.js +10 -2
  52. package/dist/commands/ls.js.map +1 -0
  53. package/dist/commands/overview.d.ts.map +1 -1
  54. package/dist/commands/overview.js +1 -0
  55. package/dist/commands/overview.js.map +1 -0
  56. package/dist/commands/query.d.ts +8 -0
  57. package/dist/commands/query.d.ts.map +1 -1
  58. package/dist/commands/query.js +217 -43
  59. package/dist/commands/query.js.map +1 -0
  60. package/dist/commands/schema.d.ts.map +1 -1
  61. package/dist/commands/schema.js +4 -2
  62. package/dist/commands/schema.js.map +1 -0
  63. package/dist/commands/server.d.ts +2 -1
  64. package/dist/commands/server.d.ts.map +1 -1
  65. package/dist/commands/server.js +76 -14
  66. package/dist/commands/server.js.map +1 -0
  67. package/dist/commands/setup-skill.d.ts +17 -0
  68. package/dist/commands/setup-skill.d.ts.map +1 -0
  69. package/dist/commands/setup-skill.js +131 -0
  70. package/dist/commands/setup-skill.js.map +1 -0
  71. package/dist/commands/stats.js +1 -0
  72. package/dist/commands/stats.js.map +1 -0
  73. package/dist/commands/trace.d.ts.map +1 -1
  74. package/dist/commands/trace.js +21 -10
  75. package/dist/commands/trace.js.map +1 -0
  76. package/dist/commands/types.js +1 -0
  77. package/dist/commands/types.js.map +1 -0
  78. package/dist/plugins/builtinPlugins.d.ts +10 -0
  79. package/dist/plugins/builtinPlugins.d.ts.map +1 -0
  80. package/dist/plugins/builtinPlugins.js +68 -0
  81. package/dist/plugins/builtinPlugins.js.map +1 -0
  82. package/dist/plugins/pluginLoader.d.ts +16 -0
  83. package/dist/plugins/pluginLoader.d.ts.map +1 -0
  84. package/dist/plugins/pluginLoader.js +101 -0
  85. package/dist/plugins/pluginLoader.js.map +1 -0
  86. package/dist/plugins/pluginResolver.js +38 -0
  87. package/dist/utils/codePreview.d.ts +1 -0
  88. package/dist/utils/codePreview.d.ts.map +1 -1
  89. package/dist/utils/codePreview.js +6 -3
  90. package/dist/utils/codePreview.js.map +1 -0
  91. package/dist/utils/errorFormatter.js +1 -0
  92. package/dist/utils/errorFormatter.js.map +1 -0
  93. package/dist/utils/formatNode.d.ts +1 -1
  94. package/dist/utils/formatNode.d.ts.map +1 -1
  95. package/dist/utils/formatNode.js +3 -2
  96. package/dist/utils/formatNode.js.map +1 -0
  97. package/dist/utils/pathUtils.d.ts +2 -0
  98. package/dist/utils/pathUtils.d.ts.map +1 -0
  99. package/dist/utils/pathUtils.js +9 -0
  100. package/dist/utils/pathUtils.js.map +1 -0
  101. package/dist/utils/progressRenderer.d.ts +119 -0
  102. package/dist/utils/progressRenderer.d.ts.map +1 -0
  103. package/dist/utils/progressRenderer.js +245 -0
  104. package/dist/utils/progressRenderer.js.map +1 -0
  105. package/dist/utils/spinner.d.ts +39 -0
  106. package/dist/utils/spinner.d.ts.map +1 -0
  107. package/dist/utils/spinner.js +84 -0
  108. package/dist/utils/spinner.js.map +1 -0
  109. package/package.json +8 -9
  110. package/skills/grafema-codebase-analysis/SKILL.md +295 -0
  111. package/skills/grafema-codebase-analysis/references/node-edge-types.md +123 -0
  112. package/skills/grafema-codebase-analysis/references/query-patterns.md +205 -0
  113. package/src/cli.ts +8 -2
  114. package/src/commands/analyze.ts +7 -342
  115. package/src/commands/analyzeAction.ts +284 -0
  116. package/src/commands/check.ts +38 -70
  117. package/src/commands/context.ts +309 -0
  118. package/src/commands/doctor/checks.ts +9 -6
  119. package/src/commands/explain.ts +4 -3
  120. package/src/commands/explore.tsx +15 -9
  121. package/src/commands/file.ts +179 -0
  122. package/src/commands/get.ts +8 -0
  123. package/src/commands/impact.ts +3 -4
  124. package/src/commands/init.ts +19 -3
  125. package/src/commands/ls.ts +11 -2
  126. package/src/commands/overview.ts +0 -4
  127. package/src/commands/query.ts +235 -44
  128. package/src/commands/schema.ts +3 -2
  129. package/src/commands/server.ts +85 -15
  130. package/src/commands/setup-skill.ts +162 -0
  131. package/src/commands/trace.ts +18 -9
  132. package/src/plugins/builtinPlugins.ts +108 -0
  133. package/src/plugins/pluginLoader.ts +123 -0
  134. package/src/plugins/pluginResolver.js +38 -0
  135. package/src/utils/codePreview.ts +7 -3
  136. package/src/utils/formatNode.ts +3 -3
  137. package/src/utils/pathUtils.ts +9 -0
  138. package/src/utils/progressRenderer.ts +288 -0
  139. 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: `Unknown plugin(s): ${unknownPlugins.join(', ')}`,
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: ${(err as Error).message}`,
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: ${(err as Error).message}`,
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: ${(err as Error).message}`,
556
+ message: `Could not check freshness: ${message}`,
554
557
  };
555
558
  }
556
559
  }
@@ -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 absolute path since graph stores absolute paths
89
- const result = await explainer.explain(absoluteFilePath);
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 = relative(projectPath, node.file);
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
  }
@@ -9,7 +9,8 @@
9
9
  */
10
10
 
11
11
  import { Command } from 'commander';
12
- import { resolve, join, relative } from 'path';
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: (err as Error).message,
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: (err as Error).message,
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: (err as Error).message,
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 = relative(projectPath, node.file);
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
- exitWithError(`Explore failed: ${(err as Error).message}`);
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: ${relative(projectPath, target.file)}${target.line ? `:${target.line}` : ''}`);
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 = relative(projectPath, node.file);
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: relative(projectPath, node.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
+ }