@grafema/cli 0.1.1-alpha
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/LICENSE +190 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +36 -0
- package/dist/commands/analyze.d.ts +6 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +209 -0
- package/dist/commands/check.d.ts +10 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +295 -0
- package/dist/commands/coverage.d.ts +11 -0
- package/dist/commands/coverage.d.ts.map +1 -0
- package/dist/commands/coverage.js +96 -0
- package/dist/commands/explore.d.ts +6 -0
- package/dist/commands/explore.d.ts.map +1 -0
- package/dist/commands/explore.js +633 -0
- package/dist/commands/get.d.ts +10 -0
- package/dist/commands/get.d.ts.map +1 -0
- package/dist/commands/get.js +189 -0
- package/dist/commands/impact.d.ts +10 -0
- package/dist/commands/impact.d.ts.map +1 -0
- package/dist/commands/impact.js +313 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +94 -0
- package/dist/commands/overview.d.ts +6 -0
- package/dist/commands/overview.d.ts.map +1 -0
- package/dist/commands/overview.js +91 -0
- package/dist/commands/query.d.ts +13 -0
- package/dist/commands/query.d.ts.map +1 -0
- package/dist/commands/query.js +340 -0
- package/dist/commands/server.d.ts +11 -0
- package/dist/commands/server.d.ts.map +1 -0
- package/dist/commands/server.js +300 -0
- package/dist/commands/stats.d.ts +6 -0
- package/dist/commands/stats.d.ts.map +1 -0
- package/dist/commands/stats.js +52 -0
- package/dist/commands/trace.d.ts +10 -0
- package/dist/commands/trace.d.ts.map +1 -0
- package/dist/commands/trace.js +270 -0
- package/dist/utils/codePreview.d.ts +28 -0
- package/dist/utils/codePreview.d.ts.map +1 -0
- package/dist/utils/codePreview.js +51 -0
- package/dist/utils/errorFormatter.d.ts +24 -0
- package/dist/utils/errorFormatter.d.ts.map +1 -0
- package/dist/utils/errorFormatter.js +32 -0
- package/dist/utils/formatNode.d.ts +53 -0
- package/dist/utils/formatNode.d.ts.map +1 -0
- package/dist/utils/formatNode.js +49 -0
- package/package.json +54 -0
- package/src/cli.ts +41 -0
- package/src/commands/analyze.ts +271 -0
- package/src/commands/check.ts +379 -0
- package/src/commands/coverage.ts +108 -0
- package/src/commands/explore.tsx +1056 -0
- package/src/commands/get.ts +265 -0
- package/src/commands/impact.ts +400 -0
- package/src/commands/init.ts +112 -0
- package/src/commands/overview.ts +108 -0
- package/src/commands/query.ts +425 -0
- package/src/commands/server.ts +335 -0
- package/src/commands/stats.ts +58 -0
- package/src/commands/trace.ts +341 -0
- package/src/utils/codePreview.ts +77 -0
- package/src/utils/errorFormatter.ts +35 -0
- package/src/utils/formatNode.ts +88 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get command - Retrieve node by semantic ID
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* grafema get "file.js->scope->TYPE->name"
|
|
6
|
+
* grafema get "file.js->scope->TYPE->name" --json
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import { resolve, join } from 'path';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
11
|
+
import { RFDBServerBackend } from '@grafema/core';
|
|
12
|
+
import { formatNodeDisplay } from '../utils/formatNode.js';
|
|
13
|
+
import { exitWithError } from '../utils/errorFormatter.js';
|
|
14
|
+
export const getCommand = new Command('get')
|
|
15
|
+
.description('Retrieve a node by its semantic ID')
|
|
16
|
+
.argument('<semantic-id>', 'Semantic ID of the node (e.g., "file.js->scope->TYPE->name")')
|
|
17
|
+
.option('-p, --project <path>', 'Project path', '.')
|
|
18
|
+
.option('-j, --json', 'Output as JSON')
|
|
19
|
+
.action(async (semanticId, options) => {
|
|
20
|
+
const projectPath = resolve(options.project);
|
|
21
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
22
|
+
const dbPath = join(grafemaDir, 'graph.rfdb');
|
|
23
|
+
if (!existsSync(dbPath)) {
|
|
24
|
+
exitWithError('No graph database found', ['Run: grafema analyze']);
|
|
25
|
+
}
|
|
26
|
+
const backend = new RFDBServerBackend({ dbPath });
|
|
27
|
+
await backend.connect();
|
|
28
|
+
try {
|
|
29
|
+
// Retrieve node by semantic ID
|
|
30
|
+
const node = await backend.getNode(semanticId);
|
|
31
|
+
if (!node) {
|
|
32
|
+
exitWithError('Node not found', [
|
|
33
|
+
`ID: ${semanticId}`,
|
|
34
|
+
'Try: grafema query "<name>" to search for nodes',
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
// Get incoming and outgoing edges
|
|
38
|
+
const incomingEdges = await backend.getIncomingEdges(semanticId, null);
|
|
39
|
+
const outgoingEdges = await backend.getOutgoingEdges(semanticId, null);
|
|
40
|
+
if (options.json) {
|
|
41
|
+
await outputJSON(backend, node, incomingEdges, outgoingEdges);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
await outputText(backend, node, incomingEdges, outgoingEdges, projectPath);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
await backend.close();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* Output node and edges as JSON
|
|
53
|
+
*/
|
|
54
|
+
async function outputJSON(backend, node, incomingEdges, outgoingEdges) {
|
|
55
|
+
// Fetch target node names for all edges
|
|
56
|
+
const incomingWithNames = await Promise.all(incomingEdges.map(async (edge) => ({
|
|
57
|
+
edgeType: edge.edgeType || edge.type || 'UNKNOWN',
|
|
58
|
+
targetId: edge.src,
|
|
59
|
+
targetName: await getNodeName(backend, edge.src),
|
|
60
|
+
})));
|
|
61
|
+
const outgoingWithNames = await Promise.all(outgoingEdges.map(async (edge) => ({
|
|
62
|
+
edgeType: edge.edgeType || edge.type || 'UNKNOWN',
|
|
63
|
+
targetId: edge.dst,
|
|
64
|
+
targetName: await getNodeName(backend, edge.dst),
|
|
65
|
+
})));
|
|
66
|
+
const result = {
|
|
67
|
+
node: {
|
|
68
|
+
id: node.id,
|
|
69
|
+
type: node.type || 'UNKNOWN',
|
|
70
|
+
name: node.name || '',
|
|
71
|
+
file: node.file || '',
|
|
72
|
+
line: node.line,
|
|
73
|
+
...getMetadataFields(node),
|
|
74
|
+
},
|
|
75
|
+
edges: {
|
|
76
|
+
incoming: incomingWithNames,
|
|
77
|
+
outgoing: outgoingWithNames,
|
|
78
|
+
},
|
|
79
|
+
stats: {
|
|
80
|
+
incomingCount: incomingEdges.length,
|
|
81
|
+
outgoingCount: outgoingEdges.length,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
console.log(JSON.stringify(result, null, 2));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Output node and edges as formatted text
|
|
88
|
+
*/
|
|
89
|
+
async function outputText(backend, node, incomingEdges, outgoingEdges, projectPath) {
|
|
90
|
+
const nodeInfo = {
|
|
91
|
+
id: node.id,
|
|
92
|
+
type: node.type || 'UNKNOWN',
|
|
93
|
+
name: node.name || '',
|
|
94
|
+
file: node.file || '',
|
|
95
|
+
line: node.line,
|
|
96
|
+
};
|
|
97
|
+
// Display node details
|
|
98
|
+
console.log(formatNodeDisplay(nodeInfo, { projectPath }));
|
|
99
|
+
// Display metadata if present
|
|
100
|
+
const metadata = getMetadataFields(node);
|
|
101
|
+
if (Object.keys(metadata).length > 0) {
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log('Metadata:');
|
|
104
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
105
|
+
console.log(` ${key}: ${JSON.stringify(value)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Display edges
|
|
109
|
+
console.log('');
|
|
110
|
+
await displayEdges(backend, 'Incoming', incomingEdges, (edge) => edge.src);
|
|
111
|
+
console.log('');
|
|
112
|
+
await displayEdges(backend, 'Outgoing', outgoingEdges, (edge) => edge.dst);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Display edges grouped by type, limited to 20 in text mode
|
|
116
|
+
*/
|
|
117
|
+
async function displayEdges(backend, direction, edges, getTargetId) {
|
|
118
|
+
const totalCount = edges.length;
|
|
119
|
+
if (totalCount === 0) {
|
|
120
|
+
console.log(`${direction} edges (0):`);
|
|
121
|
+
console.log(' (none)');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Group edges by type
|
|
125
|
+
const byType = new Map();
|
|
126
|
+
for (const edge of edges) {
|
|
127
|
+
const edgeType = edge.edgeType || edge.type || 'UNKNOWN';
|
|
128
|
+
const targetId = getTargetId(edge);
|
|
129
|
+
const targetName = await getNodeName(backend, targetId);
|
|
130
|
+
if (!byType.has(edgeType)) {
|
|
131
|
+
byType.set(edgeType, []);
|
|
132
|
+
}
|
|
133
|
+
byType.get(edgeType).push({ edgeType, targetId, targetName });
|
|
134
|
+
}
|
|
135
|
+
// Display header with count
|
|
136
|
+
const limitApplied = totalCount > 20;
|
|
137
|
+
console.log(`${direction} edges (${totalCount}):`);
|
|
138
|
+
// Display edges, limited to 20 total
|
|
139
|
+
let displayed = 0;
|
|
140
|
+
const limit = 20;
|
|
141
|
+
for (const [edgeType, edgesOfType] of Array.from(byType.entries())) {
|
|
142
|
+
console.log(` ${edgeType}:`);
|
|
143
|
+
for (const edge of edgesOfType) {
|
|
144
|
+
if (displayed >= limit)
|
|
145
|
+
break;
|
|
146
|
+
// Format: TYPE#name
|
|
147
|
+
const label = edge.targetName ? `${edge.edgeType}#${edge.targetName}` : edge.targetId;
|
|
148
|
+
console.log(` ${label}`);
|
|
149
|
+
displayed++;
|
|
150
|
+
}
|
|
151
|
+
if (displayed >= limit)
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
// Show "and X more" if we hit the limit
|
|
155
|
+
if (limitApplied) {
|
|
156
|
+
const remaining = totalCount - displayed;
|
|
157
|
+
console.log(` ... and ${remaining} more (use --json to see all)`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get node name for display
|
|
162
|
+
*/
|
|
163
|
+
async function getNodeName(backend, nodeId) {
|
|
164
|
+
try {
|
|
165
|
+
const node = await backend.getNode(nodeId);
|
|
166
|
+
if (node) {
|
|
167
|
+
return node.name || '';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Ignore errors
|
|
172
|
+
}
|
|
173
|
+
return '';
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Extract metadata fields (exclude standard fields)
|
|
177
|
+
*/
|
|
178
|
+
function getMetadataFields(node) {
|
|
179
|
+
const standardFields = new Set([
|
|
180
|
+
'id', 'type', 'nodeType', 'name', 'file', 'line',
|
|
181
|
+
]);
|
|
182
|
+
const metadata = {};
|
|
183
|
+
for (const [key, value] of Object.entries(node)) {
|
|
184
|
+
if (!standardFields.has(key) && value !== undefined && value !== null) {
|
|
185
|
+
metadata[key] = value;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return metadata;
|
|
189
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Impact command - Change impact analysis
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* grafema impact "function authenticate"
|
|
6
|
+
* grafema impact "class UserService"
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
export declare const impactCommand: Command;
|
|
10
|
+
//# sourceMappingURL=impact.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Impact command - Change impact analysis
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* grafema impact "function authenticate"
|
|
6
|
+
* grafema impact "class UserService"
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import { resolve, join, dirname } from 'path';
|
|
10
|
+
import { relative } from 'path';
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import { RFDBServerBackend } from '@grafema/core';
|
|
13
|
+
import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
|
|
14
|
+
import { exitWithError } from '../utils/errorFormatter.js';
|
|
15
|
+
export const impactCommand = new Command('impact')
|
|
16
|
+
.description('Analyze change impact for a function or class')
|
|
17
|
+
.argument('<pattern>', 'Target: "function X" or "class Y" or just "X"')
|
|
18
|
+
.option('-p, --project <path>', 'Project path', '.')
|
|
19
|
+
.option('-j, --json', 'Output as JSON')
|
|
20
|
+
.option('-d, --depth <n>', 'Max traversal depth', '10')
|
|
21
|
+
.action(async (pattern, options) => {
|
|
22
|
+
const projectPath = resolve(options.project);
|
|
23
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
24
|
+
const dbPath = join(grafemaDir, 'graph.rfdb');
|
|
25
|
+
if (!existsSync(dbPath)) {
|
|
26
|
+
exitWithError('No graph database found', ['Run: grafema analyze']);
|
|
27
|
+
}
|
|
28
|
+
const backend = new RFDBServerBackend({ dbPath });
|
|
29
|
+
await backend.connect();
|
|
30
|
+
try {
|
|
31
|
+
const { type, name } = parsePattern(pattern);
|
|
32
|
+
const maxDepth = parseInt(options.depth, 10);
|
|
33
|
+
console.log(`Analyzing impact of changing ${name}...`);
|
|
34
|
+
console.log('');
|
|
35
|
+
// Find target node
|
|
36
|
+
const target = await findTarget(backend, type, name);
|
|
37
|
+
if (!target) {
|
|
38
|
+
console.log(`No ${type || 'node'} "${name}" found`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Analyze impact
|
|
42
|
+
const impact = await analyzeImpact(backend, target, maxDepth, projectPath);
|
|
43
|
+
if (options.json) {
|
|
44
|
+
console.log(JSON.stringify({
|
|
45
|
+
target: impact.target,
|
|
46
|
+
directCallers: impact.directCallers.length,
|
|
47
|
+
transitiveCallers: impact.transitiveCallers.length,
|
|
48
|
+
affectedModules: Object.fromEntries(impact.affectedModules),
|
|
49
|
+
callChains: impact.callChains.slice(0, 5),
|
|
50
|
+
}, null, 2));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Display results
|
|
54
|
+
displayImpact(impact, projectPath);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
await backend.close();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* Parse pattern like "function authenticate"
|
|
62
|
+
*/
|
|
63
|
+
function parsePattern(pattern) {
|
|
64
|
+
const words = pattern.trim().split(/\s+/);
|
|
65
|
+
if (words.length >= 2) {
|
|
66
|
+
const typeWord = words[0].toLowerCase();
|
|
67
|
+
const name = words.slice(1).join(' ');
|
|
68
|
+
const typeMap = {
|
|
69
|
+
function: 'FUNCTION',
|
|
70
|
+
fn: 'FUNCTION',
|
|
71
|
+
class: 'CLASS',
|
|
72
|
+
module: 'MODULE',
|
|
73
|
+
};
|
|
74
|
+
if (typeMap[typeWord]) {
|
|
75
|
+
return { type: typeMap[typeWord], name };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { type: null, name: pattern.trim() };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Find target node
|
|
82
|
+
*/
|
|
83
|
+
async function findTarget(backend, type, name) {
|
|
84
|
+
const searchTypes = type ? [type] : ['FUNCTION', 'CLASS'];
|
|
85
|
+
for (const nodeType of searchTypes) {
|
|
86
|
+
for await (const node of backend.queryNodes({ nodeType: nodeType })) {
|
|
87
|
+
const nodeName = node.name || '';
|
|
88
|
+
if (nodeName.toLowerCase() === name.toLowerCase()) {
|
|
89
|
+
return {
|
|
90
|
+
id: node.id,
|
|
91
|
+
type: node.type || nodeType,
|
|
92
|
+
name: nodeName,
|
|
93
|
+
file: node.file || '',
|
|
94
|
+
line: node.line,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Analyze impact of changing a node
|
|
103
|
+
*/
|
|
104
|
+
async function analyzeImpact(backend, target, maxDepth, projectPath) {
|
|
105
|
+
const directCallers = [];
|
|
106
|
+
const transitiveCallers = [];
|
|
107
|
+
const affectedModules = new Map();
|
|
108
|
+
const callChains = [];
|
|
109
|
+
const visited = new Set();
|
|
110
|
+
// BFS to find all callers
|
|
111
|
+
const queue = [
|
|
112
|
+
{ id: target.id, depth: 0, chain: [target.name] }
|
|
113
|
+
];
|
|
114
|
+
while (queue.length > 0) {
|
|
115
|
+
const { id, depth, chain } = queue.shift();
|
|
116
|
+
if (visited.has(id))
|
|
117
|
+
continue;
|
|
118
|
+
visited.add(id);
|
|
119
|
+
if (depth > maxDepth)
|
|
120
|
+
continue;
|
|
121
|
+
try {
|
|
122
|
+
// Find what calls this node
|
|
123
|
+
// First, find CALL nodes that have this as target
|
|
124
|
+
const containingCalls = await findCallsToNode(backend, id);
|
|
125
|
+
for (const callNode of containingCalls) {
|
|
126
|
+
// Find the function containing this call
|
|
127
|
+
const container = await findContainingFunction(backend, callNode.id);
|
|
128
|
+
if (container && !visited.has(container.id)) {
|
|
129
|
+
const caller = {
|
|
130
|
+
id: container.id,
|
|
131
|
+
type: container.type,
|
|
132
|
+
name: container.name,
|
|
133
|
+
file: container.file,
|
|
134
|
+
line: container.line,
|
|
135
|
+
};
|
|
136
|
+
if (depth === 0) {
|
|
137
|
+
directCallers.push(caller);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
transitiveCallers.push(caller);
|
|
141
|
+
}
|
|
142
|
+
// Track affected modules
|
|
143
|
+
const modulePath = getModulePath(caller.file, projectPath);
|
|
144
|
+
affectedModules.set(modulePath, (affectedModules.get(modulePath) || 0) + 1);
|
|
145
|
+
// Track call chain
|
|
146
|
+
const newChain = [...chain, caller.name];
|
|
147
|
+
if (newChain.length <= 4) {
|
|
148
|
+
callChains.push(newChain);
|
|
149
|
+
}
|
|
150
|
+
// Continue BFS
|
|
151
|
+
queue.push({ id: container.id, depth: depth + 1, chain: newChain });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Ignore errors
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Sort call chains by length
|
|
160
|
+
callChains.sort((a, b) => b.length - a.length);
|
|
161
|
+
return {
|
|
162
|
+
target,
|
|
163
|
+
directCallers,
|
|
164
|
+
transitiveCallers,
|
|
165
|
+
affectedModules,
|
|
166
|
+
callChains,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Find CALL nodes that reference a target
|
|
171
|
+
*/
|
|
172
|
+
async function findCallsToNode(backend, targetId) {
|
|
173
|
+
const calls = [];
|
|
174
|
+
try {
|
|
175
|
+
// Get incoming CALLS edges
|
|
176
|
+
const edges = await backend.getIncomingEdges(targetId, ['CALLS']);
|
|
177
|
+
for (const edge of edges) {
|
|
178
|
+
const callNode = await backend.getNode(edge.src);
|
|
179
|
+
if (callNode) {
|
|
180
|
+
calls.push({
|
|
181
|
+
id: callNode.id,
|
|
182
|
+
type: callNode.type || 'CALL',
|
|
183
|
+
name: callNode.name || '',
|
|
184
|
+
file: callNode.file || '',
|
|
185
|
+
line: callNode.line,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Ignore
|
|
192
|
+
}
|
|
193
|
+
return calls;
|
|
194
|
+
}
|
|
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
|
+
/**
|
|
240
|
+
* Get module path relative to project
|
|
241
|
+
*/
|
|
242
|
+
function getModulePath(file, projectPath) {
|
|
243
|
+
if (!file)
|
|
244
|
+
return '<unknown>';
|
|
245
|
+
const relPath = relative(projectPath, file);
|
|
246
|
+
const dir = dirname(relPath);
|
|
247
|
+
return dir === '.' ? relPath : `${dir}/*`;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Display impact analysis results with semantic IDs
|
|
251
|
+
*/
|
|
252
|
+
function displayImpact(impact, projectPath) {
|
|
253
|
+
console.log(formatNodeDisplay(impact.target, { projectPath }));
|
|
254
|
+
console.log('');
|
|
255
|
+
// Direct impact
|
|
256
|
+
console.log('Direct impact:');
|
|
257
|
+
console.log(` ${impact.directCallers.length} direct callers`);
|
|
258
|
+
console.log(` ${impact.transitiveCallers.length} transitive callers`);
|
|
259
|
+
console.log(` ${impact.directCallers.length + impact.transitiveCallers.length} total affected`);
|
|
260
|
+
console.log('');
|
|
261
|
+
// Show direct callers
|
|
262
|
+
if (impact.directCallers.length > 0) {
|
|
263
|
+
console.log('Direct callers:');
|
|
264
|
+
for (const caller of impact.directCallers.slice(0, 10)) {
|
|
265
|
+
console.log(` <- ${formatNodeInline(caller)}`);
|
|
266
|
+
}
|
|
267
|
+
if (impact.directCallers.length > 10) {
|
|
268
|
+
console.log(` ... and ${impact.directCallers.length - 10} more`);
|
|
269
|
+
}
|
|
270
|
+
console.log('');
|
|
271
|
+
}
|
|
272
|
+
// Affected modules
|
|
273
|
+
if (impact.affectedModules.size > 0) {
|
|
274
|
+
console.log('Affected modules:');
|
|
275
|
+
const sorted = [...impact.affectedModules.entries()].sort((a, b) => b[1] - a[1]);
|
|
276
|
+
for (const [module, count] of sorted.slice(0, 5)) {
|
|
277
|
+
console.log(` ├─ ${module} (${count} calls)`);
|
|
278
|
+
}
|
|
279
|
+
if (sorted.length > 5) {
|
|
280
|
+
console.log(` └─ ... and ${sorted.length - 5} more modules`);
|
|
281
|
+
}
|
|
282
|
+
console.log('');
|
|
283
|
+
}
|
|
284
|
+
// Call chains
|
|
285
|
+
if (impact.callChains.length > 0) {
|
|
286
|
+
console.log('Call chains (sample):');
|
|
287
|
+
for (const chain of impact.callChains.slice(0, 3)) {
|
|
288
|
+
console.log(` ${chain.join(' → ')}`);
|
|
289
|
+
}
|
|
290
|
+
console.log('');
|
|
291
|
+
}
|
|
292
|
+
// Risk assessment
|
|
293
|
+
const totalAffected = impact.directCallers.length + impact.transitiveCallers.length;
|
|
294
|
+
const moduleCount = impact.affectedModules.size;
|
|
295
|
+
let risk = 'LOW';
|
|
296
|
+
let color = '\x1b[32m'; // green
|
|
297
|
+
if (totalAffected > 20 || moduleCount > 5) {
|
|
298
|
+
risk = 'HIGH';
|
|
299
|
+
color = '\x1b[31m'; // red
|
|
300
|
+
}
|
|
301
|
+
else if (totalAffected > 5 || moduleCount > 2) {
|
|
302
|
+
risk = 'MEDIUM';
|
|
303
|
+
color = '\x1b[33m'; // yellow
|
|
304
|
+
}
|
|
305
|
+
console.log(`Risk level: ${color}${risk}\x1b[0m`);
|
|
306
|
+
if (risk === 'HIGH') {
|
|
307
|
+
console.log('');
|
|
308
|
+
console.log('Recommendation:');
|
|
309
|
+
console.log(' • Consider adding backward-compatible wrapper');
|
|
310
|
+
console.log(' • Update tests in affected modules');
|
|
311
|
+
console.log(' • Notify team about breaking change');
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init command - Initialize Grafema in a project
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { resolve, join } from 'path';
|
|
6
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
7
|
+
import { exitWithError } from '../utils/errorFormatter.js';
|
|
8
|
+
import { stringify as stringifyYAML } from 'yaml';
|
|
9
|
+
import { DEFAULT_CONFIG } from '@grafema/core';
|
|
10
|
+
/**
|
|
11
|
+
* Generate config.yaml content with commented future features.
|
|
12
|
+
* Only includes implemented features (plugins).
|
|
13
|
+
*/
|
|
14
|
+
function generateConfigYAML() {
|
|
15
|
+
// Start with working default config
|
|
16
|
+
const config = {
|
|
17
|
+
// Plugin list (fully implemented)
|
|
18
|
+
plugins: DEFAULT_CONFIG.plugins,
|
|
19
|
+
};
|
|
20
|
+
// Convert to YAML
|
|
21
|
+
const yaml = stringifyYAML(config, {
|
|
22
|
+
lineWidth: 0, // Don't wrap long lines
|
|
23
|
+
});
|
|
24
|
+
// Add header comment
|
|
25
|
+
return `# Grafema Configuration
|
|
26
|
+
# Documentation: https://github.com/grafema/grafema#configuration
|
|
27
|
+
|
|
28
|
+
${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
|
+
#
|
|
33
|
+
# include:
|
|
34
|
+
# - "src/**/*.{ts,js,tsx,jsx}"
|
|
35
|
+
# exclude:
|
|
36
|
+
# - "**/*.test.ts"
|
|
37
|
+
# - "node_modules/**"
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
40
|
+
export const initCommand = new Command('init')
|
|
41
|
+
.description('Initialize Grafema in current project')
|
|
42
|
+
.argument('[path]', 'Project path', '.')
|
|
43
|
+
.option('-f, --force', 'Overwrite existing config')
|
|
44
|
+
.action(async (path, options) => {
|
|
45
|
+
const projectPath = resolve(path);
|
|
46
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
47
|
+
const configPath = join(grafemaDir, 'config.yaml');
|
|
48
|
+
const packageJsonPath = join(projectPath, 'package.json');
|
|
49
|
+
const tsconfigPath = join(projectPath, 'tsconfig.json');
|
|
50
|
+
// Check package.json
|
|
51
|
+
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
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
console.log('✓ Found package.json');
|
|
58
|
+
// Detect TypeScript
|
|
59
|
+
const isTypeScript = existsSync(tsconfigPath);
|
|
60
|
+
if (isTypeScript) {
|
|
61
|
+
console.log('✓ Detected TypeScript project');
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
console.log('✓ Detected JavaScript project');
|
|
65
|
+
}
|
|
66
|
+
// Check existing config
|
|
67
|
+
if (existsSync(configPath) && !options.force) {
|
|
68
|
+
console.log('');
|
|
69
|
+
console.log('✓ Grafema already initialized');
|
|
70
|
+
console.log(' → Use --force to overwrite config');
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log('Next: Run "grafema analyze" to build the code graph');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Create .grafema directory
|
|
76
|
+
if (!existsSync(grafemaDir)) {
|
|
77
|
+
mkdirSync(grafemaDir, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
// Write config
|
|
80
|
+
const configContent = generateConfigYAML();
|
|
81
|
+
writeFileSync(configPath, configContent);
|
|
82
|
+
console.log('✓ Created .grafema/config.yaml');
|
|
83
|
+
// Add to .gitignore if exists
|
|
84
|
+
const gitignorePath = join(projectPath, '.gitignore');
|
|
85
|
+
if (existsSync(gitignorePath)) {
|
|
86
|
+
const gitignore = readFileSync(gitignorePath, 'utf-8');
|
|
87
|
+
if (!gitignore.includes('.grafema/graph.rfdb')) {
|
|
88
|
+
writeFileSync(gitignorePath, gitignore + '\n# Grafema\n.grafema/graph.rfdb\n.grafema/rfdb.sock\n');
|
|
89
|
+
console.log('✓ Updated .gitignore');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log('Next: Run "grafema analyze" to build the code graph');
|
|
94
|
+
});
|
|
@@ -0,0 +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"}
|