@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.
Files changed (139) hide show
  1. package/README.md +85 -0
  2. package/dist/cli.js +7 -2
  3. package/dist/cli.js.map +1 -0
  4. package/dist/commands/analyze.d.ts +3 -1
  5. package/dist/commands/analyze.d.ts.map +1 -1
  6. package/dist/commands/analyze.js +8 -266
  7. package/dist/commands/analyze.js.map +1 -0
  8. package/dist/commands/analyzeAction.d.ts +28 -0
  9. package/dist/commands/analyzeAction.d.ts.map +1 -0
  10. package/dist/commands/analyzeAction.js +243 -0
  11. package/dist/commands/analyzeAction.js.map +1 -0
  12. package/dist/commands/check.d.ts +2 -6
  13. package/dist/commands/check.d.ts.map +1 -1
  14. package/dist/commands/check.js +34 -48
  15. package/dist/commands/check.js.map +1 -0
  16. package/dist/commands/context.d.ts +16 -0
  17. package/dist/commands/context.d.ts.map +1 -0
  18. package/dist/commands/context.js +238 -0
  19. package/dist/commands/context.js.map +1 -0
  20. package/dist/commands/coverage.js +1 -0
  21. package/dist/commands/coverage.js.map +1 -0
  22. package/dist/commands/doctor/checks.d.ts.map +1 -1
  23. package/dist/commands/doctor/checks.js +10 -6
  24. package/dist/commands/doctor/checks.js.map +1 -0
  25. package/dist/commands/doctor/output.js +1 -0
  26. package/dist/commands/doctor/output.js.map +1 -0
  27. package/dist/commands/doctor/types.js +1 -0
  28. package/dist/commands/doctor/types.js.map +1 -0
  29. package/dist/commands/doctor.js +1 -0
  30. package/dist/commands/doctor.js.map +1 -0
  31. package/dist/commands/explain.d.ts.map +1 -1
  32. package/dist/commands/explain.js +5 -3
  33. package/dist/commands/explain.js.map +1 -0
  34. package/dist/commands/explore.d.ts.map +1 -1
  35. package/dist/commands/explore.js +9 -4
  36. package/dist/commands/explore.js.map +1 -0
  37. package/dist/commands/file.d.ts +15 -0
  38. package/dist/commands/file.d.ts.map +1 -0
  39. package/dist/commands/file.js +144 -0
  40. package/dist/commands/file.js.map +1 -0
  41. package/dist/commands/get.d.ts.map +1 -1
  42. package/dist/commands/get.js +7 -0
  43. package/dist/commands/get.js.map +1 -0
  44. package/dist/commands/impact.d.ts.map +1 -1
  45. package/dist/commands/impact.js +3 -3
  46. package/dist/commands/impact.js.map +1 -0
  47. package/dist/commands/init.d.ts.map +1 -1
  48. package/dist/commands/init.js +20 -2
  49. package/dist/commands/init.js.map +1 -0
  50. package/dist/commands/ls.d.ts.map +1 -1
  51. package/dist/commands/ls.js +10 -2
  52. package/dist/commands/ls.js.map +1 -0
  53. package/dist/commands/overview.d.ts.map +1 -1
  54. package/dist/commands/overview.js +1 -0
  55. package/dist/commands/overview.js.map +1 -0
  56. package/dist/commands/query.d.ts +8 -0
  57. package/dist/commands/query.d.ts.map +1 -1
  58. package/dist/commands/query.js +217 -43
  59. package/dist/commands/query.js.map +1 -0
  60. package/dist/commands/schema.d.ts.map +1 -1
  61. package/dist/commands/schema.js +4 -2
  62. package/dist/commands/schema.js.map +1 -0
  63. package/dist/commands/server.d.ts +2 -1
  64. package/dist/commands/server.d.ts.map +1 -1
  65. package/dist/commands/server.js +76 -14
  66. package/dist/commands/server.js.map +1 -0
  67. package/dist/commands/setup-skill.d.ts +17 -0
  68. package/dist/commands/setup-skill.d.ts.map +1 -0
  69. package/dist/commands/setup-skill.js +131 -0
  70. package/dist/commands/setup-skill.js.map +1 -0
  71. package/dist/commands/stats.js +1 -0
  72. package/dist/commands/stats.js.map +1 -0
  73. package/dist/commands/trace.d.ts.map +1 -1
  74. package/dist/commands/trace.js +21 -10
  75. package/dist/commands/trace.js.map +1 -0
  76. package/dist/commands/types.js +1 -0
  77. package/dist/commands/types.js.map +1 -0
  78. package/dist/plugins/builtinPlugins.d.ts +10 -0
  79. package/dist/plugins/builtinPlugins.d.ts.map +1 -0
  80. package/dist/plugins/builtinPlugins.js +68 -0
  81. package/dist/plugins/builtinPlugins.js.map +1 -0
  82. package/dist/plugins/pluginLoader.d.ts +16 -0
  83. package/dist/plugins/pluginLoader.d.ts.map +1 -0
  84. package/dist/plugins/pluginLoader.js +101 -0
  85. package/dist/plugins/pluginLoader.js.map +1 -0
  86. package/dist/plugins/pluginResolver.js +38 -0
  87. package/dist/utils/codePreview.d.ts +1 -0
  88. package/dist/utils/codePreview.d.ts.map +1 -1
  89. package/dist/utils/codePreview.js +6 -3
  90. package/dist/utils/codePreview.js.map +1 -0
  91. package/dist/utils/errorFormatter.js +1 -0
  92. package/dist/utils/errorFormatter.js.map +1 -0
  93. package/dist/utils/formatNode.d.ts +1 -1
  94. package/dist/utils/formatNode.d.ts.map +1 -1
  95. package/dist/utils/formatNode.js +3 -2
  96. package/dist/utils/formatNode.js.map +1 -0
  97. package/dist/utils/pathUtils.d.ts +2 -0
  98. package/dist/utils/pathUtils.d.ts.map +1 -0
  99. package/dist/utils/pathUtils.js +9 -0
  100. package/dist/utils/pathUtils.js.map +1 -0
  101. package/dist/utils/progressRenderer.d.ts +119 -0
  102. package/dist/utils/progressRenderer.d.ts.map +1 -0
  103. package/dist/utils/progressRenderer.js +245 -0
  104. package/dist/utils/progressRenderer.js.map +1 -0
  105. package/dist/utils/spinner.d.ts +39 -0
  106. package/dist/utils/spinner.d.ts.map +1 -0
  107. package/dist/utils/spinner.js +84 -0
  108. package/dist/utils/spinner.js.map +1 -0
  109. package/package.json +8 -9
  110. package/skills/grafema-codebase-analysis/SKILL.md +295 -0
  111. package/skills/grafema-codebase-analysis/references/node-edge-types.md +123 -0
  112. package/skills/grafema-codebase-analysis/references/query-patterns.md +205 -0
  113. package/src/cli.ts +8 -2
  114. package/src/commands/analyze.ts +7 -342
  115. package/src/commands/analyzeAction.ts +284 -0
  116. package/src/commands/check.ts +38 -70
  117. package/src/commands/context.ts +309 -0
  118. package/src/commands/doctor/checks.ts +9 -6
  119. package/src/commands/explain.ts +4 -3
  120. package/src/commands/explore.tsx +15 -9
  121. package/src/commands/file.ts +179 -0
  122. package/src/commands/get.ts +8 -0
  123. package/src/commands/impact.ts +3 -4
  124. package/src/commands/init.ts +19 -3
  125. package/src/commands/ls.ts +11 -2
  126. package/src/commands/overview.ts +0 -4
  127. package/src/commands/query.ts +235 -44
  128. package/src/commands/schema.ts +3 -2
  129. package/src/commands/server.ts +85 -15
  130. package/src/commands/setup-skill.ts +162 -0
  131. package/src/commands/trace.ts +18 -9
  132. package/src/plugins/builtinPlugins.ts +108 -0
  133. package/src/plugins/pluginLoader.ts +123 -0
  134. package/src/plugins/pluginResolver.js +38 -0
  135. package/src/utils/codePreview.ts +7 -3
  136. package/src/utils/formatNode.ts +3 -3
  137. package/src/utils/pathUtils.ts +9 -0
  138. package/src/utils/progressRenderer.ts +288 -0
  139. package/src/utils/spinner.ts +94 -0
