@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.
Files changed (66) hide show
  1. package/LICENSE +190 -0
  2. package/dist/cli.d.ts +6 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +36 -0
  5. package/dist/commands/analyze.d.ts +6 -0
  6. package/dist/commands/analyze.d.ts.map +1 -0
  7. package/dist/commands/analyze.js +209 -0
  8. package/dist/commands/check.d.ts +10 -0
  9. package/dist/commands/check.d.ts.map +1 -0
  10. package/dist/commands/check.js +295 -0
  11. package/dist/commands/coverage.d.ts +11 -0
  12. package/dist/commands/coverage.d.ts.map +1 -0
  13. package/dist/commands/coverage.js +96 -0
  14. package/dist/commands/explore.d.ts +6 -0
  15. package/dist/commands/explore.d.ts.map +1 -0
  16. package/dist/commands/explore.js +633 -0
  17. package/dist/commands/get.d.ts +10 -0
  18. package/dist/commands/get.d.ts.map +1 -0
  19. package/dist/commands/get.js +189 -0
  20. package/dist/commands/impact.d.ts +10 -0
  21. package/dist/commands/impact.d.ts.map +1 -0
  22. package/dist/commands/impact.js +313 -0
  23. package/dist/commands/init.d.ts +6 -0
  24. package/dist/commands/init.d.ts.map +1 -0
  25. package/dist/commands/init.js +94 -0
  26. package/dist/commands/overview.d.ts +6 -0
  27. package/dist/commands/overview.d.ts.map +1 -0
  28. package/dist/commands/overview.js +91 -0
  29. package/dist/commands/query.d.ts +13 -0
  30. package/dist/commands/query.d.ts.map +1 -0
  31. package/dist/commands/query.js +340 -0
  32. package/dist/commands/server.d.ts +11 -0
  33. package/dist/commands/server.d.ts.map +1 -0
  34. package/dist/commands/server.js +300 -0
  35. package/dist/commands/stats.d.ts +6 -0
  36. package/dist/commands/stats.d.ts.map +1 -0
  37. package/dist/commands/stats.js +52 -0
  38. package/dist/commands/trace.d.ts +10 -0
  39. package/dist/commands/trace.d.ts.map +1 -0
  40. package/dist/commands/trace.js +270 -0
  41. package/dist/utils/codePreview.d.ts +28 -0
  42. package/dist/utils/codePreview.d.ts.map +1 -0
  43. package/dist/utils/codePreview.js +51 -0
  44. package/dist/utils/errorFormatter.d.ts +24 -0
  45. package/dist/utils/errorFormatter.d.ts.map +1 -0
  46. package/dist/utils/errorFormatter.js +32 -0
  47. package/dist/utils/formatNode.d.ts +53 -0
  48. package/dist/utils/formatNode.d.ts.map +1 -0
  49. package/dist/utils/formatNode.js +49 -0
  50. package/package.json +54 -0
  51. package/src/cli.ts +41 -0
  52. package/src/commands/analyze.ts +271 -0
  53. package/src/commands/check.ts +379 -0
  54. package/src/commands/coverage.ts +108 -0
  55. package/src/commands/explore.tsx +1056 -0
  56. package/src/commands/get.ts +265 -0
  57. package/src/commands/impact.ts +400 -0
  58. package/src/commands/init.ts +112 -0
  59. package/src/commands/overview.ts +108 -0
  60. package/src/commands/query.ts +425 -0
  61. package/src/commands/server.ts +335 -0
  62. package/src/commands/stats.ts +58 -0
  63. package/src/commands/trace.ts +341 -0
  64. package/src/utils/codePreview.ts +77 -0
  65. package/src/utils/errorFormatter.ts +35 -0
  66. 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
+ });