@parseme/cli 0.0.3 → 0.0.5

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 (47) hide show
  1. package/README.md +182 -187
  2. package/dist/cli/cli.js +144 -0
  3. package/dist/{analyzers → core/analyzers}/ast-analyzer.d.ts +1 -1
  4. package/dist/{analyzers → core/analyzers}/ast-analyzer.js +14 -27
  5. package/dist/core/analyzers/framework-detector.d.ts +7 -0
  6. package/dist/core/analyzers/framework-detector.js +165 -0
  7. package/dist/{analyzers → core/analyzers}/pattern-detector.d.ts +2 -5
  8. package/dist/{analyzers → core/analyzers}/pattern-detector.js +134 -49
  9. package/dist/{analyzers → core/analyzers}/project-analyzer.d.ts +2 -0
  10. package/dist/{analyzers → core/analyzers}/project-analyzer.js +12 -9
  11. package/dist/core/config.d.ts +19 -0
  12. package/dist/{config.js → core/config.js} +79 -91
  13. package/dist/{builders → core}/context-builder.d.ts +4 -11
  14. package/dist/core/context-builder.js +225 -0
  15. package/dist/{generator.d.ts → core/generator.d.ts} +0 -3
  16. package/dist/{generator.js → core/generator.js} +12 -17
  17. package/dist/core/types/analyzer-types.d.ts +38 -0
  18. package/dist/core/types/analyzer-types.js +2 -0
  19. package/dist/core/types/config-types.d.ts +36 -0
  20. package/dist/core/types/config-types.js +2 -0
  21. package/dist/core/types/generator-types.d.ts +6 -0
  22. package/dist/core/types/generator-types.js +2 -0
  23. package/dist/core/types/index.d.ts +4 -0
  24. package/dist/core/types/index.js +5 -0
  25. package/dist/core/types/project-types.d.ts +30 -0
  26. package/dist/core/types/project-types.js +2 -0
  27. package/dist/core/types.d.ts +1 -0
  28. package/dist/core/types.js +2 -0
  29. package/dist/index.d.ts +3 -4
  30. package/dist/index.js +2 -2
  31. package/dist/utils/file-collector.d.ts +23 -0
  32. package/dist/utils/file-collector.js +61 -0
  33. package/dist/utils/file-filter.d.ts +30 -0
  34. package/dist/utils/file-filter.js +99 -0
  35. package/dist/{analyzers/git-analyzer.d.ts → utils/git.d.ts} +1 -4
  36. package/dist/{analyzers/git-analyzer.js → utils/git.js} +0 -4
  37. package/dist/{prompt.d.ts → utils/prompt.d.ts} +0 -1
  38. package/dist/{prompt.js → utils/prompt.js} +0 -9
  39. package/package.json +12 -8
  40. package/dist/analyzers/framework-detector.d.ts +0 -12
  41. package/dist/analyzers/framework-detector.js +0 -180
  42. package/dist/builders/context-builder.js +0 -386
  43. package/dist/cli.js +0 -145
  44. package/dist/config.d.ts +0 -44
  45. package/dist/types.d.ts +0 -84
  46. package/dist/types.js +0 -2
  47. /package/dist/{cli.d.ts → cli/cli.d.ts} +0 -0
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ import { join } from 'path';
3
+ import { Command } from 'commander';
4
+ import { ParsemeConfig } from '../core/config.js';
5
+ import { ParsemeGenerator } from '../core/generator.js';
6
+ import { prompt } from '../utils/prompt.js';
7
+ const program = new Command();
8
+ async function promptForMissingConfig(config) {
9
+ return { ...config };
10
+ }
11
+ program.name('parseme').description('AI Project Context Generator').version('0.1.0');
12
+ // Generate command
13
+ program
14
+ .command('generate')
15
+ .alias('g')
16
+ .description('Generate project context using config file')
17
+ .option('-c, --config <path>', 'Config file path')
18
+ .option('-o, --output <path>', 'Output file path')
19
+ .option('-r, --root <path>', 'Root directory to analyze')
20
+ .option('--context-dir <path>', 'Context directory path (default: parseme-context)')
21
+ .option('--file-types <types...>', 'File types to analyze (e.g., ts tsx js jsx)')
22
+ .option('--exclude <patterns...>', 'Exclude patterns (glob)')
23
+ .option('--no-git', 'Disable git integration (info and file discovery)')
24
+ .option('--max-depth <number>', 'Maximum directory depth', parseInt)
25
+ .action(async (options) => {
26
+ try {
27
+ // Convert CLI options to config format
28
+ const cliOptions = {
29
+ ...(options.output && { outputPath: options.output }),
30
+ ...(options.root && { rootDir: options.root }),
31
+ ...(options.contextDir && { contextDir: options.contextDir }),
32
+ ...(options.fileTypes && { analyzeFileTypes: options.fileTypes }),
33
+ ...(options.exclude && { excludePatterns: options.exclude }),
34
+ ...(options.git === false && { includeGitInfo: false, useGitForFiles: false }),
35
+ ...(options.maxDepth && { maxDepth: options.maxDepth }),
36
+ };
37
+ const configFromFile = await ParsemeConfig.fromFile(options.config, {
38
+ showWarnings: true,
39
+ throwOnNotFound: true,
40
+ });
41
+ const interactiveConfig = await promptForMissingConfig(configFromFile.get());
42
+ // Merge: CLI options > interactive prompts > config file > defaults
43
+ const finalConfig = {
44
+ ...interactiveConfig,
45
+ ...cliOptions,
46
+ };
47
+ const config = new ParsemeConfig(finalConfig);
48
+ const generator = new ParsemeGenerator(config.get());
49
+ await generator.generateToFile();
50
+ console.log('Context generated successfully');
51
+ }
52
+ catch (error) {
53
+ if (error instanceof Error && error.message.includes('No configuration file found')) {
54
+ console.error(error.message);
55
+ process.exit(1);
56
+ }
57
+ console.error('Failed to generate context:', error);
58
+ process.exit(1);
59
+ }
60
+ });
61
+ // Init command
62
+ program
63
+ .command('init')
64
+ .alias('i')
65
+ .description('Initialize parseme configuration')
66
+ .option('-f, --force', 'Overwrite existing config')
67
+ .option('--format <format>', 'Config format: json, ts, or js', 'json')
68
+ .action(async (options) => {
69
+ try {
70
+ // Validate format
71
+ if (!['js', 'ts', 'json'].includes(options.format)) {
72
+ console.error('Invalid format. Use js, ts, or json');
73
+ process.exit(1);
74
+ }
75
+ const configPath = join(process.cwd(), `parseme.config.${options.format}`);
76
+ // Check if config already exists
77
+ if (!options.force) {
78
+ try {
79
+ const fs = await import('fs/promises');
80
+ await fs.access(configPath);
81
+ console.log('Configuration file already exists. Use --force to overwrite.');
82
+ process.exit(1);
83
+ }
84
+ catch {
85
+ // File doesn't exist, continue
86
+ }
87
+ }
88
+ // Build config with only user-specified values
89
+ const userConfig = {};
90
+ // Set defaults that would normally be prompted for
91
+ const defaultExcludePatterns = ['node_modules/**', 'dist/**', '.git/**'];
92
+ userConfig.contextDir = 'parseme-context';
93
+ userConfig.excludePatterns = defaultExcludePatterns;
94
+ // Only prompt if interactive (TTY) and not in CI
95
+ if (process.stdin.isTTY && !process.env.CI) {
96
+ // Ask about context directory path
97
+ // Set context directory to what user entered (or default if they pressed enter)
98
+ userConfig.contextDir = await prompt({
99
+ message: 'Context directory path',
100
+ defaultValue: 'parseme-context',
101
+ });
102
+ // Ask about exclude patterns
103
+ const excludePatternsAnswer = await prompt({
104
+ message: 'Exclude patterns (comma-separated glob patterns - in git repositories, additional to git-tracked files)',
105
+ defaultValue: defaultExcludePatterns.join(', '),
106
+ });
107
+ // Always set exclude patterns to what user entered (or defaults if they pressed enter)
108
+ userConfig.excludePatterns = excludePatternsAnswer
109
+ .split(',')
110
+ .map((p) => p.trim())
111
+ .filter((p) => p.length > 0);
112
+ }
113
+ const config = new ParsemeConfig(userConfig);
114
+ await config.save(configPath);
115
+ console.log(`Configuration file created at ${configPath}`);
116
+ if (options.format === 'ts') {
117
+ console.log('For TypeScript configs, ensure tsx or ts-node is available to load .ts files');
118
+ }
119
+ console.log('Tip: Add "parseme": "parseme" to your package.json scripts for easier manual execution or hook integration');
120
+ console.log('Tip: Add parseme as a git hook to keep context auto-updated! See README for setup instructions.');
121
+ console.log('');
122
+ console.log('Tip: Add this section to your README.md to help AI agents find the context:');
123
+ console.log('');
124
+ console.log('## Instructions For AI Agents');
125
+ console.log('This project includes AI-optimized documentation for efficient context providing:');
126
+ console.log('- `PARSEME.md` - Project overview with links to detailed context files');
127
+ console.log('- `parseme-context/` - Structured data files (AST analysis, dependencies, routes, git info)');
128
+ console.log('');
129
+ }
130
+ catch (error) {
131
+ console.error('Failed to create configuration:', error);
132
+ process.exit(1);
133
+ }
134
+ });
135
+ // If no command provided, show error and available commands
136
+ if (process.argv.length <= 2) {
137
+ console.error('No command specified.\n');
138
+ console.error('Available commands:');
139
+ console.error(' parseme generate (or parseme g) - Generate project context');
140
+ console.error(' parseme init (or parseme i) - Initialize parseme configuration');
141
+ console.error('\nUse "parseme --help" for more information');
142
+ process.exit(1);
143
+ }
144
+ program.parse();
@@ -2,7 +2,7 @@ import type { ParsemeConfig } from '../config.js';
2
2
  import type { FileAnalysis } from '../types.js';