@@ -0,0 +1,205 @@
1
+ # Grafema Datalog Query Patterns
2
+
3
+ All queries use `query_graph` tool. Every query must define a `violation/1` predicate —
4
+ matching nodes are returned as results.
5
+
6
+ ## Syntax Quick Reference
7
+
8
+ ```
9
+ violation(X) :- <body>. # Rule: X is a result if body is true
10
+ node(X, "TYPE") # Match node by type
11
+ edge(X, Y, "TYPE") # Match edge: X -> Y of given type
12
+ attr(X, "name", "value") # Match node attribute
13
+ \+ <condition> # Negation: condition is NOT true
14
+ ```
15
+
16
+ ## Basic Patterns
17
+
18
+ ### Find nodes by type
19
+
20
+ ```datalog
21
+ violation(X) :- node(X, "FUNCTION").
22
+ ```
23
+
24
+ ### Find nodes by type and name
25
+
26
+ ```datalog
27
+ violation(X) :- node(X, "FUNCTION"), attr(X, "name", "processPayment").
28
+ ```
29
+
30
+ ### Find nodes by type and file
31
+
32
+ ```datalog
33
+ violation(X) :- node(X, "FUNCTION"), attr(X, "file", "src/api.ts").
34
+ ```
35
+
36
+ ### Find nodes matching multiple criteria
37
+
38
+ ```datalog
39
+ violation(X) :- node(X, "CALL"), attr(X, "name", "eval"), attr(X, "file", "src/handler.ts").
40
+ ```
41
+
42
+ ## Edge Traversal
43
+
44
+ ### One-hop: Find all calls from a function
45
+
46
+ ```datalog
47
+ violation(Call) :-
48
+ node(F, "FUNCTION"), attr(F, "name", "main"),
49
+ edge(F, S, "HAS_SCOPE"), edge(S, Call, "CONTAINS"),
50
+ node(Call, "CALL").
51
+ ```
52
+
53
+ ### One-hop: Find all callers of a function
54
+
55
+ ```datalog
56
+ violation(Caller) :-
57
+ node(Target, "FUNCTION"), attr(Target, "name", "validate"),
58
+ edge(Call, Target, "CALLS"),
59
+ edge(Scope, Call, "CONTAINS"),
60
+ edge(Caller, Scope, "HAS_SCOPE"),
61
+ node(Caller, "FUNCTION").
62
+ ```
63
+
64
+ ### Find module dependencies
65
+
66
+ ```datalog
67
+ violation(Dep) :-
68
+ node(M, "MODULE"), attr(M, "name", "api"),
69
+ edge(M, Dep, "DEPENDS_ON"), node(Dep, "MODULE").
70
+ ```
71
+
72
+ ### Find what a variable is assigned from
73
+
74
+ ```datalog
75
+ violation(Source) :-
76
+ node(V, "VARIABLE"), attr(V, "name", "config"),
77
+ edge(V, Source, "ASSIGNED_FROM").
78
+ ```
79
+
80
+ ## Negation Patterns
81
+
82
+ ### Functions with no callers (potential dead code)
83
+
84
+ ```datalog
85
+ violation(X) :-
86
+ node(X, "FUNCTION"),
87
+ \+ edge(_, X, "CALLS").
88
+ ```
89
+
90
+ ### Modules with no dependents (unused modules)
91
+
92
+ ```datalog
93
+ violation(X) :-
94
+ node(X, "MODULE"),
95
+ \+ edge(_, X, "DEPENDS_ON").
96
+ ```
97
+
98
+ ### Unresolved calls (external/dynamic targets)
99
+
100
+ ```datalog
101
+ violation(X) :-
102
+ node(X, "CALL"),
103
+ attr(X, "resolved", "false").
104
+ ```
105
+
106
+ ## Invariant Patterns
107
+
108
+ ### No eval() usage
109
+
110
+ ```datalog
111
+ violation(X) :- node(X, "CALL"), attr(X, "name", "eval").
112
+ ```
113
+
114
+ ### No direct database queries outside service layer
115
+
116
+ ```datalog
117
+ violation(X) :-
118
+ node(X, "db:query"),
119
+ attr(X, "file", File),
120
+ \+ attr(X, "file", "src/services/").
121
+ ```
122
+
123
+ Note: File matching is exact. For pattern matching, use the `find_nodes` tool instead.
124
+
125
+ ### All HTTP endpoints must have handlers
126
+
127
+ ```datalog
128
+ violation(X) :-
129
+ node(X, "http:request"),
130
+ \+ edge(_, X, "CALLS").
131
+ ```
132
+
133
+ ## Join Patterns
134
+
135
+ ### Find functions that call both X and Y
136
+
137
+ ```datalog
138
+ violation(F) :-
139
+ node(F, "FUNCTION"),
140
+ edge(F, S, "HAS_SCOPE"),
141
+ edge(S, C1, "CONTAINS"), node(C1, "CALL"), attr(C1, "name", "readFile"),
142
+ edge(S, C2, "CONTAINS"), node(C2, "CALL"), attr(C2, "name", "writeFile").
143
+ ```
144
+
145
+ ### Find classes that extend a specific base class
146
+
147
+ ```datalog
148
+ violation(Child) :-
149
+ node(Base, "CLASS"), attr(Base, "name", "BaseService"),
150
+ edge(Child, Base, "EXTENDS"),
151
+ node(Child, "CLASS").
152
+ ```
153
+
154
+ ## Performance Tips
155
+
156
+ 1. **Put most selective filters first.** `attr(X, "name", "specific")` before `node(X, "FUNCTION")`.
157
+
158
+ 2. **Avoid unconstrained joins.** Every variable should be bounded by at least one specific condition.
159
+
160
+ 3. **Use high-level tools when possible.** `find_calls` is faster than writing a Datalog query for the same pattern — it uses optimized indexes.
161
+
162
+ 4. **Use `explain: true` to debug.** If a query returns nothing, add `explain: true` to see step-by-step execution.
163
+
164
+ 5. **Use `limit` and `offset` for large result sets.** Default limit applies, but you can paginate through results.
165
+
166
+ ## Common Mistakes
167
+
168
+ ### Wrong: Unbound variable
169
+
170
+ ```datalog
171
+ # BAD: Y is never constrained
172
+ violation(X) :- node(X, "FUNCTION"), edge(X, Y, "CALLS").
173
+ ```
174
+
175
+ ```datalog
176
+ # GOOD: Constrain Y
177
+ violation(X) :- node(X, "FUNCTION"), edge(X, Y, "CALLS"), node(Y, "FUNCTION").
178
+ ```
179
+
180
+ ### Wrong: Using wrong edge direction
181
+
182
+ ```datalog
183
+ # BAD: CALLS edge goes Caller -> Callee, not reverse
184
+ violation(X) :- node(X, "FUNCTION"), edge(X, Target, "CALLS").
185
+ ```
186
+
187
+ The CALLS edge typically goes from CALL/CALL_SITE node to target FUNCTION,
188
+ not directly from FUNCTION to FUNCTION. Check [node-edge-types.md](node-edge-types.md)
189
+ for correct edge directions.
190
+
191
+ ### Wrong: Missing scope traversal
192
+
193
+ Functions don't directly CONTAIN calls — they have scopes that contain calls:
194
+
195
+ ```datalog
196
+ # BAD: No direct CONTAINS edge from FUNCTION to CALL
197
+ violation(C) :- node(F, "FUNCTION"), edge(F, C, "CONTAINS"), node(C, "CALL").
198
+ ```
199
+
200
+ ```datalog
201
+ # GOOD: Go through HAS_SCOPE
202
+ violation(C) :-
203
+ node(F, "FUNCTION"), edge(F, S, "HAS_SCOPE"),
204
+ edge(S, C, "CONTAINS"), node(C, "CALL").
205
+ ```
package/src/cli.ts CHANGED
@@ -16,7 +16,8 @@ import { lsCommand } from './commands/ls.js';
16
16
  import { getCommand } from './commands/get.js';
