@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
package/src/commands/get.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { existsSync } from 'fs';
|
|
|
12
12
|
import { RFDBServerBackend } from '@grafema/core';
|
|
13
13
|
import { formatNodeDisplay } from '../utils/formatNode.js';
|
|
14
14
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
15
|
+
import { Spinner } from '../utils/spinner.js';
|
|
15
16
|
|
|
16
17
|
interface GetOptions {
|
|
17
18
|
project: string;
|
|
@@ -66,11 +67,15 @@ Examples:
|
|
|
66
67
|
const backend = new RFDBServerBackend({ dbPath });
|
|
67
68
|
await backend.connect();
|
|
68
69
|
|
|
70
|
+
const spinner = new Spinner('Querying graph...');
|
|
71
|
+
spinner.start();
|
|
72
|
+
|
|
69
73
|
try {
|
|
70
74
|
// Retrieve node by semantic ID
|
|
71
75
|
const node = await backend.getNode(semanticId);
|
|
72
76
|
|
|
73
77
|
if (!node) {
|
|
78
|
+
spinner.stop();
|
|
74
79
|
exitWithError('Node not found', [
|
|
75
80
|
`ID: ${semanticId}`,
|
|
76
81
|
'Try: grafema query "<name>" to search for nodes',
|
|
@@ -81,6 +86,8 @@ Examples:
|
|
|
81
86
|
const incomingEdges = await backend.getIncomingEdges(semanticId, null);
|
|
82
87
|
const outgoingEdges = await backend.getOutgoingEdges(semanticId, null);
|
|
83
88
|
|
|
89
|
+
spinner.stop();
|
|
90
|
+
|
|
84
91
|
if (options.json) {
|
|
85
92
|
await outputJSON(backend, node, incomingEdges, outgoingEdges);
|
|
86
93
|
} else {
|
|
@@ -88,6 +95,7 @@ Examples:
|
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
} finally {
|
|
98
|
+
spinner.stop();
|
|
91
99
|
await backend.close();
|
|
92
100
|
}
|
|
93
101
|
});
|
package/src/commands/impact.ts
CHANGED
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { Command } from 'commander';
|
|
10
|
-
import { resolve, join, dirname } from 'path';
|
|
11
|
-
import { relative } from 'path';
|
|
10
|
+
import { isAbsolute, resolve, join, dirname, relative } from 'path';
|
|
12
11
|
import { existsSync } from 'fs';
|
|
13
|
-
import { RFDBServerBackend, findContainingFunction as findContainingFunctionCore
|
|
12
|
+
import { RFDBServerBackend, findContainingFunction as findContainingFunctionCore } from '@grafema/core';
|
|
14
13
|
import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
|
|
15
14
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
16
15
|
|
|
@@ -313,7 +312,7 @@ async function findCallsToNode(
|
|
|
313
312
|
*/
|
|
314
313
|
function getModulePath(file: string, projectPath: string): string {
|
|
315
314
|
if (!file) return '<unknown>';
|
|
316
|
-
const relPath = relative(projectPath, file);
|
|
315
|
+
const relPath = isAbsolute(file) ? relative(projectPath, file) : file;
|
|
317
316
|
const dir = dirname(relPath);
|
|
318
317
|
return dir === '.' ? relPath : `${dir}/*`;
|
|
319
318
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -8,9 +8,9 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
|
8
8
|
import { spawn } from 'child_process';
|
|
9
9
|
import { createInterface } from 'readline';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
|
-
import { exitWithError } from '../utils/errorFormatter.js';
|
|
12
11
|
import { stringify as stringifyYAML } from 'yaml';
|
|
13
|
-
import { DEFAULT_CONFIG } from '@grafema/core';
|
|
12
|
+
import { DEFAULT_CONFIG, GRAFEMA_VERSION, getSchemaVersion } from '@grafema/core';
|
|
13
|
+
import { installSkill } from './setup-skill.js';
|
|
14
14
|
|
|
15
15
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
16
16
|
|
|
@@ -21,6 +21,7 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
|
21
21
|
function generateConfigYAML(): string {
|
|
22
22
|
// Start with working default config
|
|
23
23
|
const config = {
|
|
24
|
+
version: getSchemaVersion(GRAFEMA_VERSION),
|
|
24
25
|
// Plugin list (fully implemented)
|
|
25
26
|
plugins: DEFAULT_CONFIG.plugins,
|
|
26
27
|
};
|
|
@@ -75,7 +76,9 @@ function askYesNo(question: string): Promise<boolean> {
|
|
|
75
76
|
function runAnalyze(projectPath: string): Promise<number> {
|
|
76
77
|
return new Promise((resolve) => {
|
|
77
78
|
const cliPath = join(__dirname, '..', 'cli.js');
|
|
78
|
-
|
|
79
|
+
// Use process.execPath (absolute path to current Node binary) instead of
|
|
80
|
+
// 'node' to avoid PATH lookup failures when nvm isn't loaded in the shell.
|
|
81
|
+
const child = spawn(process.execPath, [cliPath, 'analyze', projectPath], {
|
|
79
82
|
stdio: 'inherit', // Pass through all I/O for user to see progress
|
|
80
83
|
});
|
|
81
84
|
child.on('close', (code) => resolve(code ?? 1));
|
|
@@ -92,6 +95,9 @@ function printNextSteps(): void {
|
|
|
92
95
|
console.log(' 1. Review config: code .grafema/config.yaml');
|
|
93
96
|
console.log(' 2. Build graph: grafema analyze');
|
|
94
97
|
console.log(' 3. Explore: grafema overview');
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log('For AI-assisted setup, use the Grafema MCP server');
|
|
100
|
+
console.log('with the "onboard_project" prompt.');
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
/**
|
|
@@ -180,6 +186,16 @@ Examples:
|
|
|
180
186
|
}
|
|
181
187
|
}
|
|
182
188
|
|
|
189
|
+
// Auto-install Agent Skill for AI-assisted development
|
|
190
|
+
try {
|
|
191
|
+
const installed = installSkill(projectPath);
|
|
192
|
+
if (installed) {
|
|
193
|
+
console.log('✓ Installed Agent Skill (.claude/skills/grafema-codebase-analysis/)');
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
// Non-critical — don't fail init if skill install fails
|
|
197
|
+
}
|
|
198
|
+
|
|
183
199
|
printNextSteps();
|
|
184
200
|
|
|
185
201
|
// Prompt to run analyze in interactive mode
|
package/src/commands/ls.ts
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { Command } from 'commander';
|
|
14
|
-
import { resolve, join
|
|
14
|
+
import { resolve, join } from 'path';
|
|
15
|
+
import { toRelativeDisplay } from '../utils/pathUtils.js';
|
|
15
16
|
import { existsSync } from 'fs';
|
|
16
17
|
import { RFDBServerBackend } from '@grafema/core';
|
|
17
18
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
19
|
+
import { Spinner } from '../utils/spinner.js';
|
|
18
20
|
|
|
19
21
|
interface LsOptions {
|
|
20
22
|
project: string;
|
|
@@ -66,6 +68,9 @@ Discover available types:
|
|
|
66
68
|
const backend = new RFDBServerBackend({ dbPath });
|
|
67
69
|
await backend.connect();
|
|
68
70
|
|
|
71
|
+
const spinner = new Spinner('Querying graph...');
|
|
72
|
+
spinner.start();
|
|
73
|
+
|
|
69
74
|
try {
|
|
70
75
|
const limit = parseInt(options.limit, 10);
|
|
71
76
|
const nodeType = options.type;
|
|
@@ -73,6 +78,7 @@ Discover available types:
|
|
|
73
78
|
// Check if type exists in graph
|
|
74
79
|
const typeCounts = await backend.countNodesByType();
|
|
75
80
|
if (!typeCounts[nodeType]) {
|
|
81
|
+
spinner.stop();
|
|
76
82
|
const availableTypes = Object.keys(typeCounts).sort();
|
|
77
83
|
exitWithError(`No nodes of type "${nodeType}" found`, [
|
|
78
84
|
'Available types:',
|
|
@@ -103,6 +109,8 @@ Discover available types:
|
|
|
103
109
|
const totalCount = typeCounts[nodeType];
|
|
104
110
|
const showing = nodes.length;
|
|
105
111
|
|
|
112
|
+
spinner.stop();
|
|
113
|
+
|
|
106
114
|
if (options.json) {
|
|
107
115
|
console.log(JSON.stringify({
|
|
108
116
|
type: nodeType,
|
|
@@ -125,6 +133,7 @@ Discover available types:
|
|
|
125
133
|
}
|
|
126
134
|
}
|
|
127
135
|
} finally {
|
|
136
|
+
spinner.stop();
|
|
128
137
|
await backend.close();
|
|
129
138
|
}
|
|
130
139
|
});
|
|
@@ -134,7 +143,7 @@ Discover available types:
|
|
|
134
143
|
* Different types show different fields.
|
|
135
144
|
*/
|
|
136
145
|
function formatNodeForList(node: NodeInfo, nodeType: string, projectPath: string): string {
|
|
137
|
-
const relFile = node.file ?
|
|
146
|
+
const relFile = node.file ? toRelativeDisplay(node.file, projectPath) : '';
|
|
138
147
|
const loc = node.line ? `${relFile}:${node.line}` : relFile;
|
|
139
148
|
|
|
140
149
|
// HTTP routes: METHOD PATH (location)
|
package/src/commands/overview.ts
CHANGED
|
@@ -8,10 +8,6 @@ import { existsSync } from 'fs';
|
|
|
8
8
|
import { RFDBServerBackend } from '@grafema/core';
|
|
9
9
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
10
10
|
|
|
11
|
-
interface NodeStats {
|
|
12
|
-
type: string;
|
|
13
|
-
count: number;
|
|
14
|
-
}
|
|
15
11
|
|
|
16
12
|
export const overviewCommand = new Command('overview')
|
|
17
13
|
.description('Show project overview and statistics')
|
package/src/commands/query.ts
CHANGED
|
@@ -10,11 +10,22 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { Command } from 'commander';
|
|
13
|
-
import { resolve, join,
|
|
13
|
+
import { resolve, join, basename } from 'path';
|
|
14
|
+
import { toRelativeDisplay } from '../utils/pathUtils.js';
|
|
14
15
|
import { existsSync } from 'fs';
|
|
15
|
-
import { RFDBServerBackend, parseSemanticId, findCallsInFunction as findCallsInFunctionCore, findContainingFunction as findContainingFunctionCore } from '@grafema/core';
|
|
16
|
+
import { RFDBServerBackend, parseSemanticId, parseSemanticIdV2, findCallsInFunction as findCallsInFunctionCore, findContainingFunction as findContainingFunctionCore } from '@grafema/core';
|
|
16
17
|
import { formatNodeDisplay, formatNodeInline, formatLocation } from '../utils/formatNode.js';
|
|
17
18
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
19
|
+
import { Spinner } from '../utils/spinner.js';
|
|
20
|
+
|
|
21
|
+
// Node type constants to avoid magic string duplication
|
|
22
|
+
const HTTP_ROUTE_TYPE = 'http:route';
|
|
23
|
+
const HTTP_REQUEST_TYPE = 'http:request';
|
|
24
|
+
const SOCKETIO_EVENT_TYPE = 'socketio:event';
|
|
25
|
+
const SOCKETIO_EMIT_TYPE = 'socketio:emit';
|
|
26
|
+
const SOCKETIO_ON_TYPE = 'socketio:on';
|
|
27
|
+
const GRAFEMA_PLUGIN_TYPE = 'grafema:plugin';
|
|
28
|
+
const PROPERTY_ACCESS_TYPE = 'PROPERTY_ACCESS';
|
|
18
29
|
|
|
19
30
|
interface QueryOptions {
|
|
20
31
|
project: string;
|
|
@@ -75,6 +86,9 @@ export const queryCommand = new Command('query')
|
|
|
75
86
|
'--raw',
|
|
76
87
|
`Execute raw Datalog query
|
|
77
88
|
|
|
89
|
+
Supports both direct queries and Datalog rules.
|
|
90
|
+
Rules (containing ":-") define a violation predicate and return matching nodes.
|
|
91
|
+
|
|
78
92
|
Predicates:
|
|
79
93
|
type(Id, Type) Find nodes by type or get type of node
|
|
80
94
|
node(Id, Type) Alias for type
|
|
@@ -83,10 +97,14 @@ Predicates:
|
|
|
83
97
|
path(Src, Dst) Check reachability between nodes
|
|
84
98
|
incoming(Dst, Src, T) Find incoming edges
|
|
85
99
|
|
|
86
|
-
|
|
100
|
+
Direct queries:
|
|
87
101
|
grafema query --raw 'type(X, "FUNCTION")'
|
|
88
102
|
grafema query --raw 'type(X, "FUNCTION"), attr(X, "name", "main")'
|
|
89
|
-
grafema query --raw 'edge(X, Y, "CALLS")'
|
|
103
|
+
grafema query --raw 'edge(X, Y, "CALLS")'
|
|
104
|
+
|
|
105
|
+
Rules (must define violation/1):
|
|
106
|
+
grafema query --raw 'violation(X) :- node(X, "FUNCTION").'
|
|
107
|
+
grafema query --raw 'violation(X) :- node(X, "http:route"), attr(X, "method", "POST").'`
|
|
90
108
|
)
|
|
91
109
|
.option(
|
|
92
110
|
'-t, --type <nodeType>',
|
|
@@ -130,15 +148,23 @@ Examples:
|
|
|
130
148
|
const backend = new RFDBServerBackend({ dbPath });
|
|
131
149
|
await backend.connect();
|
|
132
150
|
|
|
151
|
+
const spinner = new Spinner('Querying graph...');
|
|
152
|
+
spinner.start();
|
|
153
|
+
|
|
133
154
|
try {
|
|
155
|
+
const limit = parseInt(options.limit, 10);
|
|
156
|
+
if (isNaN(limit) || limit < 1) {
|
|
157
|
+
spinner.stop();
|
|
158
|
+
exitWithError('Invalid limit', ['Use a positive number, e.g.: --limit 10']);
|
|
159
|
+
}
|
|
160
|
+
|
|
134
161
|
// Raw Datalog mode
|
|
135
162
|
if (options.raw) {
|
|
136
|
-
|
|
163
|
+
spinner.stop();
|
|
164
|
+
await executeRawQuery(backend, pattern, limit, options.json);
|
|
137
165
|
return;
|
|
138
166
|
}
|
|
139
167
|
|
|
140
|
-
const limit = parseInt(options.limit, 10);
|
|
141
|
-
|
|
142
168
|
// Parse query with scope support
|
|
143
169
|
let query: ParsedQuery;
|
|
144
170
|
|
|
@@ -159,6 +185,8 @@ Examples:
|
|
|
159
185
|
// Find matching nodes
|
|
160
186
|
const nodes = await findNodes(backend, query, limit);
|
|
161
187
|
|
|
188
|
+
spinner.stop();
|
|
189
|
+
|
|
162
190
|
// Check if query has scope constraints for suggestion
|
|
163
191
|
const hasScope = query.file !== null || query.scopes.length > 0;
|
|
164
192
|
|
|
@@ -218,6 +246,7 @@ Examples:
|
|
|
218
246
|
}
|
|
219
247
|
|
|
220
248
|
} finally {
|
|
249
|
+
spinner.stop();
|
|
221
250
|
await backend.close();
|
|
222
251
|
}
|
|
223
252
|
});
|
|
@@ -237,23 +266,31 @@ function parsePattern(pattern: string): { type: string | null; name: string } {
|
|
|
237
266
|
fn: 'FUNCTION',
|
|
238
267
|
func: 'FUNCTION',
|
|
239
268
|
class: 'CLASS',
|
|
269
|
+
interface: 'INTERFACE',
|
|
270
|
+
type: 'TYPE',
|
|
271
|
+
enum: 'ENUM',
|
|
240
272
|
module: 'MODULE',
|
|
241
273
|
variable: 'VARIABLE',
|
|
242
274
|
var: 'VARIABLE',
|
|
243
275
|
const: 'CONSTANT',
|
|
244
276
|
constant: 'CONSTANT',
|
|
245
277
|
// HTTP route aliases
|
|
246
|
-
route:
|
|
247
|
-
endpoint:
|
|
278
|
+
route: HTTP_ROUTE_TYPE,
|
|
279
|
+
endpoint: HTTP_ROUTE_TYPE,
|
|
248
280
|
// HTTP request aliases
|
|
249
|
-
request:
|
|
250
|
-
fetch:
|
|
251
|
-
api:
|
|
281
|
+
request: HTTP_REQUEST_TYPE,
|
|
282
|
+
fetch: HTTP_REQUEST_TYPE,
|
|
283
|
+
api: HTTP_REQUEST_TYPE,
|
|
252
284
|
// Socket.IO aliases
|
|
253
|
-
event:
|
|
254
|
-
emit:
|
|
255
|
-
on:
|
|
256
|
-
listener:
|
|
285
|
+
event: SOCKETIO_EVENT_TYPE,
|
|
286
|
+
emit: SOCKETIO_EMIT_TYPE,
|
|
287
|
+
on: SOCKETIO_ON_TYPE,
|
|
288
|
+
listener: SOCKETIO_ON_TYPE,
|
|
289
|
+
// Grafema internal
|
|
290
|
+
plugin: GRAFEMA_PLUGIN_TYPE,
|
|
291
|
+
// Property access aliases (REG-395)
|
|
292
|
+
property: PROPERTY_ACCESS_TYPE,
|
|
293
|
+
prop: PROPERTY_ACCESS_TYPE,
|
|
257
294
|
};
|
|
258
295
|
|
|
259
296
|
if (typeMap[typeWord]) {
|
|
@@ -357,6 +394,36 @@ export function isFileScope(scope: string): boolean {
|
|
|
357
394
|
* @returns true if ID matches all constraints
|
|
358
395
|
*/
|
|
359
396
|
export function matchesScope(semanticId: string, file: string | null, scopes: string[]): boolean {
|
|
397
|
+
// No constraints = everything matches (regardless of ID format)
|
|
398
|
+
if (file === null && scopes.length === 0) return true;
|
|
399
|
+
|
|
400
|
+
// Try v2 parsing first
|
|
401
|
+
const parsedV2 = parseSemanticIdV2(semanticId);
|
|
402
|
+
if (parsedV2) {
|
|
403
|
+
// File scope check (v2)
|
|
404
|
+
if (file !== null) {
|
|
405
|
+
if (parsedV2.file === file) {
|
|
406
|
+
// Exact match - OK
|
|
407
|
+
} else if (parsedV2.file.endsWith('/' + file)) {
|
|
408
|
+
// Partial path match - OK
|
|
409
|
+
} else if (basename(parsedV2.file) === file) {
|
|
410
|
+
// Basename exact match - OK
|
|
411
|
+
} else {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Function/class scope check (v2): check namedParent
|
|
417
|
+
for (const scope of scopes) {
|
|
418
|
+
if (!parsedV2.namedParent || parsedV2.namedParent.toLowerCase() !== scope.toLowerCase()) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Fallback to v1 parsing
|
|
360
427
|
const parsed = parseSemanticId(semanticId);
|
|
361
428
|
if (!parsed) return false;
|
|
362
429
|
|
|
@@ -411,6 +478,17 @@ export function matchesScope(semanticId: string, file: string | null, scopes: st
|
|
|
411
478
|
* @returns Human-readable scope context or null
|
|
412
479
|
*/
|
|
413
480
|
export function extractScopeContext(semanticId: string): string | null {
|
|
481
|
+
// Try v2 parsing first
|
|
482
|
+
const parsedV2 = parseSemanticIdV2(semanticId);
|
|
483
|
+
if (parsedV2) {
|
|
484
|
+
if (parsedV2.namedParent) {
|
|
485
|
+
return `inside ${parsedV2.namedParent}`;
|
|
486
|
+
}
|
|
487
|
+
// v2 with no parent = top-level
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Fallback to v1 parsing
|
|
414
492
|
const parsed = parseSemanticId(semanticId);
|
|
415
493
|
if (!parsed) return null;
|
|
416
494
|
|
|
@@ -462,7 +540,7 @@ function matchesSearchPattern(
|
|
|
462
540
|
const lowerPattern = pattern.toLowerCase();
|
|
463
541
|
|
|
464
542
|
// HTTP routes: search method and path
|
|
465
|
-
if (nodeType ===
|
|
543
|
+
if (nodeType === HTTP_ROUTE_TYPE) {
|
|
466
544
|
const method = (node.method || '').toLowerCase();
|
|
467
545
|
const path = (node.path || '').toLowerCase();
|
|
468
546
|
|
|
@@ -488,7 +566,7 @@ function matchesSearchPattern(
|
|
|
488
566
|
}
|
|
489
567
|
|
|
490
568
|
// HTTP requests: search method and url
|
|
491
|
-
if (nodeType ===
|
|
569
|
+
if (nodeType === HTTP_REQUEST_TYPE) {
|
|
492
570
|
const method = (node.method || '').toLowerCase();
|
|
493
571
|
const url = (node.url || '').toLowerCase();
|
|
494
572
|
|
|
@@ -514,13 +592,13 @@ function matchesSearchPattern(
|
|
|
514
592
|
}
|
|
515
593
|
|
|
516
594
|
// Socket.IO event channels: search name field (standard)
|
|
517
|
-
if (nodeType ===
|
|
595
|
+
if (nodeType === SOCKETIO_EVENT_TYPE) {
|
|
518
596
|
const nodeName = (node.name || '').toLowerCase();
|
|
519
597
|
return nodeName.includes(lowerPattern);
|
|
520
598
|
}
|
|
521
599
|
|
|
522
600
|
// Socket.IO emit/on: search event field
|
|
523
|
-
if (nodeType ===
|
|
601
|
+
if (nodeType === SOCKETIO_EMIT_TYPE || nodeType === SOCKETIO_ON_TYPE) {
|
|
524
602
|
const eventName = (node.event || '').toLowerCase();
|
|
525
603
|
return eventName.includes(lowerPattern);
|
|
526
604
|
}
|
|
@@ -544,18 +622,22 @@ async function findNodes(
|
|
|
544
622
|
: [
|
|
545
623
|
'FUNCTION',
|
|
546
624
|
'CLASS',
|
|
625
|
+
'INTERFACE',
|
|
626
|
+
'TYPE',
|
|
627
|
+
'ENUM',
|
|
547
628
|
'MODULE',
|
|
548
629
|
'VARIABLE',
|
|
549
630
|
'CONSTANT',
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
631
|
+
HTTP_ROUTE_TYPE,
|
|
632
|
+
HTTP_REQUEST_TYPE,
|
|
633
|
+
SOCKETIO_EVENT_TYPE,
|
|
634
|
+
SOCKETIO_EMIT_TYPE,
|
|
635
|
+
SOCKETIO_ON_TYPE,
|
|
636
|
+
PROPERTY_ACCESS_TYPE,
|
|
555
637
|
];
|
|
556
638
|
|
|
557
639
|
for (const nodeType of searchTypes) {
|
|
558
|
-
for await (const node of backend.queryNodes({ nodeType
|
|
640
|
+
for await (const node of backend.queryNodes({ nodeType })) {
|
|
559
641
|
// Type-aware field matching (name)
|
|
560
642
|
const nameMatches = matchesSearchPattern(node, nodeType, query.name);
|
|
561
643
|
if (!nameMatches) continue;
|
|
@@ -576,24 +658,24 @@ async function findNodes(
|
|
|
576
658
|
nodeInfo.scopeContext = extractScopeContext(node.id);
|
|
577
659
|
|
|
578
660
|
// Include method and path for http:route nodes
|
|
579
|
-
if (nodeType ===
|
|
661
|
+
if (nodeType === HTTP_ROUTE_TYPE) {
|
|
580
662
|
nodeInfo.method = node.method as string | undefined;
|
|
581
663
|
nodeInfo.path = node.path as string | undefined;
|
|
582
664
|
}
|
|
583
665
|
|
|
584
666
|
// Include method and url for http:request nodes
|
|
585
|
-
if (nodeType ===
|
|
667
|
+
if (nodeType === HTTP_REQUEST_TYPE) {
|
|
586
668
|
nodeInfo.method = node.method as string | undefined;
|
|
587
669
|
nodeInfo.url = node.url as string | undefined;
|
|
588
670
|
}
|
|
589
671
|
|
|
590
672
|
// Include event field for Socket.IO nodes
|
|
591
|
-
if (nodeType ===
|
|
673
|
+
if (nodeType === SOCKETIO_EVENT_TYPE || nodeType === SOCKETIO_EMIT_TYPE || nodeType === SOCKETIO_ON_TYPE) {
|
|
592
674
|
nodeInfo.event = node.event as string | undefined;
|
|
593
675
|
}
|
|
594
676
|
|
|
595
677
|
// Include emit-specific fields
|
|
596
|
-
if (nodeType ===
|
|
678
|
+
if (nodeType === SOCKETIO_EMIT_TYPE) {
|
|
597
679
|
nodeInfo.room = node.room as string | undefined;
|
|
598
680
|
nodeInfo.namespace = node.namespace as string | undefined;
|
|
599
681
|
nodeInfo.broadcast = node.broadcast as boolean | undefined;
|
|
@@ -601,11 +683,26 @@ async function findNodes(
|
|
|
601
683
|
}
|
|
602
684
|
|
|
603
685
|
// Include listener-specific fields
|
|
604
|
-
if (nodeType ===
|
|
686
|
+
if (nodeType === SOCKETIO_ON_TYPE) {
|
|
605
687
|
nodeInfo.objectName = node.objectName as string | undefined;
|
|
606
688
|
nodeInfo.handlerName = node.handlerName as string | undefined;
|
|
607
689
|
}
|
|
608
690
|
|
|
691
|
+
// Include plugin-specific fields
|
|
692
|
+
if (nodeType === GRAFEMA_PLUGIN_TYPE) {
|
|
693
|
+
nodeInfo.phase = node.phase as string | undefined;
|
|
694
|
+
nodeInfo.priority = node.priority as number | undefined;
|
|
695
|
+
nodeInfo.builtin = node.builtin as boolean | undefined;
|
|
696
|
+
nodeInfo.createsNodes = node.createsNodes as string[] | undefined;
|
|
697
|
+
nodeInfo.createsEdges = node.createsEdges as string[] | undefined;
|
|
698
|
+
nodeInfo.dependencies = node.dependencies as string[] | undefined;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Include objectName for PROPERTY_ACCESS nodes (REG-395)
|
|
702
|
+
if (nodeType === PROPERTY_ACCESS_TYPE) {
|
|
703
|
+
nodeInfo.objectName = node.objectName as string | undefined;
|
|
704
|
+
}
|
|
705
|
+
|
|
609
706
|
results.push(nodeInfo);
|
|
610
707
|
if (results.length >= limit) break;
|
|
611
708
|
}
|
|
@@ -712,29 +809,35 @@ async function getCallees(
|
|
|
712
809
|
*/
|
|
713
810
|
async function displayNode(node: NodeInfo, projectPath: string, backend: RFDBServerBackend): Promise<void> {
|
|
714
811
|
// Special formatting for HTTP routes
|
|
715
|
-
if (node.type ===
|
|
812
|
+
if (node.type === HTTP_ROUTE_TYPE && node.method && node.path) {
|
|
716
813
|
console.log(formatHttpRouteDisplay(node, projectPath));
|
|
717
814
|
return;
|
|
718
815
|
}
|
|
719
816
|
|
|
720
817
|
// Special formatting for HTTP requests
|
|
721
|
-
if (node.type ===
|
|
818
|
+
if (node.type === HTTP_REQUEST_TYPE) {
|
|
722
819
|
console.log(formatHttpRequestDisplay(node, projectPath));
|
|
723
820
|
return;
|
|
724
821
|
}
|
|
725
822
|
|
|
726
823
|
// Special formatting for Socket.IO event channels
|
|
727
|
-
if (node.type ===
|
|
824
|
+
if (node.type === SOCKETIO_EVENT_TYPE) {
|
|
728
825
|
console.log(await formatSocketEventDisplay(node, projectPath, backend));
|
|
729
826
|
return;
|
|
730
827
|
}
|
|
731
828
|
|
|
732
829
|
// Special formatting for Socket.IO emit/on
|
|
733
|
-
if (node.type ===
|
|
830
|
+
if (node.type === SOCKETIO_EMIT_TYPE || node.type === SOCKETIO_ON_TYPE) {
|
|
734
831
|
console.log(formatSocketIONodeDisplay(node, projectPath));
|
|
735
832
|
return;
|
|
736
833
|
}
|
|
737
834
|
|
|
835
|
+
// Special formatting for Grafema plugin nodes
|
|
836
|
+
if (node.type === GRAFEMA_PLUGIN_TYPE) {
|
|
837
|
+
console.log(formatPluginDisplay(node, projectPath));
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
738
841
|
console.log(formatNodeDisplay(node, { projectPath }));
|
|
739
842
|
|
|
740
843
|
// Add scope context if present
|
|
@@ -758,7 +861,7 @@ function formatHttpRouteDisplay(node: NodeInfo, projectPath: string): string {
|
|
|
758
861
|
|
|
759
862
|
// Line 2: Location
|
|
760
863
|
if (node.file) {
|
|
761
|
-
const relPath =
|
|
864
|
+
const relPath = toRelativeDisplay(node.file, projectPath);
|
|
762
865
|
const loc = node.line ? `${relPath}:${node.line}` : relPath;
|
|
763
866
|
lines.push(` Location: ${loc}`);
|
|
764
867
|
}
|
|
@@ -783,7 +886,7 @@ function formatHttpRequestDisplay(node: NodeInfo, projectPath: string): string {
|
|
|
783
886
|
|
|
784
887
|
// Line 2: Location
|
|
785
888
|
if (node.file) {
|
|
786
|
-
const relPath =
|
|
889
|
+
const relPath = toRelativeDisplay(node.file, projectPath);
|
|
787
890
|
const loc = node.line ? `${relPath}:${node.line}` : relPath;
|
|
788
891
|
lines.push(` Location: ${loc}`);
|
|
789
892
|
}
|
|
@@ -870,7 +973,7 @@ function formatSocketIONodeDisplay(node: NodeInfo, projectPath: string): string
|
|
|
870
973
|
}
|
|
871
974
|
|
|
872
975
|
// Emit-specific fields
|
|
873
|
-
if (node.type ===
|
|
976
|
+
if (node.type === SOCKETIO_EMIT_TYPE) {
|
|
874
977
|
if (node.room) {
|
|
875
978
|
lines.push(` Room: ${node.room}`);
|
|
876
979
|
}
|
|
@@ -883,7 +986,7 @@ function formatSocketIONodeDisplay(node: NodeInfo, projectPath: string): string
|
|
|
883
986
|
}
|
|
884
987
|
|
|
885
988
|
// Listener-specific fields
|
|
886
|
-
if (node.type ===
|
|
989
|
+
if (node.type === SOCKETIO_ON_TYPE && node.handlerName) {
|
|
887
990
|
lines.push(` Handler: ${node.handlerName}`);
|
|
888
991
|
}
|
|
889
992
|
|
|
@@ -891,18 +994,96 @@ function formatSocketIONodeDisplay(node: NodeInfo, projectPath: string): string
|
|
|
891
994
|
}
|
|
892
995
|
|
|
893
996
|
/**
|
|
894
|
-
*
|
|
997
|
+
* Format Grafema plugin node for display.
|
|
998
|
+
*
|
|
999
|
+
* Output:
|
|
1000
|
+
* [grafema:plugin] HTTPConnectionEnricher
|
|
1001
|
+
* Phase: ENRICHMENT (priority: 50)
|
|
1002
|
+
* Creates: edges: INTERACTS_WITH, HTTP_RECEIVES
|
|
1003
|
+
* Dependencies: ExpressRouteAnalyzer, FetchAnalyzer, ExpressResponseAnalyzer
|
|
1004
|
+
* Source: packages/core/src/plugins/enrichment/HTTPConnectionEnricher.ts
|
|
1005
|
+
*/
|
|
1006
|
+
function formatPluginDisplay(node: NodeInfo, projectPath: string): string {
|
|
1007
|
+
const lines: string[] = [];
|
|
1008
|
+
|
|
1009
|
+
lines.push(`[${node.type}] ${node.name}`);
|
|
1010
|
+
|
|
1011
|
+
const phase = (node.phase as string) || 'unknown';
|
|
1012
|
+
const priority = (node.priority as number) ?? 0;
|
|
1013
|
+
lines.push(` Phase: ${phase} (priority: ${priority})`);
|
|
1014
|
+
|
|
1015
|
+
const createsNodes = (node.createsNodes as string[]) || [];
|
|
1016
|
+
const createsEdges = (node.createsEdges as string[]) || [];
|
|
1017
|
+
const createsParts: string[] = [];
|
|
1018
|
+
if (createsNodes.length > 0) createsParts.push(`nodes: ${createsNodes.join(', ')}`);
|
|
1019
|
+
if (createsEdges.length > 0) createsParts.push(`edges: ${createsEdges.join(', ')}`);
|
|
1020
|
+
if (createsParts.length > 0) {
|
|
1021
|
+
lines.push(` Creates: ${createsParts.join('; ')}`);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const deps = (node.dependencies as string[]) || [];
|
|
1025
|
+
if (deps.length > 0) {
|
|
1026
|
+
lines.push(` Dependencies: ${deps.join(', ')}`);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (node.file) {
|
|
1030
|
+
const relPath = toRelativeDisplay(node.file, projectPath);
|
|
1031
|
+
lines.push(` Source: ${relPath}`);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return lines.join('\n');
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/** Built-in Datalog predicates supported by RFDB server */
|
|
1038
|
+
export const BUILTIN_PREDICATES = new Set([
|
|
1039
|
+
'node', 'type', 'edge', 'incoming', 'path',
|
|
1040
|
+
'attr', 'attr_edge',
|
|
1041
|
+
'neq', 'starts_with', 'not_starts_with',
|
|
1042
|
+
]);
|
|
1043
|
+
|
|
1044
|
+
/** Extract predicate names from a Datalog query string */
|
|
1045
|
+
export function extractPredicates(query: string): string[] {
|
|
1046
|
+
const regex = /\b([a-z_][a-z0-9_]*)\s*\(/g;
|
|
1047
|
+
const predicates = new Set<string>();
|
|
1048
|
+
let match;
|
|
1049
|
+
while ((match = regex.exec(query)) !== null) {
|
|
1050
|
+
predicates.add(match[1]);
|
|
1051
|
+
}
|
|
1052
|
+
return [...predicates];
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/** Extract predicate names defined as rule heads (word(...) :-) */
|
|
1056
|
+
export function extractRuleHeads(query: string): Set<string> {
|
|
1057
|
+
const regex = /\b([a-z_][a-z0-9_]*)\s*\([^)]*\)\s*:-/g;
|
|
1058
|
+
const heads = new Set<string>();
|
|
1059
|
+
let match;
|
|
1060
|
+
while ((match = regex.exec(query)) !== null) {
|
|
1061
|
+
heads.add(match[1]);
|
|
1062
|
+
}
|
|
1063
|
+
return heads;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/** Find predicates in a query that are not built-in and not user-defined rule heads */
|
|
1067
|
+
export function getUnknownPredicates(query: string): string[] {
|
|
1068
|
+
const predicates = extractPredicates(query);
|
|
1069
|
+
const ruleHeads = extractRuleHeads(query);
|
|
1070
|
+
return predicates.filter(p => !BUILTIN_PREDICATES.has(p) && !ruleHeads.has(p));
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Execute raw Datalog query.
|
|
1075
|
+
* Uses unified executeDatalog endpoint which auto-detects rules vs direct queries.
|
|
895
1076
|
*/
|
|
896
1077
|
async function executeRawQuery(
|
|
897
1078
|
backend: RFDBServerBackend,
|
|
898
1079
|
query: string,
|
|
899
|
-
|
|
1080
|
+
limit: number,
|
|
1081
|
+
json?: boolean
|
|
900
1082
|
): Promise<void> {
|
|
901
|
-
const results = await backend.
|
|
902
|
-
const limit = parseInt(options.limit, 10);
|
|
1083
|
+
const results = await backend.executeDatalog(query);
|
|
903
1084
|
const limited = results.slice(0, limit);
|
|
904
1085
|
|
|
905
|
-
if (
|
|
1086
|
+
if (json) {
|
|
906
1087
|
console.log(JSON.stringify(limited, null, 2));
|
|
907
1088
|
} else {
|
|
908
1089
|
if (limited.length === 0) {
|
|
@@ -916,4 +1097,14 @@ async function executeRawQuery(
|
|
|
916
1097
|
}
|
|
917
1098
|
}
|
|
918
1099
|
}
|
|
1100
|
+
|
|
1101
|
+
// Show warning for unknown predicates (on stderr, works in both text and JSON mode)
|
|
1102
|
+
if (limited.length === 0) {
|
|
1103
|
+
const unknown = getUnknownPredicates(query);
|
|
1104
|
+
if (unknown.length > 0) {
|
|
1105
|
+
const unknownList = unknown.map(p => `'${p}'`).join(', ');
|
|
1106
|
+
const builtinList = [...BUILTIN_PREDICATES].join(', ');
|
|
1107
|
+
console.error(`Note: unknown predicate ${unknownList}. Built-in predicates: ${builtinList}`);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
919
1110
|
}
|