3
3
  export declare class ASTAnalyzer {
4
4
  private readonly config;
5
- private readonly ig;
5
+ private readonly fileCollector;
6
6
  private readonly patternDetector;
7
7
  constructor(config: ParsemeConfig);
8
8
  analyzeProject(rootDir: string): Promise<FileAnalysis[]>;
@@ -1,45 +1,32 @@
1
1
  import { readFile } from 'fs/promises';
2
- import { relative, extname } from 'path';
2
+ import { join, extname } from 'path';
3
3
  import { parse } from '@babel/parser';
4
4
  import traverse from '@babel/traverse';
5
5
  import * as t from '@babel/types';
6
- import { glob } from 'glob';
7
- import ignore from 'ignore';
8
6
  import { PatternDetector } from './pattern-detector.js';
7
+ import { FileCollector } from '../../utils/file-collector.js';
9
8
  export class ASTAnalyzer {
10
9
  config;
11
- ig;
10
+ fileCollector;
12
11
  patternDetector;
13
12
  constructor(config) {
14
13
  this.config = config;
15
- this.ig = ignore();
16
- const configData = this.config.get();
17
- this.ig.add(configData.excludePatterns || []);
18
- this.patternDetector = new PatternDetector(config);
14
+ this.fileCollector = new FileCollector(config);
15
+ this.patternDetector = new PatternDetector();
19
16
  }
20
17
  async analyzeProject(rootDir) {
21
- const configData = this.config.get();
22
- const patterns = configData.includePatterns || ['**/*.ts', '**/*.js'];
23
- const files = await glob(patterns, {
24
- cwd: rootDir,
25
- absolute: true,
26
- ignore: configData.excludePatterns,
27
- });
18
+ const result = await this.fileCollector.getCodeFiles(rootDir);
28
19
  const analyses = [];
29
- for (const file of files) {
30
- const relativePath = relative(rootDir, file);
31
- // Skip if ignored
32
- if (this.ig.ignores(relativePath)) {
33
- continue;
34
- }
20
+ for (const file of result.files) {
21
+ const filePath = join(rootDir, file);
35
22
  try {
36
- const analysis = await this.analyzeFile(file, relativePath);
23
+ const analysis = await this.analyzeFile(filePath, file);
37
24
  if (analysis) {
38
25
  analyses.push(analysis);
39
26
  }
40
27
  }
41
28
  catch (error) {
42
- console.warn(`Failed to analyze ${relativePath}:`, error);
29
+ console.warn(`Failed to analyze ${file}:`, error);
43
30
  // Continue with other files
44
31
  }
45
32
  }
@@ -58,7 +45,7 @@ export class ASTAnalyzer {
58
45
  const patterns = this.patternDetector.analyzePatterns(ast, relativePath, content);
59
46
  const analysis = {
60
47
  path: relativePath,
61
- type: this.determineFileType(relativePath, content, patterns),
48
+ type: this.determineFileType(relativePath, patterns),
62
49
  exports: [],
63
50
  imports: [],
64
51
  functions: [],
@@ -116,8 +103,8 @@ export class ASTAnalyzer {
116
103
  });
117
104
  return analysis;
118
105
  }
119
- catch (error) {
120
- console.warn(`Failed to parse ${relativePath}:`, error);
106
+ catch {
107
+ console.warn(`Failed to parse ${relativePath}`);
121
108
  return null;
122
109
  }
123
110
  }
@@ -144,7 +131,7 @@ export class ASTAnalyzer {
144
131
  ],
145
132
  });
146
133
  }
147
- determineFileType(relativePath, content, patterns) {
134
+ determineFileType(relativePath, patterns) {
148
135
  // Use pattern analysis to determine file type dynamically
149
136
  if (patterns.endpoints.length > 0) {
150
137
  return 'route';
@@ -0,0 +1,7 @@
1
+ import type { ProjectInfo, FrameworkInfo } from '../types.js';
2
+ export declare class FrameworkDetector {
3
+ private readonly frameworks;
4
+ detect(projectInfo: ProjectInfo): Promise<FrameworkInfo[]>;
5
+ private shouldDetect;
6
+ private buildFrameworkInfo;
7
+ }
@@ -0,0 +1,165 @@
1
+ export class FrameworkDetector {
2
+ frameworks = [
3
+ // Backend frameworks
4
+ {
5
+ name: 'nestjs',
6
+ detectionKeys: ['@nestjs/core', '@nestjs/common'],
7
+ versionKey: '@nestjs/core',
8
+ builtInFeatures: ['decorators', 'dependency-injection', 'modules'],
9
+ featureMap: {
10
+ '@nestjs/typeorm': 'orm',
11
+ '@nestjs/mongoose': 'orm',
12
+ '@nestjs/passport': 'authentication',
13
+ '@nestjs/jwt': 'jwt',
14
+ '@nestjs/swagger': 'swagger',
15
+ '@nestjs/graphql': 'graphql',
16
+ '@nestjs/websockets': 'websockets',
17
+ '@nestjs/microservices': 'microservices',
18
+ '@nestjs/testing': 'testing',
19
+ },
20
+ },
21
+ {
22
+ name: 'fastify',
23
+ detectionKeys: ['fastify'],
24
+ versionKey: 'fastify',
25
+ featureMap: {
26
+ '@fastify/cors': 'cors',
27
+ '@fastify/helmet': 'security',
28
+ '@fastify/rate-limit': 'rate-limiting',
29
+ '@fastify/multipart': 'file-upload',
30
+ '@fastify/static': 'static-files',
31
+ '@fastify/jwt': 'jwt',
32
+ '@fastify/session': 'sessions',
33
+ },
34
+ },
35
+ {
36
+ name: 'express',
37
+ detectionKeys: ['express'],
38
+ versionKey: 'express',
39
+ featureMap: {
40
+ 'express-session': 'sessions',
41
+ passport: 'authentication',
42
+ 'express-rate-limit': 'rate-limiting',
43
+ helmet: 'security',
44
+ cors: 'cors',
45
+ 'body-parser': 'body-parsing',
46
+ 'express-validator': 'validation',
47
+ multer: 'file-upload',
48
+ 'express-static': 'static-files',
49
+ },
50
+ },
51
+ // Fullstack frameworks
52
+ {
53
+ name: 'next.js',
54
+ detectionKeys: ['next'],
55
+ versionKey: 'next',
56
+ builtInFeatures: ['ssr', 'routing', 'api-routes', 'file-based-routing'],
57
+ featureMap: {
58
+ 'next-auth': 'authentication',
59
+ '@vercel/analytics': 'analytics',
60
+ },
61
+ },
62
+ {
63
+ name: 'nuxt.js',
64
+ detectionKeys: ['nuxt'],
65
+ versionKey: 'nuxt',
66
+ builtInFeatures: ['ssr', 'routing', 'api-routes', 'file-based-routing', 'auto-imports'],
67
+ featureMap: {
68
+ '@nuxt/content': 'content-management',
69
+ '@nuxtjs/auth': 'authentication',
70
+ '@nuxtjs/auth-next': 'authentication',
71
+ '@pinia/nuxt': 'state-management-pinia',
72
+ '@nuxt/image': 'image-optimization',
73
+ '@nuxtjs/tailwindcss': 'tailwind',
74
+ },
75
+ },
76
+ // Frontend frameworks
77
+ {
78
+ name: 'react',
79
+ detectionKeys: ['react', 'react-dom'],
80
+ versionKey: 'react',
81
+ featureMap: {
82
+ 'react-router': 'routing',
83
+ 'react-router-dom': 'routing',
84
+ redux: 'state-management-redux',
85
+ '@reduxjs/toolkit': 'state-management-redux',
86
+ zustand: 'state-management-zustand',
87
+ 'react-query': 'data-fetching',
88
+ '@tanstack/react-query': 'data-fetching',
89
+ '@testing-library/react': 'testing',
90
+ },
91
+ },
92
+ {
93
+ name: 'vue',
94
+ detectionKeys: (deps) => !!deps['vue'] || Object.keys(deps).some((dep) => dep.startsWith('@vue/')),
95
+ versionKey: 'vue',
96
+ featureMap: {
97
+ 'vue-router': 'routing',
98
+ pinia: 'state-management-pinia',
99
+ vuex: 'state-management-vuex',
100
+ '@vue/test-utils': 'testing',
101
+ },
102
+ },
103
+ {
104
+ name: 'angular',
105
+ detectionKeys: ['@angular/core'],
106
+ versionKey: '@angular/core',
107
+ builtInFeatures: ['decorators', 'dependency-injection', 'typescript'],
108
+ featureMap: {
109
+ '@angular/router': 'routing',
110
+ '@angular/forms': 'forms',
111
+ '@angular/common/http': 'http-client',
112
+ '@angular/common': 'http-client',
113
+ '@ngrx/store': 'state-management-ngrx',
114
+ '@angular/material': 'material-design',
115
+ '@angular/animations': 'animations',
116
+ },
117
+ },
118
+ {
119
+ name: 'svelte',
120
+ detectionKeys: (deps) => !!deps['svelte'] || Object.keys(deps).some((dep) => dep.startsWith('@sveltejs/')),
121
+ versionKey: 'svelte',
122
+ featureMap: {
123
+ '@sveltejs/kit': 'sveltekit',
124
+ '@sveltejs/adapter-auto': 'sveltekit',
125
+ 'svelte-routing': 'routing',
126
+ '@testing-library/svelte': 'testing',
127
+ },
128
+ },
129
+ ];
130
+ async detect(projectInfo) {
131
+ // Only check dependencies (not devDependencies) for framework detection
132
+ // This prevents false positives from libraries that have frameworks in devDependencies for testing
133
+ const deps = projectInfo.dependencies || {};
134
+ const detectedFrameworks = [];
135
+ for (const config of this.frameworks) {
136
+ if (this.shouldDetect(config, deps)) {
137
+ detectedFrameworks.push(this.buildFrameworkInfo(config, deps));
138
+ }
139
+ }
140
+ return detectedFrameworks;
141
+ }
142
+ shouldDetect(config, deps) {
143
+ if (typeof config.detectionKeys === 'function') {
144
+ return config.detectionKeys(deps);
145
+ }
146
+ return config.detectionKeys.some((key) => deps[key]);
147
+ }
148
+ buildFrameworkInfo(config, deps) {
149
+ const features = [...(config.builtInFeatures || [])];
150
+ // Detect features based on feature map
151
+ for (const [depKey, feature] of Object.entries(config.featureMap)) {
152
+ if (deps[depKey]) {
153
+ // Avoid duplicates
154
+ if (!features.includes(feature)) {
155
+ features.push(feature);
156
+ }
157
+ }
158
+ }
159
+ return {
160
+ name: config.name,
161
+ version: deps[config.versionKey],
162
+ features,
163
+ };
164
+ }
165
+ }
@@ -1,5 +1,4 @@
1
1
  import * as t from '@babel/types';
2
- import type { ParsemeConfig } from '../config.js';
3
2
  import type { ComponentInfo, RouteInfo } from '../types.js';
4
3
  export interface PatternAnalysis {
5
4
  endpoints: EndpointInfo[];
@@ -11,8 +10,6 @@ export interface PatternAnalysis {
11
10
  utilities: UtilityInfo[];
12
11
  }
13
12
  export interface EndpointInfo extends RouteInfo {
14
- type: 'rest' | 'graphql' | 'websocket' | 'rpc' | 'unknown';
15
- framework?: string;
16
13
  decorator?: string;
17
14
  }
18
15
  export interface ServiceInfo {
@@ -51,8 +48,8 @@ export interface UtilityInfo {
51
48
  type: 'helper' | 'lib' | 'hook' | 'composable';
52
49
  }
53
50
  export declare class PatternDetector {
54
- private readonly config;
55
- constructor(config: ParsemeConfig);
56
51
  analyzePatterns(ast: t.File, filePath: string, _content: string): PatternAnalysis;
57
52
  private hasJSXReturn;
53
+ private extractNextJSRoutePath;
54
+ private extractNuxtRoutePath;
58
55
  }
@@ -1,10 +1,6 @@
1
1
  import traverse from '@babel/traverse';
2
2
  import * as t from '@babel/types';
3
3
  export class PatternDetector {
4
- config;
5
- constructor(config) {
6
- this.config = config;
7
- }
8
4
  analyzePatterns(ast, filePath, _content) {
9
5
  const analysis = {
10
6
  endpoints: [],
@@ -15,27 +11,8 @@ export class PatternDetector {
15
11
  middleware: [],
16
12
  utilities: [],
17
13
  };
18
- // Use a single traverse call to detect all patterns
14
+ // Analyze patterns in the AST
19
15
  traverse.default(ast, {
20
- // Detect Express-style routes: app.get(), router.post(), etc.
21
- CallExpression: (path) => {
22
- if (t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.property)) {
23
- const methodName = path.node.callee.property.name;
24
- const httpMethods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'all'];
25
- if (httpMethods.includes(methodName)) {
26
- const routeArg = path.node.arguments[0];
27
- const routePath = t.isStringLiteral(routeArg) ? routeArg.value : '/';
28
- analysis.endpoints.push({
29
- method: methodName.toUpperCase(),
30
- path: routePath,
31
- handler: 'callback',
32
- file: filePath,
33
- line: path.node.loc?.start.line || 0,
34
- type: 'rest',
35
- });
36
- }
37
- }
38
- },
39
16
  // Detect decorator-based routes: @Get(), @Post(), etc.
40
17
  ClassMethod: (path) => {
41
18
  const decorators = path.node.decorators;
@@ -54,7 +31,6 @@ export class PatternDetector {
54
31
  handler: t.isIdentifier(path.node.key) ? path.node.key.name : 'anonymous',
55
32
  file: filePath,
56
33
  line: path.node.loc?.start.line || 0,
57
- type: 'rest',
58
34
  decorator: callee.name,
59
35
  });
60
36
  }
@@ -63,6 +39,107 @@ export class PatternDetector {
63
39
  });
64
40
  }
65
41
  },
42
+ // Detect Express/Fastify-style routes: app.get(), router.post(), fastify.get(), etc.
43
+ CallExpression: (path) => {
44
+ const { callee, arguments: args } = path.node;
45
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
46
+ const httpMethods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head'];
47
+ const methodName = callee.property.name;
48
+ if (httpMethods.includes(methodName) && args.length >= 2) {
49
+ const routePath = args[0];
50
+ // Only detect if:
51
+ // 1. First argument is a string literal (route path)
52
+ // 2. Object is a known route handler name
53
+ if (t.isStringLiteral(routePath) && t.isIdentifier(callee.object)) {
54
+ const objectName = callee.object.name;
55
+ const routeObjectNames = [
56
+ 'app',
57
+ 'router',
58
+ 'server',
59
+ 'fastify',
60
+ 'express',
61
+ 'route',
62
+ 'api',
63
+ ];
64
+ // Only detect if it's a common route object name
65
+ // This filters out axios.get(), client.get(), etc.
66
+ if (routeObjectNames.includes(objectName.toLowerCase())) {
67
+ analysis.endpoints.push({
68
+ method: methodName.toUpperCase(),
69
+ path: routePath.value,
70
+ handler: 'anonymous',
71
+ file: filePath,
72
+ line: path.node.loc?.start.line || 0,
73
+ });
74
+ }
75
+ }
76
+ }
77
+ }
78
+ // Detect Nuxt.js server routes: defineEventHandler()
79
+ if (t.isIdentifier(callee) && callee.name === 'defineEventHandler') {
80
+ // Extract route path from file path
81
+ // Nuxt server routes are in server/api/ or server/routes/
82
+ const routePath = this.extractNuxtRoutePath(filePath);
83
+ analysis.endpoints.push({
84
+ method: 'GET/POST',
85
+ path: routePath,
86
+ handler: 'defineEventHandler',
87
+ file: filePath,
88
+ line: path.node.loc?.start.line || 0,
89
+ });
90
+ }
91
+ },
92
+ // Detect TypeScript interfaces and type aliases
93
+ TSInterfaceDeclaration: (path) => {
94
+ const interfaceName = path.node.id.name;
95
+ const fields = path.node.body.body
96
+ .filter((member) => t.isTSPropertySignature(member))
97
+ .map((member) => {
98
+ if (t.isIdentifier(member.key)) {
99
+ return member.key.name;
100
+ }
101
+ return '';
102
+ })
103
+ .filter((name) => name !== '');
104
+ analysis.models.push({
105
+ name: interfaceName,
106
+ file: filePath,
107
+ line: path.node.loc?.start.line || 0,
108
+ fields,
109
+ type: 'interface',
110
+ });
111
+ },
112
+ TSTypeAliasDeclaration: (path) => {
113
+ const typeName = path.node.id.name;
114
+ analysis.models.push({
115
+ name: typeName,
116
+ file: filePath,
117
+ line: path.node.loc?.start.line || 0,
118
+ fields: [],
119
+ type: 'type',
120
+ });
121
+ },
122
+ // Detect Next.js API route handlers: export function GET/POST/etc.
123
+ ExportNamedDeclaration: (path) => {
124
+ const declaration = path.node.declaration;
125
+ if (t.isFunctionDeclaration(declaration) && declaration.id) {
126
+ const functionName = declaration.id.name;
127
+ const httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'];
128
+ // Only detect if it's an HTTP method name and in an api directory
129
+ if (httpMethods.includes(functionName) && /\/api\//.test(filePath)) {
130
+ // Extract route path from file path
131
+ // Next.js API routes are in app/api/ or pages/api/
132
+ const routePath = this.extractNextJSRoutePath(filePath);
133
+ analysis.endpoints.push({
134
+ method: functionName,
135
+ path: routePath,
136
+ handler: functionName,
137
+ file: filePath,
138
+ line: declaration.loc?.start.line || 0,
139
+ });
140
+ }
141
+ }
142
+ },
66
143
  // Detect React components
67
144
  FunctionDeclaration: (path) => {
68
145
  const functionName = path.node.id?.name;
@@ -141,34 +218,42 @@ export class PatternDetector {
141
218
  });
142
219
  }
143
220
  },
144
- // Detect TypeScript interfaces
145
- TSInterfaceDeclaration: (path) => {
146
- const interfaceName = path.node.id.name;
147
- const fields = path.node.body.body
148
- .filter((member) => t.isTSPropertySignature(member) && t.isIdentifier(member.key))
149
- .map((member) => member.key.name);
150
- analysis.models.push({
151
- name: interfaceName,
152
- file: filePath,
153
- line: path.node.loc?.start.line || 0,
154
- fields,
155
- type: 'interface',
156
- });
157
- },
158
- // Detect type aliases
159
- TSTypeAliasDeclaration: (path) => {
160
- analysis.models.push({
161
- name: path.node.id.name,
162
- file: filePath,
163
- line: path.node.loc?.start.line || 0,
164
- fields: [],
165
- type: 'type',
166
- });
167
- },
168
221
  });