17
17
  import { traceCommand } from './commands/trace.js';
18
18
  import { impactCommand } from './commands/impact.js';
19
- import { exploreCommand } from './commands/explore.js';
19
+ import { contextCommand } from './commands/context.js';
20
+
20
21
  import { statsCommand } from './commands/stats.js';
21
22
  import { checkCommand } from './commands/check.js';
22
23
  import { serverCommand } from './commands/server.js';
@@ -24,6 +25,8 @@ import { coverageCommand } from './commands/coverage.js';
24
25
  import { doctorCommand } from './commands/doctor.js';
25
26
  import { schemaCommand } from './commands/schema.js';
26
27
  import { explainCommand } from './commands/explain.js';
28
+ import { fileCommand } from './commands/file.js';
29
+ import { setupSkillCommand } from './commands/setup-skill.js';
27
30
 
28
31
  // Read version from package.json
29
32
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -41,12 +44,13 @@ program.addCommand(initCommand);
41
44
  program.addCommand(analyzeCommand);
42
45
  program.addCommand(overviewCommand);
43
46
  program.addCommand(queryCommand);
47
+ program.addCommand(contextCommand);
44
48
  program.addCommand(typesCommand);
45
49
  program.addCommand(lsCommand);
46
50
  program.addCommand(getCommand);
