@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,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Preview Utility
|
|
3
|
+
*
|
|
4
|
+
* Reads source files and extracts code snippets around a given line number
|
|
5
|
+
* for displaying in the explorer UI.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, existsSync } from 'fs';
|
|
8
|
+
/**
|
|
9
|
+
* Get a code preview snippet from a source file.
|
|
10
|
+
* Returns lines around the specified line number with context.
|
|
11
|
+
*/
|
|
12
|
+
export function getCodePreview(options) {
|
|
13
|
+
const { file, line, contextBefore = 2, contextAfter = 12 } = options;
|
|
14
|
+
if (!existsSync(file)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const content = readFileSync(file, 'utf-8');
|
|
19
|
+
const allLines = content.split('\n');
|
|
20
|
+
// Calculate range (1-indexed)
|
|
21
|
+
const startLine = Math.max(1, line - contextBefore);
|
|
22
|
+
const endLine = Math.min(allLines.length, line + contextAfter);
|
|
23
|
+
// Extract lines (convert to 0-indexed for array access)
|
|
24
|
+
const lines = allLines.slice(startLine - 1, endLine);
|
|
25
|
+
return {
|
|
26
|
+
lines,
|
|
27
|
+
startLine,
|
|
28
|
+
endLine
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Format code preview lines with line numbers for display.
|
|
37
|
+
* Returns an array of formatted strings like " 42 | code here"
|
|
38
|
+
*/
|
|
39
|
+
export function formatCodePreview(preview, highlightLine) {
|
|
40
|
+
const { lines, startLine } = preview;
|
|
41
|
+
const maxLineNum = startLine + lines.length - 1;
|
|
42
|
+
const lineNumWidth = String(maxLineNum).length;
|
|
43
|
+
return lines.map((line, index) => {
|
|
44
|
+
const lineNum = startLine + index;
|
|
45
|
+
const paddedNum = String(lineNum).padStart(lineNumWidth, ' ');
|
|
46
|
+
const prefix = highlightLine === lineNum ? '>' : ' ';
|
|
47
|
+
// Truncate very long lines
|
|
48
|
+
const displayLine = line.length > 80 ? line.slice(0, 77) + '...' : line;
|
|
49
|
+
return `${prefix}${paddedNum} | ${displayLine}`;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardized error formatting for CLI commands - REG-157
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent error messages across all CLI commands.
|
|
5
|
+
* Format:
|
|
6
|
+
* ✗ Main error message (1 line, concise)
|
|
7
|
+
*
|
|
8
|
+
* → Next action 1
|
|
9
|
+
* → Next action 2
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Print a standardized error message and exit.
|
|
13
|
+
*
|
|
14
|
+
* @param title - Main error message (should be under 80 chars)
|
|
15
|
+
* @param nextSteps - Optional array of actionable suggestions
|
|
16
|
+
* @returns never - always calls process.exit(1)
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* exitWithError('No graph database found', [
|
|
20
|
+
* 'Run: grafema analyze'
|
|
21
|
+
* ]);
|
|
22
|
+
*/
|
|
23
|
+
export declare function exitWithError(title: string, nextSteps?: string[]): never;
|
|
24
|
+
//# sourceMappingURL=errorFormatter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errorFormatter.d.ts","sourceRoot":"","sources":["../../src/utils/errorFormatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,KAAK,CAWxE"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardized error formatting for CLI commands - REG-157
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent error messages across all CLI commands.
|
|
5
|
+
* Format:
|
|
6
|
+
* ✗ Main error message (1 line, concise)
|
|
7
|
+
*
|
|
8
|
+
* → Next action 1
|
|
9
|
+
* → Next action 2
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Print a standardized error message and exit.
|
|
13
|
+
*
|
|
14
|
+
* @param title - Main error message (should be under 80 chars)
|
|
15
|
+
* @param nextSteps - Optional array of actionable suggestions
|
|
16
|
+
* @returns never - always calls process.exit(1)
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* exitWithError('No graph database found', [
|
|
20
|
+
* 'Run: grafema analyze'
|
|
21
|
+
* ]);
|
|
22
|
+
*/
|
|
23
|
+
export function exitWithError(title, nextSteps) {
|
|
24
|
+
console.error(`✗ ${title}`);
|
|
25
|
+
if (nextSteps && nextSteps.length > 0) {
|
|
26
|
+
console.error('');
|
|
27
|
+
for (const step of nextSteps) {
|
|
28
|
+
console.error(`→ ${step}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node display formatting utilities - REG-125
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent formatting for node display across all CLI commands.
|
|
5
|
+
* Semantic IDs are shown as the PRIMARY identifier, with location as secondary.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Format options for node display
|
|
9
|
+
*/
|
|
10
|
+
export interface FormatNodeOptions {
|
|
11
|
+
/** Project path for relative file paths */
|
|
12
|
+
projectPath: string;
|
|
13
|
+
/** Include location line (default: true) */
|
|
14
|
+
showLocation?: boolean;
|
|
15
|
+
/** Prefix for each line (default: '') */
|
|
16
|
+
indent?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Node information required for display
|
|
20
|
+
*/
|
|
21
|
+
export interface DisplayableNode {
|
|
22
|
+
/** Semantic ID (e.g., "auth/service.ts->AuthService->FUNCTION->authenticate") */
|
|
23
|
+
id: string;
|
|
24
|
+
/** Node type (e.g., "FUNCTION", "CLASS") */
|
|
25
|
+
type: string;
|
|
26
|
+
/** Human-readable name */
|
|
27
|
+
name: string;
|
|
28
|
+
/** Absolute file path */
|
|
29
|
+
file: string;
|
|
30
|
+
/** Line number (optional) */
|
|
31
|
+
line?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Format a node for primary display (multi-line)
|
|
35
|
+
*
|
|
36
|
+
* Output format:
|
|
37
|
+
* [FUNCTION] authenticate
|
|
38
|
+
* ID: auth/service.ts->AuthService->FUNCTION->authenticate
|
|
39
|
+
* Location: auth/service.ts:42
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatNodeDisplay(node: DisplayableNode, options: FormatNodeOptions): string;
|
|
42
|
+
/**
|
|
43
|
+
* Format a node for inline display in lists (semantic ID only)
|
|
44
|
+
*
|
|
45
|
+
* Output format:
|
|
46
|
+
* auth/service.ts->AuthService->FUNCTION->authenticate
|
|
47
|
+
*/
|
|
48
|
+
export declare function formatNodeInline(node: DisplayableNode): string;
|
|
49
|
+
/**
|
|
50
|
+
* Format file location relative to project
|
|
51
|
+
*/
|
|
52
|
+
export declare function formatLocation(file: string | undefined, line: number | undefined, projectPath: string): string;
|
|
53
|
+
//# sourceMappingURL=formatNode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatNode.d.ts","sourceRoot":"","sources":["../../src/utils/formatNode.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,yCAAyC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,EAAE,EAAE,MAAM,CAAC;IACX,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAmB3F;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAE9D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,WAAW,EAAE,MAAM,GAClB,MAAM,CAIR"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node display formatting utilities - REG-125
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent formatting for node display across all CLI commands.
|
|
5
|
+
* Semantic IDs are shown as the PRIMARY identifier, with location as secondary.
|
|
6
|
+
*/
|
|
7
|
+
import { relative } from 'path';
|
|
8
|
+
/**
|
|
9
|
+
* Format a node for primary display (multi-line)
|
|
10
|
+
*
|
|
11
|
+
* Output format:
|
|
12
|
+
* [FUNCTION] authenticate
|
|
13
|
+
* ID: auth/service.ts->AuthService->FUNCTION->authenticate
|
|
14
|
+
* Location: auth/service.ts:42
|
|
15
|
+
*/
|
|
16
|
+
export function formatNodeDisplay(node, options) {
|
|
17
|
+
const { projectPath, showLocation = true, indent = '' } = options;
|
|
18
|
+
const lines = [];
|
|
19
|
+
// Line 1: [TYPE] name
|
|
20
|
+
lines.push(`${indent}[${node.type}] ${node.name}`);
|
|
21
|
+
// Line 2: ID (semantic ID)
|
|
22
|
+
lines.push(`${indent} ID: ${node.id}`);
|
|
23
|
+
// Line 3: Location (optional)
|
|
24
|
+
if (showLocation) {
|
|
25
|
+
const loc = formatLocation(node.file, node.line, projectPath);
|
|
26
|
+
if (loc) {
|
|
27
|
+
lines.push(`${indent} Location: ${loc}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return lines.join('\n');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Format a node for inline display in lists (semantic ID only)
|
|
34
|
+
*
|
|
35
|
+
* Output format:
|
|
36
|
+
* auth/service.ts->AuthService->FUNCTION->authenticate
|
|
37
|
+
*/
|
|
38
|
+
export function formatNodeInline(node) {
|
|
39
|
+
return node.id;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Format file location relative to project
|
|
43
|
+
*/
|
|
44
|
+
export function formatLocation(file, line, projectPath) {
|
|
45
|
+
if (!file)
|
|
46
|
+
return '';
|
|
47
|
+
const relPath = relative(projectPath, file);
|
|
48
|
+
return line ? `${relPath}:${line}` : relPath;
|
|
49
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@grafema/cli",
|
|
3
|
+
"version": "0.1.1-alpha",
|
|
4
|
+
"description": "CLI for Grafema code analysis toolkit",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/cli.js",
|
|
7
|
+
"types": "./dist/cli.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"grafema": "./dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/cli.d.ts",
|
|
14
|
+
"import": "./dist/cli.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"grafema",
|
|
23
|
+
"cli",
|
|
24
|
+
"code-analysis",
|
|
25
|
+
"static-analysis"
|
|
26
|
+
],
|
|
27
|
+
"license": "Apache-2.0",
|
|
28
|
+
"author": "Vadim Reshetnikov",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/grafema/grafema.git",
|
|
32
|
+
"directory": "packages/cli"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"commander": "^13.0.0",
|
|
36
|
+
"ink": "^6.6.0",
|
|
37
|
+
"ink-text-input": "^6.0.0",
|
|
38
|
+
"react": "^19.2.3",
|
|
39
|
+
"yaml": "^2.8.2",
|
|
40
|
+
"@grafema/core": "0.1.1-alpha",
|
|
41
|
+
"@grafema/types": "0.1.1-alpha"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^25.0.8",
|
|
45
|
+
"@types/react": "^19.2.9",
|
|
46
|
+
"typescript": "^5.9.3"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc",
|
|
50
|
+
"clean": "rm -rf dist",
|
|
51
|
+
"start": "node dist/cli.js",
|
|
52
|
+
"test": "node --import tsx --test test/*.test.ts"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @grafema/cli - CLI for Grafema code analysis toolkit
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import { initCommand } from './commands/init.js';
|
|
8
|
+
import { analyzeCommand } from './commands/analyze.js';
|
|
9
|
+
import { overviewCommand } from './commands/overview.js';
|
|
10
|
+
import { queryCommand } from './commands/query.js';
|
|
11
|
+
import { getCommand } from './commands/get.js';
|
|
12
|
+
import { traceCommand } from './commands/trace.js';
|
|
13
|
+
import { impactCommand } from './commands/impact.js';
|
|
14
|
+
import { exploreCommand } from './commands/explore.js';
|
|
15
|
+
import { statsCommand } from './commands/stats.js';
|
|
16
|
+
import { checkCommand } from './commands/check.js';
|
|
17
|
+
import { serverCommand } from './commands/server.js';
|
|
18
|
+
import { coverageCommand } from './commands/coverage.js';
|
|
19
|
+
|
|
20
|
+
const program = new Command();
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.name('grafema')
|
|
24
|
+
.description('Grafema code analysis CLI')
|
|
25
|
+
.version('0.1.0-alpha.1');
|
|
26
|
+
|
|
27
|
+
// Commands in logical order
|
|
28
|
+
program.addCommand(initCommand);
|
|
29
|
+
program.addCommand(analyzeCommand);
|
|
30
|
+
program.addCommand(overviewCommand);
|
|
31
|
+
program.addCommand(queryCommand);
|
|
32
|
+
program.addCommand(getCommand);
|
|
33
|
+
program.addCommand(traceCommand);
|
|
34
|
+
program.addCommand(impactCommand);
|
|
35
|
+
program.addCommand(exploreCommand);
|
|
36
|
+
program.addCommand(statsCommand); // Keep for backwards compat
|
|
37
|
+
program.addCommand(coverageCommand);
|
|
38
|
+
program.addCommand(checkCommand);
|
|
39
|
+
program.addCommand(serverCommand);
|
|
40
|
+
|
|
41
|
+
program.parse();
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze command - Run project analysis via Orchestrator
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { resolve, join } from 'path';
|
|
7
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
8
|
+
import {
|
|
9
|
+
Orchestrator,
|
|
10
|
+
RFDBServerBackend,
|
|
11
|
+
Plugin,
|
|
12
|
+
DiagnosticReporter,
|
|
13
|
+
DiagnosticWriter,
|
|
14
|
+
createLogger,
|
|
15
|
+
loadConfig,
|
|
16
|
+
type GrafemaConfig,
|
|
17
|
+
// Discovery
|
|
18
|
+
SimpleProjectDiscovery,
|
|
19
|
+
MonorepoServiceDiscovery,
|
|
20
|
+
WorkspaceDiscovery,
|
|
21
|
+
// Indexing
|
|
22
|
+
JSModuleIndexer,
|
|
23
|
+
RustModuleIndexer,
|
|
24
|
+
// Analysis
|
|
25
|
+
JSASTAnalyzer,
|
|
26
|
+
ExpressRouteAnalyzer,
|
|
27
|
+
SocketIOAnalyzer,
|
|
28
|
+
DatabaseAnalyzer,
|
|
29
|
+
FetchAnalyzer,
|
|
30
|
+
ServiceLayerAnalyzer,
|
|
31
|
+
ReactAnalyzer,
|
|
32
|
+
RustAnalyzer,
|
|
33
|
+
// Enrichment
|
|
34
|
+
MethodCallResolver,
|
|
35
|
+
AliasTracker,
|
|
36
|
+
ValueDomainAnalyzer,
|
|
37
|
+
MountPointResolver,
|
|
38
|
+
PrefixEvaluator,
|
|
39
|
+
InstanceOfResolver,
|
|
40
|
+
ImportExportLinker,
|
|
41
|
+
HTTPConnectionEnricher,
|
|
42
|
+
RustFFIEnricher,
|
|
43
|
+
// Validation
|
|
44
|
+
CallResolverValidator,
|
|
45
|
+
EvalBanValidator,
|
|
46
|
+
SQLInjectionValidator,
|
|
47
|
+
ShadowingDetector,
|
|
48
|
+
GraphConnectivityValidator,
|
|
49
|
+
DataFlowValidator,
|
|
50
|
+
TypeScriptDeadCodeValidator,
|
|
51
|
+
} from '@grafema/core';
|
|
52
|
+
import type { LogLevel } from '@grafema/types';
|
|
53
|
+
|
|
54
|
+
const BUILTIN_PLUGINS: Record<string, () => Plugin> = {
|
|
55
|
+
// Discovery
|
|
56
|
+
SimpleProjectDiscovery: () => new SimpleProjectDiscovery() as Plugin,
|
|
57
|
+
MonorepoServiceDiscovery: () => new MonorepoServiceDiscovery() as Plugin,
|
|
58
|
+
WorkspaceDiscovery: () => new WorkspaceDiscovery() as Plugin,
|
|
59
|
+
// Indexing
|
|
60
|
+
JSModuleIndexer: () => new JSModuleIndexer() as Plugin,
|
|
61
|
+
RustModuleIndexer: () => new RustModuleIndexer() as Plugin,
|
|
62
|
+
// Analysis
|
|
63
|
+
JSASTAnalyzer: () => new JSASTAnalyzer() as Plugin,
|
|
64
|
+
ExpressRouteAnalyzer: () => new ExpressRouteAnalyzer() as Plugin,
|
|
65
|
+
SocketIOAnalyzer: () => new SocketIOAnalyzer() as Plugin,
|
|
66
|
+
DatabaseAnalyzer: () => new DatabaseAnalyzer() as Plugin,
|
|
67
|
+
FetchAnalyzer: () => new FetchAnalyzer() as Plugin,
|
|
68
|
+
ServiceLayerAnalyzer: () => new ServiceLayerAnalyzer() as Plugin,
|
|
69
|
+
ReactAnalyzer: () => new ReactAnalyzer() as Plugin,
|
|
70
|
+
RustAnalyzer: () => new RustAnalyzer() as Plugin,
|
|
71
|
+
// Enrichment
|
|
72
|
+
MethodCallResolver: () => new MethodCallResolver() as Plugin,
|
|
73
|
+
AliasTracker: () => new AliasTracker() as Plugin,
|
|
74
|
+
ValueDomainAnalyzer: () => new ValueDomainAnalyzer() as Plugin,
|
|
75
|
+
MountPointResolver: () => new MountPointResolver() as Plugin,
|
|
76
|
+
PrefixEvaluator: () => new PrefixEvaluator() as Plugin,
|
|
77
|
+
InstanceOfResolver: () => new InstanceOfResolver() as Plugin,
|
|
78
|
+
ImportExportLinker: () => new ImportExportLinker() as Plugin,
|
|
79
|
+
HTTPConnectionEnricher: () => new HTTPConnectionEnricher() as Plugin,
|
|
80
|
+
RustFFIEnricher: () => new RustFFIEnricher() as Plugin,
|
|
81
|
+
// Validation
|
|
82
|
+
CallResolverValidator: () => new CallResolverValidator() as Plugin,
|
|
83
|
+
EvalBanValidator: () => new EvalBanValidator() as Plugin,
|
|
84
|
+
SQLInjectionValidator: () => new SQLInjectionValidator() as Plugin,
|
|
85
|
+
ShadowingDetector: () => new ShadowingDetector() as Plugin,
|
|
86
|
+
GraphConnectivityValidator: () => new GraphConnectivityValidator() as Plugin,
|
|
87
|
+
DataFlowValidator: () => new DataFlowValidator() as Plugin,
|
|
88
|
+
TypeScriptDeadCodeValidator: () => new TypeScriptDeadCodeValidator() as Plugin,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function createPlugins(config: GrafemaConfig['plugins']): Plugin[] {
|
|
92
|
+
const plugins: Plugin[] = [];
|
|
93
|
+
const phases: (keyof GrafemaConfig['plugins'])[] = ['discovery', 'indexing', 'analysis', 'enrichment', 'validation'];
|
|
94
|
+
|
|
95
|
+
for (const phase of phases) {
|
|
96
|
+
const names = config[phase] || [];
|
|
97
|
+
for (const name of names) {
|
|
98
|
+
const factory = BUILTIN_PLUGINS[name];
|
|
99
|
+
if (factory) {
|
|
100
|
+
plugins.push(factory());
|
|
101
|
+
} else {
|
|
102
|
+
console.warn(`Unknown plugin: ${name}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return plugins;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Determine log level from CLI options.
|
|
112
|
+
* Priority: --log-level > --quiet > --verbose > default ('info')
|
|
113
|
+
*/
|
|
114
|
+
function getLogLevel(options: { quiet?: boolean; verbose?: boolean; logLevel?: string }): LogLevel {
|
|
115
|
+
if (options.logLevel) {
|
|
116
|
+
const validLevels: LogLevel[] = ['silent', 'errors', 'warnings', 'info', 'debug'];
|
|
117
|
+
if (validLevels.includes(options.logLevel as LogLevel)) {
|
|
118
|
+
return options.logLevel as LogLevel;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (options.quiet) return 'silent';
|
|
122
|
+
if (options.verbose) return 'debug';
|
|
123
|
+
return 'info';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const analyzeCommand = new Command('analyze')
|
|
127
|
+
.description('Run project analysis')
|
|
128
|
+
.argument('[path]', 'Project path to analyze', '.')
|
|
129
|
+
.option('-s, --service <name>', 'Analyze only a specific service')
|
|
130
|
+
.option('-e, --entrypoint <path>', 'Override entrypoint (bypasses auto-detection)')
|
|
131
|
+
.option('-c, --clear', 'Clear existing database before analysis')
|
|
132
|
+
.option('-q, --quiet', 'Suppress progress output')
|
|
133
|
+
.option('-v, --verbose', 'Show verbose logging')
|
|
134
|
+
.option('--debug', 'Enable debug mode (writes diagnostics.log)')
|
|
135
|
+
.option('--log-level <level>', 'Set log level (silent, errors, warnings, info, debug)')
|
|
136
|
+
.action(async (path: string, options: { service?: string; entrypoint?: string; clear?: boolean; quiet?: boolean; verbose?: boolean; debug?: boolean; logLevel?: string }) => {
|
|
137
|
+
const projectPath = resolve(path);
|
|
138
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
139
|
+
const dbPath = join(grafemaDir, 'graph.rfdb');
|
|
140
|
+
|
|
141
|
+
if (!existsSync(grafemaDir)) {
|
|
142
|
+
mkdirSync(grafemaDir, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const log = options.quiet ? () => {} : console.log;
|
|
146
|
+
|
|
147
|
+
// Create logger based on CLI flags
|
|
148
|
+
const logLevel = getLogLevel(options);
|
|
149
|
+
const logger = createLogger(logLevel);
|
|
150
|
+
|
|
151
|
+
log(`Analyzing project: ${projectPath}`);
|
|
152
|
+
|
|
153
|
+
const backend = new RFDBServerBackend({ dbPath });
|
|
154
|
+
await backend.connect();
|
|
155
|
+
|
|
156
|
+
if (options.clear) {
|
|
157
|
+
log('Clearing existing database...');
|
|
158
|
+
await backend.clear();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const config = loadConfig(projectPath, logger);
|
|
162
|
+
|
|
163
|
+
// Extract services from config (REG-174)
|
|
164
|
+
if (config.services.length > 0) {
|
|
165
|
+
log(`Loaded ${config.services.length} service(s) from config`);
|
|
166
|
+
for (const svc of config.services) {
|
|
167
|
+
const entry = svc.entryPoint ? ` (entry: ${svc.entryPoint})` : '';
|
|
168
|
+
log(` - ${svc.name}: ${svc.path}${entry}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const plugins = createPlugins(config.plugins);
|
|
173
|
+
|
|
174
|
+
log(`Loaded ${plugins.length} plugins`);
|
|
175
|
+
|
|
176
|
+
const startTime = Date.now();
|
|
177
|
+
|
|
178
|
+
const orchestrator = new Orchestrator({
|
|
179
|
+
graph: backend as unknown as import('@grafema/types').GraphBackend,
|
|
180
|
+
plugins,
|
|
181
|
+
serviceFilter: options.service || null,
|
|
182
|
+
entrypoint: options.entrypoint,
|
|
183
|
+
forceAnalysis: options.clear || false,
|
|
184
|
+
logger,
|
|
185
|
+
services: config.services.length > 0 ? config.services : undefined, // Pass config services (REG-174)
|
|
186
|
+
onProgress: (progress) => {
|
|
187
|
+
if (options.verbose) {
|
|
188
|
+
log(`[${progress.phase}] ${progress.message}`);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
let exitCode = 0;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
await orchestrator.run(projectPath);
|
|
197
|
+
await backend.flush();
|
|
198
|
+
|
|
199
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
200
|
+
const stats = await backend.getStats();
|
|
201
|
+
|
|
202
|
+
log('');
|
|
203
|
+
log(`Analysis complete in ${elapsed}s`);
|
|
204
|
+
log(` Nodes: ${stats.nodeCount}`);
|
|
205
|
+
log(` Edges: ${stats.edgeCount}`);
|
|
206
|
+
|
|
207
|
+
// Get diagnostics and report summary
|
|
208
|
+
const diagnostics = orchestrator.getDiagnostics();
|
|
209
|
+
const reporter = new DiagnosticReporter(diagnostics);
|
|
210
|
+
|
|
211
|
+
// Print summary if there are any issues
|
|
212
|
+
if (diagnostics.count() > 0) {
|
|
213
|
+
log('');
|
|
214
|
+
log(reporter.summary());
|
|
215
|
+
|
|
216
|
+
// In verbose mode, print full report
|
|
217
|
+
if (options.verbose) {
|
|
218
|
+
log('');
|
|
219
|
+
log(reporter.report({ format: 'text', includeSummary: false }));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Write diagnostics.log in debug mode
|
|
224
|
+
if (options.debug) {
|
|
225
|
+
const writer = new DiagnosticWriter();
|
|
226
|
+
await writer.write(diagnostics, grafemaDir);
|
|
227
|
+
log(`Diagnostics written to ${writer.getLogPath(grafemaDir)}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Determine exit code based on severity
|
|
231
|
+
if (diagnostics.hasFatal()) {
|
|
232
|
+
exitCode = 1;
|
|
233
|
+
} else if (diagnostics.hasErrors()) {
|
|
234
|
+
exitCode = 2; // Completed with errors
|
|
235
|
+
} else {
|
|
236
|
+
exitCode = 0; // Success (maybe warnings)
|
|
237
|
+
}
|
|
238
|
+
} catch (e) {
|
|
239
|
+
// Orchestrator threw (fatal error stopped analysis)
|
|
240
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
241
|
+
const diagnostics = orchestrator.getDiagnostics();
|
|
242
|
+
const reporter = new DiagnosticReporter(diagnostics);
|
|
243
|
+
|
|
244
|
+
console.error('');
|
|
245
|
+
console.error(`✗ Analysis failed: ${error.message}`);
|
|
246
|
+
console.error('');
|
|
247
|
+
console.error('→ Run with --debug for detailed diagnostics');
|
|
248
|
+
|
|
249
|
+
if (diagnostics.count() > 0) {
|
|
250
|
+
console.error('');
|
|
251
|
+
console.error(reporter.report({ format: 'text', includeSummary: true }));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Write diagnostics.log in debug mode even on failure
|
|
255
|
+
if (options.debug) {
|
|
256
|
+
const writer = new DiagnosticWriter();
|
|
257
|
+
await writer.write(diagnostics, grafemaDir);
|
|
258
|
+
console.error(`Diagnostics written to ${writer.getLogPath(grafemaDir)}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
exitCode = 1;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await backend.close();
|
|
265
|
+
|
|
266
|
+
// Exit with appropriate code
|
|
267
|
+
// 0 = success, 1 = fatal, 2 = errors
|
|
268
|
+
if (exitCode !== 0) {
|
|
269
|
+
process.exit(exitCode);
|
|
270
|
+
}
|
|
271
|
+
});
|