@grafema/cli 0.2.5-beta → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/cli.js +6 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyze.d.ts +3 -10
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +5 -347
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/analyzeAction.d.ts +28 -0
- package/dist/commands/analyzeAction.d.ts.map +1 -0
- package/dist/commands/analyzeAction.js +243 -0
- package/dist/commands/analyzeAction.js.map +1 -0
- package/dist/commands/check.js +2 -2
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/context.d.ts +16 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +238 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/doctor/checks.js +1 -1
- package/dist/commands/doctor/checks.js.map +1 -1
- package/dist/commands/explain.d.ts.map +1 -1
- package/dist/commands/explain.js +4 -3
- package/dist/commands/explain.js.map +1 -1
- package/dist/commands/file.d.ts +15 -0
- package/dist/commands/file.d.ts.map +1 -0
- package/dist/commands/file.js +144 -0
- package/dist/commands/file.js.map +1 -0
- package/dist/commands/impact.d.ts.map +1 -1
- package/dist/commands/impact.js +2 -3
- package/dist/commands/impact.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +13 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +3 -2
- package/dist/commands/ls.js.map +1 -1
- package/dist/commands/query.d.ts +8 -0
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +158 -51
- package/dist/commands/query.js.map +1 -1
- package/dist/commands/schema.d.ts.map +1 -1
- package/dist/commands/schema.js +3 -2
- package/dist/commands/schema.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +8 -59
- package/dist/commands/server.js.map +1 -1
- package/dist/commands/setup-skill.d.ts +17 -0
- package/dist/commands/setup-skill.d.ts.map +1 -0
- package/dist/commands/setup-skill.js +131 -0
- package/dist/commands/setup-skill.js.map +1 -0
- package/dist/commands/trace.d.ts.map +1 -1
- package/dist/commands/trace.js +20 -10
- package/dist/commands/trace.js.map +1 -1
- package/dist/plugins/builtinPlugins.d.ts +10 -0
- package/dist/plugins/builtinPlugins.d.ts.map +1 -0
- package/dist/plugins/builtinPlugins.js +68 -0
- package/dist/plugins/builtinPlugins.js.map +1 -0
- package/dist/plugins/pluginLoader.d.ts +16 -0
- package/dist/plugins/pluginLoader.d.ts.map +1 -0
- package/dist/plugins/pluginLoader.js +101 -0
- package/dist/plugins/pluginLoader.js.map +1 -0
- package/dist/plugins/pluginResolver.js +38 -0
- package/dist/utils/codePreview.d.ts +1 -0
- package/dist/utils/codePreview.d.ts.map +1 -1
- package/dist/utils/codePreview.js +5 -3
- package/dist/utils/codePreview.js.map +1 -1
- package/dist/utils/formatNode.d.ts +1 -1
- package/dist/utils/formatNode.d.ts.map +1 -1
- package/dist/utils/formatNode.js +2 -2
- package/dist/utils/formatNode.js.map +1 -1
- package/dist/utils/pathUtils.d.ts +2 -0
- package/dist/utils/pathUtils.d.ts.map +1 -0
- package/dist/utils/pathUtils.js +9 -0
- package/dist/utils/pathUtils.js.map +1 -0
- package/dist/utils/progressRenderer.d.ts +4 -0
- package/dist/utils/progressRenderer.d.ts.map +1 -1
- package/dist/utils/progressRenderer.js +23 -4
- package/dist/utils/progressRenderer.js.map +1 -1
- package/package.json +7 -9
- package/skills/grafema-codebase-analysis/SKILL.md +295 -0
- package/skills/grafema-codebase-analysis/references/node-edge-types.md +123 -0
- package/skills/grafema-codebase-analysis/references/query-patterns.md +205 -0
- package/src/cli.ts +8 -2
- package/src/commands/analyze.ts +5 -435
- package/src/commands/analyzeAction.ts +284 -0
- package/src/commands/check.ts +2 -2
- package/src/commands/context.ts +309 -0
- package/src/commands/doctor/checks.ts +1 -1
- package/src/commands/explain.ts +4 -3
- package/src/commands/explore.tsx +7 -5
- package/src/commands/file.ts +179 -0
- package/src/commands/impact.ts +2 -3
- package/src/commands/init.ts +13 -1
- package/src/commands/ls.ts +3 -2
- package/src/commands/query.ts +167 -52
- package/src/commands/schema.ts +3 -2
- package/src/commands/server.ts +8 -64
- package/src/commands/setup-skill.ts +162 -0
- package/src/commands/trace.ts +18 -9
- package/src/plugins/builtinPlugins.ts +108 -0
- package/src/plugins/pluginLoader.ts +123 -0
- package/src/plugins/pluginResolver.js +38 -0
- package/src/utils/codePreview.ts +7 -3
- package/src/utils/formatNode.ts +3 -3
- package/src/utils/pathUtils.ts +9 -0
- package/src/utils/progressRenderer.ts +25 -4
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup-skill command - Install Grafema Agent Skill into a project
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { resolve, join } from 'path';
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, cpSync } from 'fs';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
11
|
+
|
|
12
|
+
/** Default install paths per platform */
|
|
13
|
+
const PLATFORM_PATHS: Record<string, string> = {
|
|
14
|
+
claude: '.claude/skills',
|
|
15
|
+
gemini: '.gemini/skills',
|
|
16
|
+
cursor: '.cursor/skills',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const SKILL_DIR_NAME = 'grafema-codebase-analysis';
|
|
20
|
+
|
|
21
|
+
interface SetupSkillOptions {
|
|
22
|
+
outputDir?: string;
|
|
23
|
+
platform?: string;
|
|
24
|
+
force?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the bundled skill source directory.
|
|
29
|
+
* In the published package, skills/ is at the package root alongside dist/.
|
|
30
|
+
*/
|
|
31
|
+
function getSkillSourceDir(): string {
|
|
32
|
+
// __dirname is dist/commands/ -> go up to package root, then into skills/
|
|
33
|
+
return join(__dirname, '..', '..', 'skills', SKILL_DIR_NAME);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Read version from skill metadata.
|
|
38
|
+
*/
|
|
39
|
+
function getSkillVersion(skillDir: string): string | null {
|
|
40
|
+
const skillMd = join(skillDir, 'SKILL.md');
|
|
41
|
+
if (!existsSync(skillMd)) return null;
|
|
42
|
+
|
|
43
|
+
const content = readFileSync(skillMd, 'utf-8');
|
|
44
|
+
const versionMatch = content.match(/version:\s*"?([^"\n]+)"?/);
|
|
45
|
+
return versionMatch ? versionMatch[1].trim() : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the target directory for skill installation.
|
|
50
|
+
*/
|
|
51
|
+
function resolveTargetDir(projectPath: string, options: SetupSkillOptions): string {
|
|
52
|
+
if (options.outputDir) {
|
|
53
|
+
return resolve(options.outputDir, SKILL_DIR_NAME);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const platform = options.platform || 'claude';
|
|
57
|
+
const basePath = PLATFORM_PATHS[platform];
|
|
58
|
+
if (!basePath) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Unknown platform: ${platform}. Supported: ${Object.keys(PLATFORM_PATHS).join(', ')}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return join(projectPath, basePath, SKILL_DIR_NAME);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Copy skill directory recursively.
|
|
69
|
+
*/
|
|
70
|
+
function copySkill(sourceDir: string, targetDir: string): void {
|
|
71
|
+
mkdirSync(targetDir, { recursive: true });
|
|
72
|
+
cpSync(sourceDir, targetDir, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Install the Grafema Agent Skill into a project directory.
|
|
77
|
+
* Returns true if skill was installed, false if skipped.
|
|
78
|
+
*/
|
|
79
|
+
export function installSkill(projectPath: string, options: SetupSkillOptions = {}): boolean {
|
|
80
|
+
const sourceDir = getSkillSourceDir();
|
|
81
|
+
|
|
82
|
+
if (!existsSync(sourceDir)) {
|
|
83
|
+
throw new Error(`Skill source not found at ${sourceDir}. Package may be corrupted.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const targetDir = resolveTargetDir(projectPath, options);
|
|
87
|
+
|
|
88
|
+
// Check if already installed
|
|
89
|
+
if (existsSync(targetDir) && !options.force) {
|
|
90
|
+
const installedVersion = getSkillVersion(targetDir);
|
|
91
|
+
const sourceVersion = getSkillVersion(sourceDir);
|
|
92
|
+
|
|
93
|
+
if (installedVersion === sourceVersion) {
|
|
94
|
+
return false; // Same version, skip
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Different version — warn but don't overwrite without --force
|
|
98
|
+
console.log(` Skill exists (v${installedVersion}), latest is v${sourceVersion}`);
|
|
99
|
+
console.log(' Use --force to update, or run: grafema setup-skill --force');
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
copySkill(sourceDir, targetDir);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const setupSkillCommand = new Command('setup-skill')
|
|
108
|
+
.description('Install Grafema Agent Skill into your project')
|
|
109
|
+
.argument('[path]', 'Project path', '.')
|
|
110
|
+
.option('--output-dir <path>', 'Custom output directory (overrides --platform)')
|
|
111
|
+
.option('--platform <name>', 'Target platform: claude, gemini, cursor', 'claude')
|
|
112
|
+
.option('-f, --force', 'Overwrite existing skill')
|
|
113
|
+
.addHelpText('after', `
|
|
114
|
+
Examples:
|
|
115
|
+
grafema setup-skill Install for Claude Code (.claude/skills/)
|
|
116
|
+
grafema setup-skill --platform gemini Install for Gemini CLI (.gemini/skills/)
|
|
117
|
+
grafema setup-skill --force Update existing skill
|
|
118
|
+
grafema setup-skill --output-dir ./my-skills/
|
|
119
|
+
`)
|
|
120
|
+
.action(async (path: string, options: SetupSkillOptions) => {
|
|
121
|
+
const projectPath = resolve(path);
|
|
122
|
+
const sourceDir = getSkillSourceDir();
|
|
123
|
+
|
|
124
|
+
if (!existsSync(sourceDir)) {
|
|
125
|
+
console.error('✗ Skill source not found. Package may be corrupted.');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const targetDir = resolveTargetDir(projectPath, options);
|
|
130
|
+
|
|
131
|
+
// Check if already installed
|
|
132
|
+
if (existsSync(targetDir) && !options.force) {
|
|
133
|
+
const installedVersion = getSkillVersion(targetDir);
|
|
134
|
+
const sourceVersion = getSkillVersion(sourceDir);
|
|
135
|
+
|
|
136
|
+
if (installedVersion === sourceVersion) {
|
|
137
|
+
console.log(`✓ Grafema skill already installed (v${installedVersion})`);
|
|
138
|
+
console.log(` Location: ${targetDir}`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(` Skill exists (v${installedVersion}), latest is v${sourceVersion}`);
|
|
143
|
+
console.log(' Use --force to update');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
copySkill(sourceDir, targetDir);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error('✗ Failed to install skill:', (err as Error).message);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const sourceVersion = getSkillVersion(sourceDir);
|
|
155
|
+
console.log(`✓ Grafema skill installed (v${sourceVersion})`);
|
|
156
|
+
console.log(` Location: ${targetDir}`);
|
|
157
|
+
console.log('');
|
|
158
|
+
console.log('Next steps:');
|
|
159
|
+
console.log(' 1. Ensure Grafema MCP server is configured in your AI agent');
|
|
160
|
+
console.log(' 2. Run "grafema analyze" to build the code graph');
|
|
161
|
+
console.log(' 3. Your AI agent will now prefer graph queries over reading files');
|
|
162
|
+
});
|
package/src/commands/trace.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { Command } from 'commander';
|
|
11
|
-
import { resolve, join } from 'path';
|
|
11
|
+
import { isAbsolute, resolve, join } from 'path';
|
|
12
12
|
import { existsSync } from 'fs';
|
|
13
|
-
import { RFDBServerBackend, parseSemanticId, traceValues, type ValueSource } from '@grafema/core';
|
|
13
|
+
import { RFDBServerBackend, parseSemanticId, parseSemanticIdV2, traceValues, type ValueSource } from '@grafema/core';
|
|
14
14
|
import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
|
|
15
15
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
16
16
|
|
|
@@ -227,12 +227,21 @@ async function findVariables(
|
|
|
227
227
|
if (name.toLowerCase() === varName.toLowerCase()) {
|
|
228
228
|
// If scope specified, check if variable is in that scope
|
|
229
229
|
if (scopeName) {
|
|
230
|
-
|
|
231
|
-
|
|
230
|
+
// Try v2 parsing first
|
|
231
|
+
const parsedV2 = parseSemanticIdV2(node.id);
|
|
232
|
+
if (parsedV2) {
|
|
233
|
+
if (!parsedV2.namedParent || parsedV2.namedParent.toLowerCase() !== lowerScopeName) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
// Fallback to v1 parsing
|
|
238
|
+
const parsed = parseSemanticId(node.id);
|
|
239
|
+
if (!parsed) continue; // Skip nodes with invalid IDs
|
|
232
240
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
241
|
+
// Check if scopeName appears anywhere in the scope chain
|
|
242
|
+
if (!parsed.scopePath.some(s => s.toLowerCase() === lowerScopeName)) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
236
245
|
}
|
|
237
246
|
}
|
|
238
247
|
|
|
@@ -738,7 +747,7 @@ async function handleSinkTrace(
|
|
|
738
747
|
const sourcesCount = pv.sources.length;
|
|
739
748
|
console.log(` - ${JSON.stringify(pv.value)} (${sourcesCount} source${sourcesCount === 1 ? '' : 's'})`);
|
|
740
749
|
for (const src of pv.sources.slice(0, 3)) {
|
|
741
|
-
const relativePath = src.file
|
|
750
|
+
const relativePath = isAbsolute(src.file)
|
|
742
751
|
? src.file.substring(projectPath.length + 1)
|
|
743
752
|
: src.file;
|
|
744
753
|
console.log(` <- ${relativePath}:${src.line}`);
|
|
@@ -900,7 +909,7 @@ async function handleRouteTrace(
|
|
|
900
909
|
// Format traced values
|
|
901
910
|
const sources = await Promise.all(
|
|
902
911
|
traced.map(async (t) => {
|
|
903
|
-
const relativePath = t.source.file
|
|
912
|
+
const relativePath = isAbsolute(t.source.file)
|
|
904
913
|
? t.source.file.substring(projectPath.length + 1)
|
|
905
914
|
: t.source.file;
|
|
906
915
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in plugin registry — maps plugin names to factory functions.
|
|
3
|
+
*
|
|
4
|
+
* Each entry creates a fresh plugin instance. Plugin names match the class names
|
|
5
|
+
* and are referenced by name in .grafema/config.yaml under phases:
|
|
6
|
+
* discovery, indexing, analysis, enrichment, validation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Plugin } from '@grafema/core';
|
|
10
|
+
import {
|
|
11
|
+
// Discovery
|
|
12
|
+
SimpleProjectDiscovery,
|
|
13
|
+
MonorepoServiceDiscovery,
|
|
14
|
+
WorkspaceDiscovery,
|
|
15
|
+
// Indexing
|
|
16
|
+
JSModuleIndexer,
|
|
17
|
+
RustModuleIndexer,
|
|
18
|
+
// Analysis
|
|
19
|
+
JSASTAnalyzer,
|
|
20
|
+
ExpressRouteAnalyzer,
|
|
21
|
+
ExpressResponseAnalyzer,
|
|
22
|
+
NestJSRouteAnalyzer,
|
|
23
|
+
SocketIOAnalyzer,
|
|
24
|
+
DatabaseAnalyzer,
|
|
25
|
+
FetchAnalyzer,
|
|
26
|
+
ServiceLayerAnalyzer,
|
|
27
|
+
ReactAnalyzer,
|
|
28
|
+
RustAnalyzer,
|
|
29
|
+
// Enrichment
|
|
30
|
+
MethodCallResolver,
|
|
31
|
+
ArgumentParameterLinker,
|
|
32
|
+
AliasTracker,
|
|
33
|
+
ValueDomainAnalyzer,
|
|
34
|
+
MountPointResolver,
|
|
35
|
+
ExpressHandlerLinker,
|
|
36
|
+
PrefixEvaluator,
|
|
37
|
+
InstanceOfResolver,
|
|
38
|
+
ImportExportLinker,
|
|
39
|
+
FunctionCallResolver,
|
|
40
|
+
HTTPConnectionEnricher,
|
|
41
|
+
ConfigRoutingMapBuilder,
|
|
42
|
+
ServiceConnectionEnricher,
|
|
43
|
+
RustFFIEnricher,
|
|
44
|
+
RejectionPropagationEnricher,
|
|
45
|
+
CallbackCallResolver,
|
|
46
|
+
// Validation
|
|
47
|
+
CallResolverValidator,
|
|
48
|
+
EvalBanValidator,
|
|
49
|
+
SQLInjectionValidator,
|
|
50
|
+
AwaitInLoopValidator,
|
|
51
|
+
ShadowingDetector,
|
|
52
|
+
GraphConnectivityValidator,
|
|
53
|
+
DataFlowValidator,
|
|
54
|
+
TypeScriptDeadCodeValidator,
|
|
55
|
+
BrokenImportValidator,
|
|
56
|
+
UnconnectedRouteValidator,
|
|
57
|
+
PackageCoverageValidator,
|
|
58
|
+
} from '@grafema/core';
|
|
59
|
+
|
|
60
|
+
export const BUILTIN_PLUGINS: Record<string, () => Plugin> = {
|
|
61
|
+
// Discovery
|
|
62
|
+
SimpleProjectDiscovery: () => new SimpleProjectDiscovery() as Plugin,
|
|
63
|
+
MonorepoServiceDiscovery: () => new MonorepoServiceDiscovery() as Plugin,
|
|
64
|
+
WorkspaceDiscovery: () => new WorkspaceDiscovery() as Plugin,
|
|
65
|
+
// Indexing
|
|
66
|
+
JSModuleIndexer: () => new JSModuleIndexer() as Plugin,
|
|
67
|
+
RustModuleIndexer: () => new RustModuleIndexer() as Plugin,
|
|
68
|
+
// Analysis
|
|
69
|
+
JSASTAnalyzer: () => new JSASTAnalyzer() as Plugin,
|
|
70
|
+
ExpressRouteAnalyzer: () => new ExpressRouteAnalyzer() as Plugin,
|
|
71
|
+
ExpressResponseAnalyzer: () => new ExpressResponseAnalyzer() as Plugin,
|
|
72
|
+
NestJSRouteAnalyzer: () => new NestJSRouteAnalyzer() as Plugin,
|
|
73
|
+
SocketIOAnalyzer: () => new SocketIOAnalyzer() as Plugin,
|
|
74
|
+
DatabaseAnalyzer: () => new DatabaseAnalyzer() as Plugin,
|
|
75
|
+
FetchAnalyzer: () => new FetchAnalyzer() as Plugin,
|
|
76
|
+
ServiceLayerAnalyzer: () => new ServiceLayerAnalyzer() as Plugin,
|
|
77
|
+
ReactAnalyzer: () => new ReactAnalyzer() as Plugin,
|
|
78
|
+
RustAnalyzer: () => new RustAnalyzer() as Plugin,
|
|
79
|
+
// Enrichment
|
|
80
|
+
MethodCallResolver: () => new MethodCallResolver() as Plugin,
|
|
81
|
+
ArgumentParameterLinker: () => new ArgumentParameterLinker() as Plugin,
|
|
82
|
+
AliasTracker: () => new AliasTracker() as Plugin,
|
|
83
|
+
ValueDomainAnalyzer: () => new ValueDomainAnalyzer() as Plugin,
|
|
84
|
+
MountPointResolver: () => new MountPointResolver() as Plugin,
|
|
85
|
+
ExpressHandlerLinker: () => new ExpressHandlerLinker() as Plugin,
|
|
86
|
+
PrefixEvaluator: () => new PrefixEvaluator() as Plugin,
|
|
87
|
+
InstanceOfResolver: () => new InstanceOfResolver() as Plugin,
|
|
88
|
+
ImportExportLinker: () => new ImportExportLinker() as Plugin,
|
|
89
|
+
FunctionCallResolver: () => new FunctionCallResolver() as Plugin,
|
|
90
|
+
HTTPConnectionEnricher: () => new HTTPConnectionEnricher() as Plugin,
|
|
91
|
+
ConfigRoutingMapBuilder: () => new ConfigRoutingMapBuilder() as Plugin,
|
|
92
|
+
ServiceConnectionEnricher: () => new ServiceConnectionEnricher() as Plugin,
|
|
93
|
+
RustFFIEnricher: () => new RustFFIEnricher() as Plugin,
|
|
94
|
+
RejectionPropagationEnricher: () => new RejectionPropagationEnricher() as Plugin,
|
|
95
|
+
CallbackCallResolver: () => new CallbackCallResolver() as Plugin,
|
|
96
|
+
// Validation
|
|
97
|
+
CallResolverValidator: () => new CallResolverValidator() as Plugin,
|
|
98
|
+
EvalBanValidator: () => new EvalBanValidator() as Plugin,
|
|
99
|
+
SQLInjectionValidator: () => new SQLInjectionValidator() as Plugin,
|
|
100
|
+
AwaitInLoopValidator: () => new AwaitInLoopValidator() as Plugin,
|
|
101
|
+
ShadowingDetector: () => new ShadowingDetector() as Plugin,
|
|
102
|
+
GraphConnectivityValidator: () => new GraphConnectivityValidator() as Plugin,
|
|
103
|
+
DataFlowValidator: () => new DataFlowValidator() as Plugin,
|
|
104
|
+
TypeScriptDeadCodeValidator: () => new TypeScriptDeadCodeValidator() as Plugin,
|
|
105
|
+
BrokenImportValidator: () => new BrokenImportValidator() as Plugin,
|
|
106
|
+
UnconnectedRouteValidator: () => new UnconnectedRouteValidator() as Plugin,
|
|
107
|
+
PackageCoverageValidator: () => new PackageCoverageValidator() as Plugin,
|
|
108
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin loading — resolves built-in and custom plugins from config.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - ESM resolve hook for custom plugin @grafema/* imports
|
|
6
|
+
* - Loading custom plugins from .grafema/plugins/
|
|
7
|
+
* - Creating plugin instances from config phases
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { existsSync, readdirSync } from 'fs';
|
|
12
|
+
import { pathToFileURL } from 'url';
|
|
13
|
+
import { register } from 'node:module';
|
|
14
|
+
import type { Plugin, GrafemaConfig } from '@grafema/core';
|
|
15
|
+
import { BUILTIN_PLUGINS } from './builtinPlugins.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Register ESM resolve hook so custom plugins can import @grafema/* packages.
|
|
19
|
+
*
|
|
20
|
+
* Plugins in .grafema/plugins/ do `import { Plugin } from '@grafema/core'`,
|
|
21
|
+
* but @grafema/core isn't in the target project's node_modules/.
|
|
22
|
+
* This hook redirects those imports to the CLI's bundled packages.
|
|
23
|
+
*
|
|
24
|
+
* Uses module.register() (stable Node.js 20.6+ API).
|
|
25
|
+
* Safe to call multiple times — subsequent calls add redundant hooks
|
|
26
|
+
* that short-circuit on the same specifiers.
|
|
27
|
+
*/
|
|
28
|
+
let pluginResolverRegistered = false;
|
|
29
|
+
|
|
30
|
+
export function registerPluginResolver(): void {
|
|
31
|
+
if (pluginResolverRegistered) return;
|
|
32
|
+
pluginResolverRegistered = true;
|
|
33
|
+
|
|
34
|
+
const grafemaPackages: Record<string, string> = {};
|
|
35
|
+
for (const pkg of ['@grafema/core', '@grafema/types']) {
|
|
36
|
+
try {
|
|
37
|
+
grafemaPackages[pkg] = import.meta.resolve(pkg);
|
|
38
|
+
} catch {
|
|
39
|
+
// Package not available from CLI context — skip
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
register(
|
|
44
|
+
new URL('./pluginResolver.js', import.meta.url),
|
|
45
|
+
{ data: { grafemaPackages } },
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Load custom plugins from .grafema/plugins/ directory
|
|
51
|
+
*/
|
|
52
|
+
export async function loadCustomPlugins(
|
|
53
|
+
projectPath: string,
|
|
54
|
+
log: (msg: string) => void
|
|
55
|
+
): Promise<Record<string, () => Plugin>> {
|
|
56
|
+
const pluginsDir = join(projectPath, '.grafema', 'plugins');
|
|
57
|
+
if (!existsSync(pluginsDir)) {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Ensure @grafema/* imports resolve for custom plugins (REG-380)
|
|
62
|
+
registerPluginResolver();
|
|
63
|
+
|
|
64
|
+
const customPlugins: Record<string, () => Plugin> = {};
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const files = readdirSync(pluginsDir).filter(
|
|
68
|
+
(f) => f.endsWith('.js') || f.endsWith('.mjs') || f.endsWith('.cjs')
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
for (const file of files) {
|
|
72
|
+
try {
|
|
73
|
+
const pluginPath = join(pluginsDir, file);
|
|
74
|
+
const pluginUrl = pathToFileURL(pluginPath).href;
|
|
75
|
+
const module = await import(pluginUrl);
|
|
76
|
+
|
|
77
|
+
const PluginClass = module.default || module[file.replace(/\.[cm]?js$/, '')];
|
|
78
|
+
if (PluginClass && typeof PluginClass === 'function') {
|
|
79
|
+
const pluginName = PluginClass.name || file.replace(/\.[cm]?js$/, '');
|
|
80
|
+
customPlugins[pluginName] = () => {
|
|
81
|
+
const instance = new PluginClass() as Plugin;
|
|
82
|
+
instance.config.sourceFile = pluginPath;
|
|
83
|
+
return instance;
|
|
84
|
+
};
|
|
85
|
+
log(`Loaded custom plugin: ${pluginName}`);
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
89
|
+
console.warn(`Failed to load plugin ${file}: ${message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
94
|
+
console.warn(`Error loading custom plugins: ${message}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return customPlugins;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function createPlugins(
|
|
101
|
+
config: GrafemaConfig['plugins'],
|
|
102
|
+
customPlugins: Record<string, () => Plugin> = {},
|
|
103
|
+
verbose: boolean = false
|
|
104
|
+
): Plugin[] {
|
|
105
|
+
const plugins: Plugin[] = [];
|
|
106
|
+
const phases: (keyof GrafemaConfig['plugins'])[] = ['discovery', 'indexing', 'analysis', 'enrichment', 'validation'];
|
|
107
|
+
|
|
108
|
+
for (const phase of phases) {
|
|
109
|
+
const names = config[phase] || [];
|
|
110
|
+
for (const name of names) {
|
|
111
|
+
// Check built-in first, then custom
|
|
112
|
+
const factory = BUILTIN_PLUGINS[name] || customPlugins[name];
|
|
113
|
+
if (factory) {
|
|
114
|
+
plugins.push(factory());
|
|
115
|
+
} else if (verbose) {
|
|
116
|
+
// Only show plugin warning in verbose mode
|
|
117
|
+
console.warn(`Plugin not found: ${name} (skipping). Check .grafema/config.yaml or add to .grafema/plugins/`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return plugins;
|
|
123
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESM resolve hook for custom Grafema plugins.
|
|
3
|
+
*
|
|
4
|
+
* Allows plugins in .grafema/plugins/ to `import { Plugin } from '@grafema/core'`
|
|
5
|
+
* without requiring @grafema/core in the target project's node_modules/.
|
|
6
|
+
*
|
|
7
|
+
* The hook maps @grafema/* bare specifiers to the actual package URLs
|
|
8
|
+
* within the CLI's dependency tree.
|
|
9
|
+
*
|
|
10
|
+
* Registered via module.register() before loading custom plugins.
|
|
11
|
+
* Must be plain JS — loader hooks run in a separate thread.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** @type {Record<string, string>} package name → resolved file URL */
|
|
15
|
+
let grafemaPackages = {};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Called once when the hook is registered via module.register().
|
|
19
|
+
* @param {{ grafemaPackages: Record<string, string> }} data
|
|
20
|
+
*/
|
|
21
|
+
export function initialize(data) {
|
|
22
|
+
grafemaPackages = data.grafemaPackages;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve hook — intercepts bare specifier imports for @grafema/* packages
|
|
27
|
+
* and redirects them to the CLI's bundled versions.
|
|
28
|
+
*
|
|
29
|
+
* Only exact package name matches are handled (e.g. '@grafema/core').
|
|
30
|
+
* All other specifiers pass through to the default resolver.
|
|
31
|
+
*/
|
|
32
|
+
export function resolve(specifier, context, next) {
|
|
33
|
+
if (grafemaPackages[specifier]) {
|
|
34
|
+
return { url: grafemaPackages[specifier], shortCircuit: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return next(specifier, context);
|
|
38
|
+
}
|
package/src/utils/codePreview.ts
CHANGED
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { readFileSync, existsSync } from 'fs';
|
|
9
|
+
import { isAbsolute, join } from 'path';
|
|
9
10
|
|
|
10
11
|
export interface CodePreviewOptions {
|
|
11
12
|
file: string;
|
|
12
13
|
line: number;
|
|
13
14
|
contextBefore?: number; // default: 2
|
|
14
15
|
contextAfter?: number; // default: 12
|
|
16
|
+
projectPath?: string;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface CodePreviewResult {
|
|
@@ -25,14 +27,16 @@ export interface CodePreviewResult {
|
|
|
25
27
|
* Returns lines around the specified line number with context.
|
|
26
28
|
*/
|
|
27
29
|
export function getCodePreview(options: CodePreviewOptions): CodePreviewResult | null {
|
|
28
|
-
const { file, line, contextBefore = 2, contextAfter = 12 } = options;
|
|
30
|
+
const { file, line, contextBefore = 2, contextAfter = 12, projectPath } = options;
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
const absoluteFile = projectPath && !isAbsolute(file) ? join(projectPath, file) : file;
|
|
33
|
+
|
|
34
|
+
if (!existsSync(absoluteFile)) {
|
|
31
35
|
return null;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
try {
|
|
35
|
-
const content = readFileSync(
|
|
39
|
+
const content = readFileSync(absoluteFile, 'utf-8');
|
|
36
40
|
const allLines = content.split('\n');
|
|
37
41
|
|
|
38
42
|
// Calculate range (1-indexed)
|
package/src/utils/formatNode.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Semantic IDs are shown as the PRIMARY identifier, with location as secondary.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { relative } from 'path';
|
|
8
|
+
import { isAbsolute, relative } from 'path';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Format options for node display
|
|
@@ -29,7 +29,7 @@ export interface DisplayableNode {
|
|
|
29
29
|
type: string;
|
|
30
30
|
/** Human-readable name */
|
|
31
31
|
name: string;
|
|
32
|
-
/**
|
|
32
|
+
/** Source file path (relative to project root, or absolute for legacy) */
|
|
33
33
|
file: string;
|
|
34
34
|
/** Line number (optional) */
|
|
35
35
|
line?: number;
|
|
@@ -123,6 +123,6 @@ export function formatLocation(
|
|
|
123
123
|
projectPath: string
|
|
124
124
|
): string {
|
|
125
125
|
if (!file) return '';
|
|
126
|
-
const relPath = relative(projectPath, file);
|
|
126
|
+
const relPath = isAbsolute(file) ? relative(projectPath, file) : file;
|
|
127
127
|
return line ? `${relPath}:${line}` : relPath;
|
|
128
128
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a node file path to relative display format.
|
|
3
|
+
* Handles both legacy absolute paths and new relative paths (REG-408).
|
|
4
|
+
*/
|
|
5
|
+
import { relative, isAbsolute } from 'path';
|
|
6
|
+
|
|
7
|
+
export function toRelativeDisplay(file: string, projectPath: string): string {
|
|
8
|
+
return isAbsolute(file) ? relative(projectPath, file) : file;
|
|
9
|
+
}
|
|
@@ -45,6 +45,8 @@ export class ProgressRenderer {
|
|
|
45
45
|
private totalFiles: number = 0;
|
|
46
46
|
private processedFiles: number = 0;
|
|
47
47
|
private servicesAnalyzed: number = 0;
|
|
48
|
+
private totalServices: number = 0;
|
|
49
|
+
private currentService: string = '';
|
|
48
50
|
private spinnerIndex: number = 0;
|
|
49
51
|
private isInteractive: boolean;
|
|
50
52
|
private startTime: number;
|
|
@@ -100,6 +102,12 @@ export class ProgressRenderer {
|
|
|
100
102
|
if (info.servicesAnalyzed !== undefined) {
|
|
101
103
|
this.servicesAnalyzed = info.servicesAnalyzed;
|
|
102
104
|
}
|
|
105
|
+
if (info.totalServices !== undefined) {
|
|
106
|
+
this.totalServices = info.totalServices;
|
|
107
|
+
}
|
|
108
|
+
if (info.currentService !== undefined) {
|
|
109
|
+
this.currentService = info.currentService;
|
|
110
|
+
}
|
|
103
111
|
|
|
104
112
|
// Update spinner
|
|
105
113
|
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length;
|
|
@@ -222,11 +230,20 @@ export class ProgressRenderer {
|
|
|
222
230
|
}
|
|
223
231
|
return '';
|
|
224
232
|
case 'indexing':
|
|
225
|
-
case 'analysis':
|
|
226
|
-
|
|
227
|
-
|
|
233
|
+
case 'analysis': {
|
|
234
|
+
const parts: string[] = [];
|
|
235
|
+
if (this.totalServices > 0) {
|
|
236
|
+
parts.push(`${this.servicesAnalyzed}/${this.totalServices} services`);
|
|
228
237
|
}
|
|
229
|
-
|
|
238
|
+
if (this.currentService) {
|
|
239
|
+
// Truncate long service names
|
|
240
|
+
const name = this.currentService.length > 30
|
|
241
|
+
? '...' + this.currentService.slice(-27)
|
|
242
|
+
: this.currentService;
|
|
243
|
+
parts.push(name);
|
|
244
|
+
}
|
|
245
|
+
return parts.length > 0 ? ` ${parts.join(' | ')}` : '';
|
|
246
|
+
}
|
|
230
247
|
case 'enrichment':
|
|
231
248
|
case 'validation':
|
|
232
249
|
if (this.activePlugins.length > 0) {
|
|
@@ -268,6 +285,8 @@ export class ProgressRenderer {
|
|
|
268
285
|
processedFiles: number;
|
|
269
286
|
totalFiles: number;
|
|
270
287
|
servicesAnalyzed: number;
|
|
288
|
+
totalServices: number;
|
|
289
|
+
currentService: string;
|
|
271
290
|
spinnerIndex: number;
|
|
272
291
|
activePlugins: string[];
|
|
273
292
|
nodeCount: number;
|
|
@@ -279,6 +298,8 @@ export class ProgressRenderer {
|
|
|
279
298
|
processedFiles: this.processedFiles,
|
|
280
299
|
totalFiles: this.totalFiles,
|
|
281
300
|
servicesAnalyzed: this.servicesAnalyzed,
|
|
301
|
+
totalServices: this.totalServices,
|
|
302
|
+
currentService: this.currentService,
|
|
282
303
|
spinnerIndex: this.spinnerIndex,
|
|
283
304
|
activePlugins: [...this.activePlugins],
|
|
284
305
|
nodeCount: this.nodeCount,
|