47
51
  program.addCommand(traceCommand);
48
52
  program.addCommand(impactCommand);
49
- program.addCommand(exploreCommand);
53
+
50
54
  program.addCommand(statsCommand); // Keep for backwards compat
51
55
  program.addCommand(coverageCommand);
52
56
  program.addCommand(checkCommand);
@@ -54,5 +58,7 @@ program.addCommand(serverCommand);
54
58
  program.addCommand(doctorCommand);
55
59
  program.addCommand(schemaCommand);
56
60
  program.addCommand(explainCommand);
61
+ program.addCommand(fileCommand);
62
+ program.addCommand(setupSkillCommand);
57
63
 
58
64
  program.parse();
@@ -1,182 +1,12 @@
1
1
  /**
2
- * Analyze command - Run project analysis via Orchestrator
2
+ * Analyze command Run project analysis via Orchestrator.
3
+ *
4
+ * Command definition only. Execution logic is in analyzeAction.ts.
3
5
  */
4
6
 
5
7
  import { Command } from 'commander';
6
- import { resolve, join } from 'path';
7
- import { existsSync, mkdirSync, readdirSync } from 'fs';
8
- import { pathToFileURL } from 'url';
9
- import {
10
- Orchestrator,
11
- RFDBServerBackend,
12
- Plugin,
13
- DiagnosticReporter,
14
- DiagnosticWriter,
15
- createLogger,
16
- loadConfig,
17
- type GrafemaConfig,
18
- // Discovery
19
- SimpleProjectDiscovery,
20
- MonorepoServiceDiscovery,
21
- WorkspaceDiscovery,
22
- // Indexing
23
- JSModuleIndexer,
24
- RustModuleIndexer,
25
- // Analysis
26
- JSASTAnalyzer,
27
- ExpressRouteAnalyzer,
28
- ExpressResponseAnalyzer,
29
- SocketIOAnalyzer,
30
- DatabaseAnalyzer,
31
- FetchAnalyzer,
32
- ServiceLayerAnalyzer,
33
- ReactAnalyzer,
34
- RustAnalyzer,
35
- // Enrichment
36
- MethodCallResolver,
37
- ArgumentParameterLinker,
38
- AliasTracker,
39
- ValueDomainAnalyzer,
40
- MountPointResolver,
41
- PrefixEvaluator,
42
- InstanceOfResolver,
43
- ImportExportLinker,
44
- FunctionCallResolver,
45
- HTTPConnectionEnricher,
46
- RustFFIEnricher,
47
- // Validation
48
- CallResolverValidator,
49
- EvalBanValidator,
50
- SQLInjectionValidator,
51
- ShadowingDetector,
52
- GraphConnectivityValidator,
53
- DataFlowValidator,
54
- TypeScriptDeadCodeValidator,
55
- BrokenImportValidator,
56
- } from '@grafema/core';
57
- import type { LogLevel } from '@grafema/types';
8
+ import { analyzeAction } from './analyzeAction.js';
58
9
 