169
222
  return analysis;
170
223
  }
171
224
  hasJSXReturn(body) {
172
225
  return body.body.some((stmt) => t.isReturnStatement(stmt) && stmt.argument && t.isJSXElement(stmt.argument));
173
226
  }
227
+ extractNextJSRoutePath(filePath) {
228
+ // Next.js API routes can be in:
229
+ // - app/api/[route]/route.ts (App Router)
230
+ // - pages/api/[route].ts (Pages Router)
231
+ // Try App Router pattern first
232
+ let match = filePath.match(/\/app\/api\/(.+)\/route\.[jt]sx?$/);
233
+ if (match) {
234
+ return `/api/${match[1]}`;
235
+ }
236
+ // Try Pages Router pattern
237
+ match = filePath.match(/\/pages\/api\/(.+)\.[jt]sx?$/);
238
+ if (match) {
239
+ return `/api/${match[1]}`;
240
+ }
241
+ // Fallback
242
+ return '/api/unknown';
243
+ }
244
+ extractNuxtRoutePath(filePath) {
245
+ // Nuxt.js server routes can be in:
246
+ // - server/api/[route].ts
247
+ // - server/routes/[route].ts
248
+ let match = filePath.match(/\/server\/api\/(.+)\.[jt]s$/);
249
+ if (match) {
250
+ return `/api/${match[1]}`;
251
+ }
252
+ match = filePath.match(/\/server\/routes\/(.+)\.[jt]s$/);
253
+ if (match) {
254
+ return `/${match[1]}`;
255
+ }
256
+ // Fallback
257
+ return '/api/unknown';
258
+ }
174
259
  }
@@ -2,6 +2,7 @@ import type { ParsemeConfig } from '../config.js';
2
2
  import type { ProjectInfo } from '../types.js';
3
3
  export declare class ProjectAnalyzer {
4
4
  private readonly config;
5
+ private readonly fileCollector;
5
6
  constructor(config: ParsemeConfig);
6
7
  analyze(rootDir: string): Promise<ProjectInfo>;
7
8
  private detectProjectType;
@@ -11,4 +12,5 @@ export declare class ProjectAnalyzer {
11
12
  private detectEntryPoints;
12
13
  private detectOutputTargets;
13
14
  private hasAppDependencies;
15
+ getAllProjectFiles(rootDir: string): Promise<string[]>;
14
16
  }