@grafema/cli 0.1.1-alpha → 0.2.1-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 (79) hide show
  1. package/dist/cli.js +10 -0
  2. package/dist/commands/analyze.d.ts.map +1 -1
  3. package/dist/commands/analyze.js +69 -11
  4. package/dist/commands/check.d.ts +6 -0
  5. package/dist/commands/check.d.ts.map +1 -1
  6. package/dist/commands/check.js +177 -1
  7. package/dist/commands/coverage.d.ts.map +1 -1
  8. package/dist/commands/coverage.js +7 -0
  9. package/dist/commands/doctor/checks.d.ts +55 -0
  10. package/dist/commands/doctor/checks.d.ts.map +1 -0
  11. package/dist/commands/doctor/checks.js +534 -0
  12. package/dist/commands/doctor/output.d.ts +20 -0
  13. package/dist/commands/doctor/output.d.ts.map +1 -0
  14. package/dist/commands/doctor/output.js +94 -0
  15. package/dist/commands/doctor/types.d.ts +42 -0
  16. package/dist/commands/doctor/types.d.ts.map +1 -0
  17. package/dist/commands/doctor/types.js +4 -0
  18. package/dist/commands/doctor.d.ts +17 -0
  19. package/dist/commands/doctor.d.ts.map +1 -0
  20. package/dist/commands/doctor.js +80 -0
  21. package/dist/commands/explain.d.ts +16 -0
  22. package/dist/commands/explain.d.ts.map +1 -0
  23. package/dist/commands/explain.js +145 -0
  24. package/dist/commands/explore.d.ts +7 -1
  25. package/dist/commands/explore.d.ts.map +1 -1
  26. package/dist/commands/explore.js +204 -85
  27. package/dist/commands/get.d.ts.map +1 -1
  28. package/dist/commands/get.js +16 -4
  29. package/dist/commands/impact.d.ts.map +1 -1
  30. package/dist/commands/impact.js +48 -50
  31. package/dist/commands/init.d.ts.map +1 -1
  32. package/dist/commands/init.js +93 -15
  33. package/dist/commands/ls.d.ts +14 -0
  34. package/dist/commands/ls.d.ts.map +1 -0
  35. package/dist/commands/ls.js +132 -0
  36. package/dist/commands/overview.d.ts.map +1 -1
  37. package/dist/commands/overview.js +15 -2
  38. package/dist/commands/query.d.ts +98 -0
  39. package/dist/commands/query.d.ts.map +1 -1
  40. package/dist/commands/query.js +549 -136
  41. package/dist/commands/schema.d.ts +13 -0
  42. package/dist/commands/schema.d.ts.map +1 -0
  43. package/dist/commands/schema.js +279 -0
  44. package/dist/commands/server.d.ts.map +1 -1
  45. package/dist/commands/server.js +13 -6
  46. package/dist/commands/stats.d.ts.map +1 -1
  47. package/dist/commands/stats.js +7 -0
  48. package/dist/commands/trace.d.ts +73 -0
  49. package/dist/commands/trace.d.ts.map +1 -1
  50. package/dist/commands/trace.js +500 -5
  51. package/dist/commands/types.d.ts +12 -0
  52. package/dist/commands/types.d.ts.map +1 -0
  53. package/dist/commands/types.js +79 -0
  54. package/dist/utils/formatNode.d.ts +13 -0
  55. package/dist/utils/formatNode.d.ts.map +1 -1
  56. package/dist/utils/formatNode.js +35 -2
  57. package/package.json +3 -3
  58. package/src/cli.ts +10 -0
  59. package/src/commands/analyze.ts +84 -9
  60. package/src/commands/check.ts +201 -0
  61. package/src/commands/coverage.ts +7 -0
  62. package/src/commands/doctor/checks.ts +612 -0
  63. package/src/commands/doctor/output.ts +115 -0
  64. package/src/commands/doctor/types.ts +45 -0
  65. package/src/commands/doctor.ts +106 -0
  66. package/src/commands/explain.ts +173 -0
  67. package/src/commands/explore.tsx +247 -97
  68. package/src/commands/get.ts +20 -6
  69. package/src/commands/impact.ts +55 -61
  70. package/src/commands/init.ts +101 -14
  71. package/src/commands/ls.ts +166 -0
  72. package/src/commands/overview.ts +15 -2
  73. package/src/commands/query.ts +643 -149
  74. package/src/commands/schema.ts +345 -0
  75. package/src/commands/server.ts +13 -6
  76. package/src/commands/stats.ts +7 -0
  77. package/src/commands/trace.ts +647 -6
  78. package/src/commands/types.ts +94 -0
  79. package/src/utils/formatNode.ts +42 -2
