@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
@@ -10,7 +10,8 @@
10
10
  */
11
11
 
12
12
  import { Command } from 'commander';
13
- import { resolve, join, relative } from 'path';
13
+ import { resolve, join } from 'path';
14
+ import { toRelativeDisplay } from '../utils/pathUtils.js';
14
15
  import { existsSync, writeFileSync } from 'fs';
15
16
  import {
16
17
  RFDBServerBackend,
@@ -82,7 +83,7 @@ function formatInterfaceYaml(schema: InterfaceSchema): string {
82
83
 
83
84
  function formatInterfaceMarkdown(schema: InterfaceSchema, projectPath: string): string {
84
85
  const lines: string[] = [];
85
- const relPath = relative(projectPath, schema.source.file);
86
+ const relPath = toRelativeDisplay(schema.source.file, projectPath);
86
87
 
87
88
  lines.push(`# Interface: ${schema.name}`);
88
89
  lines.push('');
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Server command - Manage RFDB server lifecycle
2
+ * Server command - Manage RFDB (Rega Flow Database) server lifecycle
3
3
  *
4
4
  * Provides explicit control over the RFDB server process:
5
5
  * grafema server start - Start detached server
6
6
  * grafema server stop - Stop server gracefully
7
7
  * grafema server status - Check if server is running
8
+ * grafema server graphql - Start GraphQL API server
8
9
  */
9
10
 
10
11
  import { Command } from 'commander';
@@ -12,7 +13,7 @@ import { resolve, join } from 'path';
12
13
  import { existsSync, unlinkSync, writeFileSync, readFileSync } from 'fs';
13
14
  import { spawn } from 'child_process';
14
15
  import { setTimeout as sleep } from 'timers/promises';
15
- import { RFDBClient, loadConfig, findRfdbBinary, getBinaryNotFoundMessage } from '@grafema/core';
16
+ import { RFDBClient, loadConfig, RFDBServerBackend, findRfdbBinary } from '@grafema/core';
16
17
  import { exitWithError } from '../utils/errorFormatter.js';
17
18
 
18
19
  // Extend config type for server settings
@@ -20,6 +21,18 @@ interface ServerConfig {
20
21
  binaryPath?: string;
21
22
  }
22
23
 
24
+ /**
25
+ * Find RFDB server binary using shared utility.
26
+ * Wraps findRfdbBinary() with CLI-specific error logging.
27
+ */
28
+ function findServerBinary(explicitPath?: string): string | null {
29
+ const binaryPath = findRfdbBinary({ explicitPath });
30
+ if (!binaryPath && explicitPath) {
31
+ console.error(`Specified binary not found: ${explicitPath}`);
32
+ }
33
+ return binaryPath;
34
+ }
35
+
23
36
  /**
24
37
  * Check if server is running by attempting to ping it
25
38
  */
@@ -56,7 +69,7 @@ function getProjectPaths(projectPath: string) {
56
69
 
57
70
  // Create main server command with subcommands
58
71
  export const serverCommand = new Command('server')
59
- .description('Manage RFDB server lifecycle')
72
+ .description('Manage RFDB (Rega Flow Database) server lifecycle')
60
73
  .addHelpText('after', `
61
74
  Examples:
62
75
  grafema server start Start the RFDB server
@@ -104,34 +117,38 @@ serverCommand
104
117
  }
105
118
 
106
119
  // Determine binary path: CLI flag > config > auto-detect
107
- let explicitPath: string | undefined;
120
+ let binaryPath: string | null = null;
108
121
 
109
122
  if (options.binary) {
110
123
  // Explicit --binary flag
111
- explicitPath = options.binary;
124
+ binaryPath = findServerBinary(options.binary);
112
125
  } else {
113
126
  // Try to read from config
114
127
  try {
115
128
  const config = loadConfig(projectPath);
116
129
  const serverConfig = (config as unknown as { server?: ServerConfig }).server;
117
130
  if (serverConfig?.binaryPath) {
118
- explicitPath = serverConfig.binaryPath;
131
+ binaryPath = findServerBinary(serverConfig.binaryPath);
119
132
  }
120
133
  } catch {
121
134
  // Config not found or invalid - continue with auto-detect
122
135
  }
123
- }
124
136
 
125
- const binaryPath = findRfdbBinary({ explicitPath });
137
+ // Auto-detect if not specified
138
+ if (!binaryPath) {
139
+ binaryPath = findServerBinary();
140
+ }
141
+ }
126
142
 
127
143
  if (!binaryPath) {
128
- // If explicit path was given but not found, show specific error
129
- if (explicitPath) {
130
- exitWithError(`Specified binary not found: ${resolve(explicitPath)}`, [
131
- 'Check the path and try again'
132
- ]);
133
- }
134
- exitWithError('RFDB server binary not found', getBinaryNotFoundMessage().split('\n').slice(2));
144
+ exitWithError('RFDB server binary not found', [
145
+ 'Specify path: grafema server start --binary /path/to/rfdb-server',
146
+ 'Or add to config.yaml:',
147
+ ' server:',
148
+ ' binaryPath: /path/to/rfdb-server',
149
+ 'Or install: npm install @grafema/rfdb',
150
+ 'Or build: cargo build --release && cp target/release/rfdb-server ~/.local/bin/'
151
+ ]);
135
152
  }
136
153
 
137
154
  console.log(`Starting RFDB server...`);
@@ -323,3 +340,56 @@ serverCommand
323
340
  }
324
341
  }
325
342
  });
343
+
344
+ // grafema server graphql
345
+ serverCommand
346
+ .command('graphql')
347
+ .description('Start GraphQL API server (requires RFDB server running)')
348
+ .option('-p, --project <path>', 'Project path', '.')
349
+ .option('--port <number>', 'Port to listen on', '4000')
350
+ .option('--host <string>', 'Hostname to bind to', 'localhost')
351
+ .action(async (options: { project: string; port: string; host: string }) => {
352
+ const projectPath = resolve(options.project);
353
+ const { socketPath } = getProjectPaths(projectPath);
354
+
355
+ // Check if RFDB server is running
356
+ const status = await isServerRunning(socketPath);
357
+ if (!status.running) {
358
+ exitWithError('RFDB server not running', [
359
+ 'Start the server first: grafema server start',
360
+ 'Or run: grafema analyze (starts server automatically)'
361
+ ]);
362
+ }
363
+
364
+ // Create backend connection
365
+ const backend = new RFDBServerBackend({ socketPath });
366
+ await backend.connect();
367
+
368
+ // Import and start GraphQL server
369
+ const { startServer } = await import('@grafema/api');
370
+ const port = parseInt(options.port, 10);
371
+
372
+ console.log('Starting Grafema GraphQL API...');
373
+ console.log(` RFDB Socket: ${socketPath}`);
374
+ if (status.version) {
375
+ console.log(` RFDB Version: ${status.version}`);
376
+ }
377
+ console.log('');
378
+
379
+ const server = startServer({
380
+ backend,
381
+ port,
382
+ hostname: options.host,
383
+ });
384
+
385
+ // Handle shutdown
386
+ const shutdown = async () => {
387
+ console.log('\nShutting down GraphQL server...');
388
+ server.close();
389
+ await backend.close();
390
+ process.exit(0);
391
+ };
392
+
393
+ process.on('SIGINT', shutdown);
394
+ process.on('SIGTERM', shutdown);
395
+ });
@@ -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
+ });
@@ -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
- const parsed = parseSemanticId(node.id);
231
- if (!parsed) continue; // Skip nodes with invalid IDs
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
- // Check if scopeName appears anywhere in the scope chain
234
- if (!parsed.scopePath.some(s => s.toLowerCase() === lowerScopeName)) {
235
- continue;
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.startsWith(projectPath)
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.startsWith(projectPath)
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
+ }