59
- const BUILTIN_PLUGINS: Record<string, () => Plugin> = {
60
- // Discovery
61
- SimpleProjectDiscovery: () => new SimpleProjectDiscovery() as Plugin,
62
- MonorepoServiceDiscovery: () => new MonorepoServiceDiscovery() as Plugin,
63
- WorkspaceDiscovery: () => new WorkspaceDiscovery() as Plugin,
64
- // Indexing
65
- JSModuleIndexer: () => new JSModuleIndexer() as Plugin,
66
- RustModuleIndexer: () => new RustModuleIndexer() as Plugin,
67
- // Analysis
68
- JSASTAnalyzer: () => new JSASTAnalyzer() as Plugin,
69
- ExpressRouteAnalyzer: () => new ExpressRouteAnalyzer() as Plugin,
70
- ExpressResponseAnalyzer: () => new ExpressResponseAnalyzer() as Plugin,
71
- SocketIOAnalyzer: () => new SocketIOAnalyzer() as Plugin,
72
- DatabaseAnalyzer: () => new DatabaseAnalyzer() as Plugin,
73
- FetchAnalyzer: () => new FetchAnalyzer() as Plugin,
74
- ServiceLayerAnalyzer: () => new ServiceLayerAnalyzer() as Plugin,
75
- ReactAnalyzer: () => new ReactAnalyzer() as Plugin,
76
- RustAnalyzer: () => new RustAnalyzer() as Plugin,
77
- // Enrichment
78
- MethodCallResolver: () => new MethodCallResolver() as Plugin,
79
- ArgumentParameterLinker: () => new ArgumentParameterLinker() as Plugin,
80
- AliasTracker: () => new AliasTracker() as Plugin,
81
- ValueDomainAnalyzer: () => new ValueDomainAnalyzer() as Plugin,
82
- MountPointResolver: () => new MountPointResolver() as Plugin,
83
- PrefixEvaluator: () => new PrefixEvaluator() as Plugin,
84
- InstanceOfResolver: () => new InstanceOfResolver() as Plugin,
85
- ImportExportLinker: () => new ImportExportLinker() as Plugin,
86
- FunctionCallResolver: () => new FunctionCallResolver() as Plugin,
87
- HTTPConnectionEnricher: () => new HTTPConnectionEnricher() as Plugin,
88
- RustFFIEnricher: () => new RustFFIEnricher() as Plugin,
89
- // Validation
90
- CallResolverValidator: () => new CallResolverValidator() as Plugin,
91
- EvalBanValidator: () => new EvalBanValidator() as Plugin,
92
- SQLInjectionValidator: () => new SQLInjectionValidator() as Plugin,
93
- ShadowingDetector: () => new ShadowingDetector() as Plugin,
94
- GraphConnectivityValidator: () => new GraphConnectivityValidator() as Plugin,
95
- DataFlowValidator: () => new DataFlowValidator() as Plugin,
96
- TypeScriptDeadCodeValidator: () => new TypeScriptDeadCodeValidator() as Plugin,
97
- BrokenImportValidator: () => new BrokenImportValidator() as Plugin,
98
- };
99
-
100
- /**
101
- * Load custom plugins from .grafema/plugins/ directory
102
- */
103
- async function loadCustomPlugins(
104
- projectPath: string,
105
- log: (msg: string) => void
106
- ): Promise<Record<string, () => Plugin>> {
107
- const pluginsDir = join(projectPath, '.grafema', 'plugins');
108
- if (!existsSync(pluginsDir)) {
109
- return {};
110
- }
111
-
112
- const customPlugins: Record<string, () => Plugin> = {};
113
-
114
- try {
115
- const files = readdirSync(pluginsDir).filter(
116
- (f) => f.endsWith('.js') || f.endsWith('.mjs')
117
- );
118
-
119
- for (const file of files) {
120
- try {
121
- const pluginPath = join(pluginsDir, file);
122
- const pluginUrl = pathToFileURL(pluginPath).href;
123
- const module = await import(pluginUrl);
124
-
125
- const PluginClass = module.default || module[file.replace(/\.(m?js)$/, '')];
126
- if (PluginClass && typeof PluginClass === 'function') {
127
- const pluginName = PluginClass.name || file.replace(/\.(m?js)$/, '');
128
- customPlugins[pluginName] = () => new PluginClass() as Plugin;
129
- log(`Loaded custom plugin: ${pluginName}`);
130
- }
131
- } catch (err) {
132
- console.warn(`Failed to load plugin ${file}: ${(err as Error).message}`);
133
- }
134
- }
135
- } catch (err) {
136
- console.warn(`Error loading custom plugins: ${(err as Error).message}`);
137
- }
138
-
139
- return customPlugins;
140
- }
141
-
142
- function createPlugins(
143
- config: GrafemaConfig['plugins'],
144
- customPlugins: Record<string, () => Plugin> = {}
145
- ): Plugin[] {
146
- const plugins: Plugin[] = [];
147
- const phases: (keyof GrafemaConfig['plugins'])[] = ['discovery', 'indexing', 'analysis', 'enrichment', 'validation'];
148
-
149
- for (const phase of phases) {
150
- const names = config[phase] || [];
151
- for (const name of names) {
152
- // Check built-in first, then custom
153
- const factory = BUILTIN_PLUGINS[name] || customPlugins[name];
154
- if (factory) {
155
- plugins.push(factory());
156
- } else {
157
- console.warn(`Unknown plugin: ${name}`);
158
- }
159
- }
160
- }
161
-
162
- return plugins;
163
- }
164
-
165
- /**
166
- * Determine log level from CLI options.
167
- * Priority: --log-level > --quiet > --verbose > default ('info')
168
- */
169
- function getLogLevel(options: { quiet?: boolean; verbose?: boolean; logLevel?: string }): LogLevel {
170
- if (options.logLevel) {
171
- const validLevels: LogLevel[] = ['silent', 'errors', 'warnings', 'info', 'debug'];
172
- if (validLevels.includes(options.logLevel as LogLevel)) {
173
- return options.logLevel as LogLevel;
174
- }
175
- }
176
- if (options.quiet) return 'silent';
177
- if (options.verbose) return 'debug';
178
- return 'info';
179
- }
180
10
 