@@ -16,6 +16,13 @@ export const getCommand = new Command('get')
16
16
  .argument('<semantic-id>', 'Semantic ID of the node (e.g., "file.js->scope->TYPE->name")')
17
17
  .option('-p, --project <path>', 'Project path', '.')
18
18
  .option('-j, --json', 'Output as JSON')
19
+ .addHelpText('after', `
20
+ Examples:
21
+ grafema get "src/auth.js->authenticate->FUNCTION" Get function node
22
+ grafema get "src/models/User.js->User->CLASS" Get class node
23
+ grafema get "src/api.js->config->VARIABLE" Get variable node
24
+ grafema get "src/auth.js->authenticate->FUNCTION" -j Output as JSON with edges
25
+ `)
19
26
  .action(async (semanticId, options) => {
20
27
  const projectPath = resolve(options.project);
21
28
  const grafemaDir = join(projectPath, '.grafema');
@@ -54,12 +61,12 @@ export const getCommand = new Command('get')
54
61
  async function outputJSON(backend, node, incomingEdges, outgoingEdges) {
55
62
  // Fetch target node names for all edges
56
63
  const incomingWithNames = await Promise.all(incomingEdges.map(async (edge) => ({
57
- edgeType: edge.edgeType || edge.type || 'UNKNOWN',
64
+ edgeType: edge.type || 'UNKNOWN',
58
65
  targetId: edge.src,
59
66
  targetName: await getNodeName(backend, edge.src),
60
67
  })));
61
68
  const outgoingWithNames = await Promise.all(outgoingEdges.map(async (edge) => ({
62
- edgeType: edge.edgeType || edge.type || 'UNKNOWN',
69
+ edgeType: edge.type || 'UNKNOWN',
63
70
  targetId: edge.dst,
64
71
  targetName: await getNodeName(backend, edge.dst),
65
72
  })));
@@ -93,6 +100,9 @@ async function outputText(backend, node, incomingEdges, outgoingEdges, projectPa
93
100
  name: node.name || '',
94
101
  file: node.file || '',
95
102
  line: node.line,
103
+ method: node.method,
104
+ path: node.path,
105
+ url: node.url,
96
106
  };
97
107
  // Display node details
98
108
  console.log(formatNodeDisplay(nodeInfo, { projectPath }));
@@ -124,7 +134,7 @@ async function displayEdges(backend, direction, edges, getTargetId) {
124
134
  // Group edges by type
125
135
  const byType = new Map();
126
136
  for (const edge of edges) {
127
- const edgeType = edge.edgeType || edge.type || 'UNKNOWN';
137
+ const edgeType = edge.type || 'UNKNOWN';
128
138
  const targetId = getTargetId(edge);
129
139
  const targetName = await getNodeName(backend, targetId);
130
140
  if (!byType.has(edgeType)) {
@@ -173,11 +183,13 @@ async function getNodeName(backend, nodeId) {
173
183
  return '';
174
184
  }
175
185
  /**
176
- * Extract metadata fields (exclude standard fields)
186
+ * Extract metadata fields (exclude standard and display fields)
177
187
  */
178
188
  function getMetadataFields(node) {
179
189
  const standardFields = new Set([
180
190
  'id', 'type', 'nodeType', 'name', 'file', 'line',
191
+ // Display fields shown in primary line for HTTP nodes
192
+ 'method', 'path', 'url',
181
193
  ]);
182
194
  const metadata = {};
183
195
  for (const [key, value] of Object.entries(node)) {
@@ -1 +1 @@
1
- {"version":3,"file":"impact.d.ts","sourceRoot":"","sources":["../../src/commands/impact.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8BpC,eAAO,MAAM,aAAa,SAqDtB,CAAC"}
1
+ {"version":3,"file":"impact.d.ts","sourceRoot":"","sources":["../../src/commands/impact.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8BpC,eAAO,MAAM,aAAa,SA6DtB,CAAC"}
@@ -9,7 +9,7 @@ import { Command } from 'commander';
9
9
  import { resolve, join, dirname } from 'path';
10
10
  import { relative } from 'path';
11
11
  import { existsSync } from 'fs';
12
- import { RFDBServerBackend } from '@grafema/core';
12
+ import { RFDBServerBackend, findContainingFunction as findContainingFunctionCore } from '@grafema/core';
13
13
  import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
14
14
  import { exitWithError } from '../utils/errorFormatter.js';
15
15
  export const impactCommand = new Command('impact')
@@ -18,6 +18,14 @@ export const impactCommand = new Command('impact')
18
18
  .option('-p, --project <path>', 'Project path', '.')
19
19
  .option('-j, --json', 'Output as JSON')
20
20
  .option('-d, --depth <n>', 'Max traversal depth', '10')
21
+ .addHelpText('after', `
22
+ Examples:
23
+ grafema impact "authenticate" Analyze impact of changing authenticate
24
+ grafema impact "function login" Impact of specific function
25
+ grafema impact "class UserService" Impact of class changes
26
+ grafema impact "validate" -d 3 Limit analysis depth to 3 levels
27
+ grafema impact "auth" --json Output impact analysis as JSON
28
+ `)
21
29
  .action(async (pattern, options) => {
22
30
  const projectPath = resolve(options.project);
23
31
  const grafemaDir = join(projectPath, '.grafema');
@@ -107,10 +115,21 @@ async function analyzeImpact(backend, target, maxDepth, projectPath) {
107
115
  const affectedModules = new Map();
108
116
  const callChains = [];
109
117
  const visited = new Set();
118
+ // If target is a CLASS, aggregate callers from all methods
119
+ let targetIds;
120
+ if (target.type === 'CLASS') {
121
+ const methodIds = await getClassMethods(backend, target.id);
122
+ targetIds = [target.id, ...methodIds];
123
+ }
124
+ else {
125
+ targetIds = [target.id];
126
+ }
110
127
  // BFS to find all callers
111
- const queue = [
112
- { id: target.id, depth: 0, chain: [target.name] }
113
- ];
128
+ const queue = targetIds.map(id => ({
129
+ id,
130
+ depth: 0,
131
+ chain: [target.name]
132
+ }));
114
133
  while (queue.length > 0) {
115
134
  const { id, depth, chain } = queue.shift();
116
135
  if (visited.has(id))
@@ -124,13 +143,17 @@ async function analyzeImpact(backend, target, maxDepth, projectPath) {
124
143
  const containingCalls = await findCallsToNode(backend, id);
125
144
  for (const callNode of containingCalls) {
126
145
  // Find the function containing this call
127
- const container = await findContainingFunction(backend, callNode.id);
146
+ const container = await findContainingFunctionCore(backend, callNode.id);
128
147
  if (container && !visited.has(container.id)) {
148
+ // Filter out internal callers (methods of the same class)
149
+ if (target.type === 'CLASS' && targetIds.includes(container.id)) {
150
+ continue;
151
+ }
129
152
  const caller = {
130
153
  id: container.id,
131
154
  type: container.type,
132
155
  name: container.name,
133
- file: container.file,
156
+ file: container.file || '',
134
157
  line: container.line,
135
158
  };
136
159
  if (depth === 0) {
@@ -166,6 +189,25 @@ async function analyzeImpact(backend, target, maxDepth, projectPath) {
166
189
  callChains,
167
190
  };
168
191
  }
192
+ /**
193
+ * Get method IDs for a class
194
+ */
195
+ async function getClassMethods(backend, classId) {
196
+ const methods = [];
197
+ try {
198
+ const edges = await backend.getOutgoingEdges(classId, ['CONTAINS']);
199
+ for (const edge of edges) {
200
+ const node = await backend.getNode(edge.dst);
201
+ if (node && node.type === 'FUNCTION') {
202
+ methods.push(node.id);
203
+ }
204
+ }
205
+ }
206
+ catch {
207
+ // Ignore errors
208
+ }
209
+ return methods;
210
+ }
169
211
  /**
170
212
  * Find CALL nodes that reference a target
171
213
  */
@@ -192,50 +234,6 @@ async function findCallsToNode(backend, targetId) {
192
234
  }
193
235
  return calls;
194
236
  }
195
- /**
196
- * Find the function that contains a call node
197
- *
198
- * Path: CALL → CONTAINS → SCOPE → CONTAINS → SCOPE → HAS_SCOPE → FUNCTION
199
- */
200
- async function findContainingFunction(backend, nodeId, maxDepth = 15) {
201
- const visited = new Set();
202
- const queue = [{ id: nodeId, depth: 0 }];
203
- while (queue.length > 0) {
204
- const { id, depth } = queue.shift();
205
- if (visited.has(id) || depth > maxDepth)
206
- continue;
207
- visited.add(id);
208
- try {
209
- // Get incoming edges: CONTAINS, HAS_SCOPE
210
- const edges = await backend.getIncomingEdges(id, null);
211
- for (const edge of edges) {
212
- const edgeType = edge.edgeType || edge.type;
213
- // Only follow structural edges
214
- if (!['CONTAINS', 'HAS_SCOPE', 'DECLARES'].includes(edgeType))
215
- continue;
216
- const parent = await backend.getNode(edge.src);
217
- if (!parent || visited.has(parent.id))
218
- continue;
219
- const parentType = parent.type;
220
- // FUNCTION, CLASS, or MODULE (for top-level calls)
221
- if (parentType === 'FUNCTION' || parentType === 'CLASS' || parentType === 'MODULE') {
222
- return {
223
- id: parent.id,
224
- type: parentType,
225
- name: parent.name || '',
226
- file: parent.file || '',
227
- line: parent.line,
228
- };
229
- }
230
- queue.push({ id: parent.id, depth: depth + 1 });
231
- }
232
- }
233
- catch {
234
- // Ignore
235
- }
236
- }
237
- return null;
238
- }
239
237
  /**
240
238
  * Get module path relative to project
241
239
  */
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4CpC,eAAO,MAAM,WAAW,SA+DpB,CAAC"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyGpC,eAAO,MAAM,WAAW,SAyFpB,CAAC"}
@@ -4,9 +4,12 @@
4
4
  import { Command } from 'commander';
5
5
  import { resolve, join } from 'path';
6
6
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
7
- import { exitWithError } from '../utils/errorFormatter.js';
7
+ import { spawn } from 'child_process';
8
+ import { createInterface } from 'readline';
9
+ import { fileURLToPath } from 'url';
8
10
  import { stringify as stringifyYAML } from 'yaml';
9
11
  import { DEFAULT_CONFIG } from '@grafema/core';
12
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
10
13
  /**
11
14
  * Generate config.yaml content with commented future features.
12
15
  * Only includes implemented features (plugins).
@@ -26,21 +29,79 @@ function generateConfigYAML() {
26
29
  # Documentation: https://github.com/grafema/grafema#configuration
27
30
 
28
31
  ${yaml}
29
- # Future: File discovery patterns (not yet implemented)
30
- # Grafema currently uses entrypoint-based discovery (follows imports from package.json main field)
31
- # Glob-based include/exclude patterns will be added in a future release
32
+ # File filtering patterns (optional)
33
+ # By default, Grafema follows imports from package.json entry points.
34
+ # Use these patterns to control which files are analyzed:
32
35
  #
33
- # include:
36
+ # include: # Only analyze files matching these patterns
34
37
  # - "src/**/*.{ts,js,tsx,jsx}"
35
- # exclude:
38
+ #
39
+ # exclude: # Skip files matching these patterns (takes precedence over include)
36
40
  # - "**/*.test.ts"
37
- # - "node_modules/**"
41
+ # - "**/__tests__/**"
42
+ # - "**/node_modules/**"
38
43
  `;
39
44
  }
45
+ /**
46
+ * Ask user a yes/no question. Returns true for yes (default), false for no.
47
+ */
48
+ function askYesNo(question) {
49
+ const rl = createInterface({
50
+ input: process.stdin,
51
+ output: process.stdout,
52
+ });
53
+ return new Promise((resolve) => {
54
+ rl.question(question, (answer) => {
55
+ rl.close();
56
+ // Default yes (empty answer or 'y' or 'yes')
57
+ const normalized = answer.toLowerCase().trim();
58
+ resolve(normalized !== 'n' && normalized !== 'no');
59
+ });
60
+ });
61
+ }
62
+ /**
63
+ * Run grafema analyze in the given project path.
64
+ * Returns the exit code of the analyze process.
65
+ */
66
+ function runAnalyze(projectPath) {
67
+ return new Promise((resolve) => {
68
+ const cliPath = join(__dirname, '..', 'cli.js');
69
+ const child = spawn('node', [cliPath, 'analyze', projectPath], {
70
+ stdio: 'inherit', // Pass through all I/O for user to see progress
71
+ });
72
+ child.on('close', (code) => resolve(code ?? 1));
73
+ child.on('error', () => resolve(1));
74
+ });
75
+ }
76
+ /**
77
+ * Print next steps after init.
78
+ */
79
+ function printNextSteps() {
80
+ console.log('');
81
+ console.log('Next steps:');
82
+ console.log(' 1. Review config: code .grafema/config.yaml');
83
+ console.log(' 2. Build graph: grafema analyze');
84
+ console.log(' 3. Explore: grafema overview');
85
+ }
86
+ /**
87
+ * Check if running in interactive mode.
88
+ * Interactive if stdin is TTY and --yes flag not provided.
89
+ */
90
+ function isInteractive(options) {
91
+ return options.yes !== true && process.stdin.isTTY === true;
92
+ }
40
93
  export const initCommand = new Command('init')
41
94
  .description('Initialize Grafema in current project')
42
95
  .argument('[path]', 'Project path', '.')
43
96
  .option('-f, --force', 'Overwrite existing config')
97
+ .option('-y, --yes', 'Skip prompts (non-interactive mode)')
98
+ .addHelpText('after', `
99
+ Examples:
100
+ grafema init Initialize in current directory
101
+ grafema init ./my-project Initialize in specific directory
102
+ grafema init --force Overwrite existing configuration
103
+ grafema init --yes Skip prompts, auto-run analyze
104
+ `)
44
105
  .action(async (path, options) => {
45
106
  const projectPath = resolve(path);
46
107
  const grafemaDir = join(projectPath, '.grafema');
@@ -49,10 +110,15 @@ export const initCommand = new Command('init')
49
110
  const tsconfigPath = join(projectPath, 'tsconfig.json');
50
111
  // Check package.json
51
112
  if (!existsSync(packageJsonPath)) {
52
- exitWithError('No package.json found', [
53
- 'Initialize a project: npm init',
54
- 'Or check you are in the right directory'
55
- ]);
113
+ console.error(' Grafema currently supports JavaScript/TypeScript projects only.');
114
+ console.error(` No package.json found in ${projectPath}`);
115
+ console.error('');
116
+ console.error(' Supported: Node.js, React, Express, Next.js, Vue, Angular, etc.');
117
+ console.error(' Coming soon: Python, Go, Rust');
118
+ console.error('');
119
+ console.error(' If this IS a JS/TS project, create package.json first:');
120
+ console.error(' npm init -y');
121
+ process.exit(1);
56
122
  }
57
123
  console.log('✓ Found package.json');
58
124
  // Detect TypeScript
@@ -68,8 +134,7 @@ export const initCommand = new Command('init')
68
134
  console.log('');
69
135
  console.log('✓ Grafema already initialized');
70
136
  console.log(' → Use --force to overwrite config');
71
- console.log('');
72
- console.log('Next: Run "grafema analyze" to build the code graph');
137
+ printNextSteps();
73
138
  return;
74
139
  }
75
140
  // Create .grafema directory
@@ -89,6 +154,19 @@ export const initCommand = new Command('init')
89
154
  console.log('✓ Updated .gitignore');
90
155
  }
91
156
  }
92
- console.log('');
93
- console.log('Next: Run "grafema analyze" to build the code graph');
157
+ printNextSteps();
158
+ // Prompt to run analyze in interactive mode
159
+ if (isInteractive(options)) {
160
+ console.log('');
161
+ const runNow = await askYesNo('Run analysis now? [Y/n] ');
162
+ if (runNow) {
163
+ console.log('');
164
+ console.log('Starting analysis...');
165
+ console.log('');
166
+ const exitCode = await runAnalyze(projectPath);
167
+ if (exitCode !== 0) {
168
+ process.exit(exitCode);
169
+ }
170
+ }
171
+ }
94
172
  });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * List command - List nodes by type
3
+ *
4
+ * Unix-style listing of nodes in the graph. Similar to `ls` for files,
5
+ * but for code graph nodes.
6
+ *
7
+ * Use cases:
8
+ * - "Show me all HTTP routes in this project"
9
+ * - "List all functions" (with limit for large codebases)
10
+ * - "What Socket.IO events are defined?"
11
+ */
12
+ import { Command } from 'commander';
13
+ export declare const lsCommand: Command;
14
+ //# sourceMappingURL=ls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ls.d.ts","sourceRoot":"","sources":["../../src/commands/ls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0BpC,eAAO,MAAM,SAAS,SA2FlB,CAAC"}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * List command - List nodes by type
3
+ *
4
+ * Unix-style listing of nodes in the graph. Similar to `ls` for files,
5
+ * but for code graph nodes.
6
+ *
7
+ * Use cases:
8
+ * - "Show me all HTTP routes in this project"
9
+ * - "List all functions" (with limit for large codebases)
10
+ * - "What Socket.IO events are defined?"
11
+ */
12
+ import { Command } from 'commander';
13
+ import { resolve, join, relative } from 'path';
14
+ import { existsSync } from 'fs';
15
+ import { RFDBServerBackend } from '@grafema/core';
16
+ import { exitWithError } from '../utils/errorFormatter.js';
17
+ export const lsCommand = new Command('ls')
18
+ .description('List nodes by type')
19
+ .requiredOption('-t, --type <nodeType>', 'Node type to list (required)')
20
+ .option('-p, --project <path>', 'Project path', '.')
21
+ .option('-j, --json', 'Output as JSON')
22
+ .option('-l, --limit <n>', 'Limit results (default: 50)', '50')
23
+ .addHelpText('after', `
24
+ Examples:
25
+ grafema ls --type FUNCTION List functions (up to 50)
26
+ grafema ls --type http:route List all HTTP routes
27
+ grafema ls --type http:request List all HTTP requests (fetch/axios)
28
+ grafema ls -t socketio:event List Socket.IO events
29
+ grafema ls --type CLASS -l 100 List up to 100 classes
30
+ grafema ls --type jsx:component --json Output as JSON
31
+
32
+ Discover available types:
33
+ grafema types List all types with counts
34
+ `)
35
+ .action(async (options) => {
36
+ const projectPath = resolve(options.project);
37
+ const grafemaDir = join(projectPath, '.grafema');
38
+ const dbPath = join(grafemaDir, 'graph.rfdb');
39
+ if (!existsSync(dbPath)) {
40
+ exitWithError('No graph database found', ['Run: grafema analyze']);
41
+ }
42
+ const backend = new RFDBServerBackend({ dbPath });
43
+ await backend.connect();
44
+ try {
45
+ const limit = parseInt(options.limit, 10);
46
+ const nodeType = options.type;
47
+ // Check if type exists in graph
48
+ const typeCounts = await backend.countNodesByType();
49
+ if (!typeCounts[nodeType]) {
50
+ const availableTypes = Object.keys(typeCounts).sort();
51
+ exitWithError(`No nodes of type "${nodeType}" found`, [
52
+ 'Available types:',
53
+ ...availableTypes.slice(0, 10).map(t => ` ${t}`),
54
+ availableTypes.length > 10 ? ` ... and ${availableTypes.length - 10} more` : '',
55
+ '',
56
+ 'Run: grafema types to see all types with counts',
57
+ ].filter(Boolean));
58
+ }
59
+ // Collect nodes
60
+ const nodes = [];
61
+ for await (const node of backend.queryNodes({ nodeType: nodeType })) {
62
+ nodes.push({
63
+ id: node.id,
64
+ type: node.type || nodeType,
65
+ name: node.name || '',
66
+ file: node.file || '',
67
+ line: node.line,
68
+ method: node.method,
69
+ path: node.path,
70
+ url: node.url,
71
+ event: node.event,
72
+ });
73
+ if (nodes.length >= limit)
74
+ break;
75
+ }
76
+ const totalCount = typeCounts[nodeType];
77
+ const showing = nodes.length;
78
+ if (options.json) {
79
+ console.log(JSON.stringify({
80
+ type: nodeType,
81
+ nodes,
82
+ showing,
83
+ total: totalCount,
84
+ }, null, 2));
85
+ }
86
+ else {
87
+ console.log(`[${nodeType}] (${showing}${showing < totalCount ? ` of ${totalCount}` : ''}):`);
88
+ console.log('');
89
+ for (const node of nodes) {
90
+ const display = formatNodeForList(node, nodeType, projectPath);
91
+ console.log(` ${display}`);
92
+ }
93
+ if (showing < totalCount) {
94
+ console.log('');
95
+ console.log(` ... ${totalCount - showing} more. Use --limit ${totalCount} to see all.`);
96
+ }
97
+ }
98
+ }
99
+ finally {
100
+ await backend.close();
101
+ }
102
+ });
103
+ /**
104
+ * Format a node for list display based on its type.
105
+ * Different types show different fields.
106
+ */
107
+ function formatNodeForList(node, nodeType, projectPath) {
108
+ const relFile = node.file ? relative(projectPath, node.file) : '';
109
+ const loc = node.line ? `${relFile}:${node.line}` : relFile;
110
+ // HTTP routes: METHOD PATH (location)
111
+ if (nodeType === 'http:route' && node.method && node.path) {
112
+ return `${node.method.padEnd(6)} ${node.path} (${loc})`;
113
+ }
114
+ // HTTP requests: METHOD URL (location)
115
+ if (nodeType === 'http:request') {
116
+ const method = (node.method || 'GET').padEnd(6);
117
+ const url = node.url || 'dynamic';
118
+ return `${method} ${url} (${loc})`;
119
+ }
120
+ // Socket.IO events: event_name
121
+ if (nodeType === 'socketio:event') {
122
+ return node.name || node.id;
123
+ }
124
+ // Socket.IO emit/on: event (location)
125
+ if (nodeType === 'socketio:emit' || nodeType === 'socketio:on') {
126
+ const event = node.event || node.name || 'unknown';
127
+ return `${event} (${loc})`;
128
+ }
129
+ // Default: name (location)
130
+ const name = node.name || node.id;
131
+ return loc ? `${name} (${loc})` : name;
132
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"overview.d.ts","sourceRoot":"","sources":["../../src/commands/overview.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,eAAO,MAAM,eAAe,SA4FxB,CAAC"}
1
+ {"version":3,"file":"overview.d.ts","sourceRoot":"","sources":["../../src/commands/overview.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,eAAO,MAAM,eAAe,SAyGxB,CAAC"}
@@ -10,6 +10,12 @@ export const overviewCommand = new Command('overview')
10
10
  .description('Show project overview and statistics')
11
11
  .option('-p, --project <path>', 'Project path', '.')
12
12
  .option('-j, --json', 'Output as JSON')
13
+ .addHelpText('after', `
14
+ Examples:
15
+ grafema overview Show project dashboard
16
+ grafema overview --json Output statistics as JSON
17
+ grafema overview -p ./app Overview for specific project
18
+ `)
13
19
  .action(async (options) => {
14
20
  const projectPath = resolve(options.project);
15
21
  const grafemaDir = join(projectPath, '.grafema');
@@ -46,6 +52,7 @@ export const overviewCommand = new Command('overview')
46
52
  console.log('External Interactions:');
47
53
  const httpRoutes = stats.nodesByType['http:route'] || 0;
48
54
  const dbQueries = stats.nodesByType['db:query'] || 0;
55
+ const socketEvents = stats.nodesByType['socketio:event'] || 0;
49
56
  const socketEmit = stats.nodesByType['socketio:emit'] || 0;
50
57
  const socketOn = stats.nodesByType['socketio:on'] || 0;
51
58
  const events = stats.nodesByType['event:listener'] || 0;
@@ -53,15 +60,21 @@ export const overviewCommand = new Command('overview')
53
60
  console.log(`├─ HTTP routes: ${httpRoutes}`);
54
61
  if (dbQueries > 0)
55
62
  console.log(`├─ Database queries: ${dbQueries}`);
56
- if (socketEmit + socketOn > 0)
63
+ if (socketEvents > 0) {
64
+ // New format showing event count prominently
65
+ console.log(`├─ Socket.IO: ${socketEvents} events (${socketEmit} emit, ${socketOn} listeners)`);
66
+ }
67
+ else if (socketEmit + socketOn > 0) {
68
+ // Fallback for graphs analyzed before REG-209
57
69
  console.log(`├─ Socket.IO: ${socketEmit} emit, ${socketOn} listeners`);
70
+ }
58
71
  if (events > 0)
59
72
  console.log(`├─ Event listeners: ${events}`);
60
73
  // Check for external module refs
61
74
  const externalModules = stats.nodesByType['EXTERNAL_MODULE'] || 0;
62
75
  if (externalModules > 0)
63
76
  console.log(`└─ External modules: ${externalModules}`);
64
- if (httpRoutes + dbQueries + socketEmit + socketOn + events + externalModules === 0) {
77
+ if (httpRoutes + dbQueries + socketEvents + socketEmit + socketOn + events + externalModules === 0) {
65
78
  console.log('└─ (none detected)');
66
79
  }
67
80
  console.log('');
@@ -9,5 +9,103 @@
9
9
  * For raw Datalog queries, use --raw flag
10
10
  */
11
11
  import { Command } from 'commander';
12
+ /**
13
+ * Parsed query with optional scope constraints.
14
+ *
15
+ * Supports patterns like:
16
+ * "response" -> { name: "response" }
17
+ * "variable response" -> { type: "VARIABLE", name: "response" }
18
+ * "response in fetchData" -> { name: "response", scopes: ["fetchData"] }
19
+ * "response in src/app.ts" -> { name: "response", file: "src/app.ts" }
20
+ * "response in catch in fetchData" -> { name: "response", scopes: ["fetchData", "catch"] }
21
+ */
22
+ export interface ParsedQuery {
23
+ /** Node type (e.g., "FUNCTION", "VARIABLE") or null for any */
24
+ type: string | null;
25
+ /** Node name to search (partial match) */
26
+ name: string;
27
+ /** File scope - filter to nodes in this file */
28
+ file: string | null;
29
+ /** Scope chain - filter to nodes inside these scopes (function/class/block names) */
30
+ scopes: string[];
31
+ }
12
32
  export declare const queryCommand: Command;
33
+ /**
34
+ * Parse search pattern with scope support.
35
+ *
36
+ * Grammar:
37
+ * query := [type] name [" in " scope]*
38
+ * type := "function" | "class" | "variable" | etc.
39
+ * scope := <filename> | <functionName>
40
+ *
41
+ * File scope detection: contains "/" or ends with .ts/.js/.tsx/.jsx
42
+ * Function scope detection: anything else
43
+ *
44
+ * IMPORTANT: Only split on " in " (space-padded) to avoid matching names like "signin"
45
+ *
46
+ * Examples:
47
+ * "response" -> { type: null, name: "response", file: null, scopes: [] }
48
+ * "variable response in fetchData" -> { type: "VARIABLE", name: "response", file: null, scopes: ["fetchData"] }
49
+ * "response in src/app.ts" -> { type: null, name: "response", file: "src/app.ts", scopes: [] }
50
+ * "error in catch in fetchData in src/app.ts" -> { type: null, name: "error", file: "src/app.ts", scopes: ["fetchData", "catch"] }
51
+ */
52
+ export declare function parseQuery(pattern: string): ParsedQuery;
53
+ /**
54
+ * Detect if a scope string looks like a file path.
55
+ *
56
+ * Heuristics:
57
+ * - Contains "/" -> file path
58
+ * - Ends with .ts, .js, .tsx, .jsx, .mjs, .cjs -> file path
59
+ *
60
+ * Examples:
61
+ * "src/app.ts" -> true
62
+ * "app.js" -> true
63
+ * "fetchData" -> false
64
+ * "UserService" -> false
65
+ * "catch" -> false
66
+ */
67
+ export declare function isFileScope(scope: string): boolean;
68
+ /**
69
+ * Check if a semantic ID matches the given scope constraints.
70
+ *
71
+ * Uses parseSemanticId from @grafema/core for robust ID parsing.
72
+ *
73
+ * Scope matching rules:
74
+ * - File scope: semantic ID must match the file path (full or basename)
75
+ * - Function/class scope: semantic ID must contain the scope in its scopePath
76
+ * - Multiple scopes: ALL must match (AND logic)
77
+ * - Scope order: independent - all scopes just need to be present
78
+ *
79
+ * Examples:
80
+ * ID: "src/app.ts->fetchData->try#0->VARIABLE->response"
81
+ * Matches: scopes=["fetchData"] -> true
82
+ * Matches: scopes=["try"] -> true (matches "try#0")
83
+ * Matches: scopes=["fetchData", "try"] -> true (both present)
84
+ * Matches: scopes=["processData"] -> false (not in ID)
85
+ *
86
+ * @param semanticId - The full semantic ID to check
87
+ * @param file - File scope (null for any file)
88
+ * @param scopes - Array of scope names to match
89
+ * @returns true if ID matches all constraints
90
+ */
91
+ export declare function matchesScope(semanticId: string, file: string | null, scopes: string[]): boolean;
92
+ /**
93
+ * Extract human-readable scope context from a semantic ID.
94
+ *
95
+ * Parses the ID and returns a description of the scope chain.
96
+ *
97
+ * Examples:
98
+ * "src/app.ts->fetchData->try#0->VARIABLE->response"
99
+ * -> "inside fetchData, inside try block"
100
+ *
101
+ * "src/app.ts->UserService->login->VARIABLE->token"
102
+ * -> "inside UserService, inside login"
103
+ *
104
+ * "src/app.ts->global->FUNCTION->main"
105
+ * -> null (no interesting scope)
106
+ *
107
+ * @param semanticId - The semantic ID to parse
108
+ * @returns Human-readable scope context or null
109
+ */
110
+ export declare function extractScopeContext(semanticId: string): string | null;
13
111
  //# sourceMappingURL=query.d.ts.map