181
11
  export const analyzeCommand = new Command('analyze')
182
12
  .description('Run project analysis')
@@ -188,6 +18,7 @@ export const analyzeCommand = new Command('analyze')
188
18
  .option('-v, --verbose', 'Show verbose logging')
189
19
  .option('--debug', 'Enable debug mode (writes diagnostics.log)')
190
20
  .option('--log-level <level>', 'Set log level (silent, errors, warnings, info, debug)')
21
+ .option('--log-file <path>', 'Write all log output to a file')
191
22
  .option('--strict', 'Enable strict mode (fail on unresolved references)')
192
23
  .option('--auto-start', 'Auto-start RFDB server if not running')
193
24
  .addHelpText('after', `
@@ -198,176 +29,10 @@ Examples:
198
29
  grafema analyze -s api Analyze only "api" service (monorepo)
199
30
  grafema analyze -v Verbose output with progress details
200
31
  grafema analyze --debug Write diagnostics.log for debugging
32
+ grafema analyze --log-file out.log Write all logs to a file
201
33
  grafema analyze --strict Fail on unresolved references (debugging)
202
34
  grafema analyze --auto-start Auto-start server (useful for CI)
203
35
 
204
36
  Note: Start the server first with: grafema server start
205
37
  `)
206
- .action(async (path: string, options: { service?: string; entrypoint?: string; clear?: boolean; quiet?: boolean; verbose?: boolean; debug?: boolean; logLevel?: string; strict?: boolean; autoStart?: boolean }) => {
207
- const projectPath = resolve(path);
208
- const grafemaDir = join(projectPath, '.grafema');
209
- const dbPath = join(grafemaDir, 'graph.rfdb');
210
-
211
- if (!existsSync(grafemaDir)) {
212
- mkdirSync(grafemaDir, { recursive: true });
213
- }
214
-
215
- const log = options.quiet ? () => {} : console.log;
216
-
217
- // Create logger based on CLI flags
218
- const logLevel = getLogLevel(options);
219
- const logger = createLogger(logLevel);
220
-
221
- log(`Analyzing project: ${projectPath}`);
222
-
223
- // Connect to RFDB server
224
- // Default: require explicit `grafema server start`
225
- // Use --auto-start for CI or backwards compatibility
226
- const backend = new RFDBServerBackend({
227
- dbPath,
228
- autoStart: options.autoStart ?? false
229
- });
230
-
231
- try {
232
- await backend.connect();
233
- } catch (err) {
234
- if (!options.autoStart && err instanceof Error && err.message.includes('not running')) {
235
- console.error('');
236
- console.error('RFDB server is not running.');
237
- console.error('');
238
- console.error('Start the server first:');
239
- console.error(' grafema server start');
240
- console.error('');
241
- console.error('Or use --auto-start flag:');
242
- console.error(' grafema analyze --auto-start');
243
- console.error('');
244
- process.exit(1);
245
- }
246
- throw err;
247
- }
248
-
249
- if (options.clear) {
250
- log('Clearing existing database...');
251
- await backend.clear();
252
- }
253
-
254
- const config = loadConfig(projectPath, logger);
255
-
256
- // Extract services from config (REG-174)
257
- if (config.services.length > 0) {
258
- log(`Loaded ${config.services.length} service(s) from config`);
259
- for (const svc of config.services) {
260
- const entry = svc.entryPoint ? ` (entry: ${svc.entryPoint})` : '';
261
- log(` - ${svc.name}: ${svc.path}${entry}`);
262
- }
263
- }
264
-
265
- // Load custom plugins from .grafema/plugins/
266
- const customPlugins = await loadCustomPlugins(projectPath, log);
267
- const plugins = createPlugins(config.plugins, customPlugins);
268
-
269
- log(`Loaded ${plugins.length} plugins`);
270
-
271
- // Resolve strict mode: CLI flag overrides config
272
- const strictMode = options.strict ?? config.strict ?? false;
273
- if (strictMode) {
274
- log('Strict mode enabled - analysis will fail on unresolved references');
275
- }
276
-
277
- const startTime = Date.now();
278
-
279
- const orchestrator = new Orchestrator({
280
- graph: backend as unknown as import('@grafema/types').GraphBackend,
281
- plugins,
282
- serviceFilter: options.service || null,
283
- entrypoint: options.entrypoint,
284
- forceAnalysis: options.clear || false,
285
- logger,
286
- services: config.services.length > 0 ? config.services : undefined, // Pass config services (REG-174)
287
- strictMode, // REG-330: Pass strict mode flag
288
- onProgress: (progress) => {
289
- if (options.verbose) {
290
- log(`[${progress.phase}] ${progress.message}`);
291
- }
292
- },
293
- });
294
-
295
- let exitCode = 0;
296
-
297
- try {
298
- await orchestrator.run(projectPath);
299
- await backend.flush();
300
-
301
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
302
- const stats = await backend.getStats();
303
-
304
- log('');
305
- log(`Analysis complete in ${elapsed}s`);
306
- log(` Nodes: ${stats.nodeCount}`);
307
- log(` Edges: ${stats.edgeCount}`);
308
-
309
- // Get diagnostics and report summary
310
- const diagnostics = orchestrator.getDiagnostics();
311
- const reporter = new DiagnosticReporter(diagnostics);
312
-
313
- // Print summary if there are any issues
314
- if (diagnostics.count() > 0) {
315
- log('');
316
- log(reporter.categorizedSummary());
317
-
318
- // In verbose mode, print full report
319
- if (options.verbose) {
320
- log('');
321
- log(reporter.report({ format: 'text', includeSummary: false }));
322
- }
323
- }
324
-
325
- // Always write diagnostics.log (required for `grafema check` command)
326
- const writer = new DiagnosticWriter();
327
- await writer.write(diagnostics, grafemaDir);
328
- if (options.debug) {
329
- log(`Diagnostics written to ${writer.getLogPath(grafemaDir)}`);
330
- }
331
-
332
- // Determine exit code based on severity
333
- if (diagnostics.hasFatal()) {
334
- exitCode = 1;
335
- } else if (diagnostics.hasErrors()) {
336
- exitCode = 2; // Completed with errors
337
- } else {
338
- exitCode = 0; // Success (maybe warnings)
339
- }
340
- } catch (e) {
341
- // Orchestrator threw (fatal error stopped analysis)
342
- const error = e instanceof Error ? e : new Error(String(e));
343
- const diagnostics = orchestrator.getDiagnostics();
344
- const reporter = new DiagnosticReporter(diagnostics);
345
-
346
- console.error('');
347
- console.error(`✗ Analysis failed: ${error.message}`);
348
- console.error('');
349
- console.error('→ Run with --debug for detailed diagnostics');
350
-
351
- if (diagnostics.count() > 0) {
352
- console.error('');
353
- console.error(reporter.report({ format: 'text', includeSummary: true }));
354
- }
355
-
356
- // Write diagnostics.log in debug mode even on failure
357
- if (options.debug) {
358
- const writer = new DiagnosticWriter();
359
- await writer.write(diagnostics, grafemaDir);
360
- console.error(`Diagnostics written to ${writer.getLogPath(grafemaDir)}`);
361
- }
362
-
363
- exitCode = 1;
364
- }
365
-
366
- await backend.close();
367
-
368
- // Exit with appropriate code
369
- // 0 = success, 1 = fatal, 2 = errors
370
- if (exitCode !== 0) {
371
- process.exit(exitCode);
372
- }
373
- });
38
+ .action(analyzeAction);