@parseme/cli 0.0.4 → 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.
@@ -1,175 +1,164 @@
1
1
  export class FrameworkDetector {
2
- async detect(projectInfo) {
3
- const deps = { ...projectInfo.dependencies, ...projectInfo.devDependencies };
4
- // Check for frameworks in dependencies
5
- if (deps['@nestjs/core'] || deps['@nestjs/common']) {
6
- return this.detectNestJS(deps);
7
- }
8
- if (deps['fastify']) {
9
- return this.detectFastify(deps);
10
- }
11
- if (deps['express']) {
12
- return this.detectExpress(deps);
13
- }
14
- if (deps['koa']) {
15
- return this.detectKoa(deps);
16
- }
17
- if (deps['@hapi/hapi']) {
18
- return this.detectHapi(deps);
19
- }
20
- return {
21
- name: 'unknown',
22
- features: [],
23
- };
24
- }
25
- detectExpress(deps) {
26
- const features = [];
27
- // Detect common Express features
28
- if (deps['express-session']) {
29
- features.push('sessions');
30
- }
31
- if (deps['passport']) {
32
- features.push('authentication');
33
- }
34
- if (deps['express-rate-limit']) {
35
- features.push('rate-limiting');
36
- }
37
- if (deps['helmet']) {
38
- features.push('security');
39
- }
40
- if (deps['cors']) {
41
- features.push('cors');
42
- }
43
- if (deps['body-parser']) {
44
- features.push('body-parsing');
45
- }
46
- if (deps['express-validator']) {
47
- features.push('validation');
48
- }
49
- if (deps['multer']) {
50
- features.push('file-upload');
51
- }
52
- if (deps['express-static']) {
53
- features.push('static-files');
54
- }
55
- return {
56
- name: 'express',
57
- version: deps['express'],
58
- features,
59
- };
60
- }
61
- detectFastify(deps) {
62
- const features = [];
63
- if (deps['@fastify/cors']) {
64
- features.push('cors');
65
- }
66
- if (deps['@fastify/helmet']) {
67
- features.push('security');
68
- }
69
- if (deps['@fastify/rate-limit']) {
70
- features.push('rate-limiting');
71
- }
72
- if (deps['@fastify/multipart']) {
73
- features.push('file-upload');
74
- }
75
- if (deps['@fastify/static']) {
76
- features.push('static-files');
77
- }
78
- if (deps['@fastify/jwt']) {
79
- features.push('jwt');
80
- }
81
- if (deps['@fastify/session']) {
82
- features.push('sessions');
83
- }
84
- return {
85
- name: 'fastify',
86
- version: deps['fastify'],
87
- features,
88
- };
89
- }
90
- detectNestJS(deps) {
91
- const features = [];
92
- if (deps['@nestjs/typeorm'] || deps['@nestjs/mongoose']) {
93
- features.push('orm');
94
- }
95
- if (deps['@nestjs/passport']) {
96
- features.push('authentication');
97
- }
98
- if (deps['@nestjs/jwt']) {
99
- features.push('jwt');
100
- }
101
- if (deps['@nestjs/swagger']) {
102
- features.push('swagger');
103
- }
104
- if (deps['@nestjs/graphql']) {
105
- features.push('graphql');
106
- }
107
- if (deps['@nestjs/websockets']) {
108
- features.push('websockets');
109
- }
110
- if (deps['@nestjs/microservices']) {
111
- features.push('microservices');
112
- }
113
- if (deps['@nestjs/testing']) {
114
- features.push('testing');
115
- }
116
- // NestJS uses decorators by default
117
- features.push('decorators', 'dependency-injection', 'modules');
118
- return {
2
+ frameworks = [
3
+ // Backend frameworks
4
+ {
119
5
  name: 'nestjs',
120
- version: deps['@nestjs/core'],
121
- features,
122
- };
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;
123
141
  }
124
- detectKoa(deps) {
125
- const features = [];
126
- if (deps['@koa/cors']) {
127
- features.push('cors');
128
- }
129
- if (deps['koa-helmet']) {
130
- features.push('security');
131
- }
132
- if (deps['koa-ratelimit']) {
133
- features.push('rate-limiting');
134
- }
135
- if (deps['koa-multer']) {
136
- features.push('file-upload');
137
- }
138
- if (deps['koa-static']) {
139
- features.push('static-files');
140
- }
141
- if (deps['koa-session']) {
142
- features.push('sessions');
143
- }
144
- if (deps['koa-bodyparser']) {
145
- features.push('body-parsing');
142
+ shouldDetect(config, deps) {
143
+ if (typeof config.detectionKeys === 'function') {
144
+ return config.detectionKeys(deps);
146
145
  }
147
- return {
148
- name: 'koa',
149
- version: deps['koa'],
150
- features,
151
- };
146
+ return config.detectionKeys.some((key) => deps[key]);
152
147
  }
153
- detectHapi(deps) {
154
- const features = [];
155
- if (deps['@hapi/cookie']) {
156
- features.push('sessions');
157
- }
158
- if (deps['@hapi/jwt']) {
159
- features.push('jwt');
160
- }
161
- if (deps['@hapi/inert']) {
162
- features.push('static-files');
163
- }
164
- if (deps['@hapi/vision']) {
165
- features.push('templates');
166
- }
167
- if (deps['@hapi/boom']) {
168
- features.push('error-handling');
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
+ }
169
158
  }
170
159
  return {
171
- name: 'hapi',
172
- version: deps['@hapi/hapi'],
160
+ name: config.name,
161
+ version: deps[config.versionKey],
173
162
  features,
174
163
  };
175
164
  }
@@ -10,8 +10,6 @@ export interface PatternAnalysis {
10
10
  utilities: UtilityInfo[];
11
11
  }
12
12
  export interface EndpointInfo extends RouteInfo {
13
- type: 'rest' | 'graphql' | 'websocket' | 'rpc' | 'unknown';
14
- framework?: string;
15
13
  decorator?: string;
16
14
  }
17
15
  export interface ServiceInfo {
@@ -52,4 +50,6 @@ export interface UtilityInfo {
52
50
  export declare class PatternDetector {
53
51
  analyzePatterns(ast: t.File, filePath: string, _content: string): PatternAnalysis;
54
52
  private hasJSXReturn;
53
+ private extractNextJSRoutePath;
54
+ private extractNuxtRoutePath;
55
55
  }
@@ -11,7 +11,7 @@ export class PatternDetector {
11
11
  middleware: [],
12
12
  utilities: [],
13
13
  };
14
- // Use a single traverse call to detect all patterns
14
+ // Analyze patterns in the AST
15
15
  traverse.default(ast, {
16
16
  // Detect decorator-based routes: @Get(), @Post(), etc.
17
17
  ClassMethod: (path) => {
@@ -31,7 +31,6 @@ export class PatternDetector {
31
31
  handler: t.isIdentifier(path.node.key) ? path.node.key.name : 'anonymous',
32
32
  file: filePath,
33
33
  line: path.node.loc?.start.line || 0,
34
- type: 'rest',
35
34
  decorator: callee.name,
36
35
  });
37
36
  }
@@ -40,7 +39,7 @@ export class PatternDetector {
40
39
  });
41
40
  }
42
41
  },
43
- // Detect Express/Router-style routes: app.get(), router.post(), etc.
42
+ // Detect Express/Fastify-style routes: app.get(), router.post(), fastify.get(), etc.
44
43
  CallExpression: (path) => {
45
44
  const { callee, arguments: args } = path.node;
46
45
  if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
@@ -48,19 +47,47 @@ export class PatternDetector {
48
47
  const methodName = callee.property.name;
49
48
  if (httpMethods.includes(methodName) && args.length >= 2) {
50
49
  const routePath = args[0];
51
- if (t.isStringLiteral(routePath)) {
52
- analysis.endpoints.push({
53
- method: methodName.toUpperCase(),
54
- path: routePath.value,
55
- handler: 'anonymous',
56
- file: filePath,
57
- line: path.node.loc?.start.line || 0,
58
- type: 'rest',
59
- framework: 'express',
60
- });
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
+ }
61
75
  }
62
76
  }
63
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
+ }
64
91
  },
65
92
  // Detect TypeScript interfaces and type aliases
66
93
  TSInterfaceDeclaration: (path) => {
@@ -92,6 +119,27 @@ export class PatternDetector {
92
119
  type: 'type',
93
120
  });
94
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
+ },
95
143
  // Detect React components
96
144
  FunctionDeclaration: (path) => {
97
145
  const functionName = path.node.id?.name;
@@ -176,4 +224,36 @@ export class PatternDetector {
176
224
  hasJSXReturn(body) {
177
225
  return body.body.some((stmt) => t.isReturnStatement(stmt) && stmt.argument && t.isJSXElement(stmt.argument));
178
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
+ }
179
259
  }
@@ -2,7 +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 ig;
5
+ private readonly fileCollector;
6
6
  constructor(config: ParsemeConfig);
7
7
  analyze(rootDir: string): Promise<ProjectInfo>;
8
8
  private detectProjectType;
@@ -1,14 +1,12 @@
1
1
  import { readFile, access, readdir, stat } from 'fs/promises';
2
- import { join, basename, relative } from 'path';
3
- import ignore from 'ignore';
2
+ import { join, basename } from 'path';
3
+ import { FileCollector } from '../../utils/file-collector.js';
4
4
  export class ProjectAnalyzer {
5
5
  config;
6
- ig;
6
+ fileCollector;
7
7
  constructor(config) {
8
8
  this.config = config;
9
- this.ig = ignore();
10
- const configData = this.config.get();
11
- this.ig.add(configData.excludePatterns || []);
9
+ this.fileCollector = new FileCollector(config);
12
10
  }
13
11
  async analyze(rootDir) {
14
12
  const packageJsonPath = join(rootDir, 'package.json');
@@ -88,11 +86,6 @@ export class ProjectAnalyzer {
88
86
  const files = [];
89
87
  for (const entry of entries) {
90
88
  const fullPath = join(dir, entry);
91
- const relativePath = relative(rootDir, fullPath);
92
- // Skip if ignored by exclude patterns
93
- if (this.ig.ignores(relativePath)) {
94
- continue;
95
- }
96
89
  const stats = await stat(fullPath);
97
90
  if (stats.isFile()) {
98
91
  files.push(fullPath);
@@ -142,11 +135,7 @@ export class ProjectAnalyzer {
142
135
  return 'frontend-web';
143
136
  }
144
137
  // Check for backend frameworks
145
- if (deps['express'] ||
146
- deps['fastify'] ||
147
- deps['@nestjs/core'] ||
148
- deps['koa'] ||
149
- deps['@hapi/hapi']) {
138
+ if (deps['express'] || deps['fastify'] || deps['@nestjs/core']) {
150
139
  return 'backend-api';
151
140
  }
152
141
  // Check if it's a library (has main/module/exports but no app dependencies)
@@ -213,7 +202,7 @@ export class ProjectAnalyzer {
213
202
  return appIndicators.some((indicator) => deps[indicator]);
214
203
  }
215
204
  async getAllProjectFiles(rootDir) {
216
- const allFiles = await this.getFilesRecursive(rootDir, rootDir, Infinity);
217
- return allFiles.map((file) => relative(rootDir, file));
205
+ const result = await this.fileCollector.getAllProjectFiles(rootDir);
206
+ return result.files;
218
207
  }
219
208
  }
@@ -16,5 +16,4 @@ export declare class ParsemeConfig {
16
16
  private generateJSConfig;
17
17
  private generateTSConfig;
18
18
  private mergeExcludePatterns;
19
- private readGitignorePatterns;
20
19
  }
@@ -1,4 +1,4 @@
1
- import { readFileSync, existsSync } from 'fs';
1
+ import { existsSync } from 'fs';
2
2
  import { readFile, writeFile } from 'fs/promises';
3
3
  import { join, extname } from 'path';
4
4
  export class ParsemeConfig {
@@ -123,6 +123,7 @@ export class ParsemeConfig {
123
123
  analyzeFileTypes: fileTypes,
124
124
  // Git
125
125
  includeGitInfo: config.includeGitInfo ?? true,
126
+ useGitForFiles: config.useGitForFiles ?? true,
126
127
  // Sections
127
128
  sections: {
128
129
  overview: true,
@@ -143,10 +144,7 @@ export class ParsemeConfig {
143
144
  },
144
145
  // Size limits
145
146
  limits: {
146
- maxLinesPerFile: config.limits?.maxLinesPerFile ?? 1000,
147
- maxCharsPerFile: config.limits?.maxCharsPerFile ?? 50000, // ~15k tokens
148
- maxFilesPerContext: config.limits?.maxFilesPerContext ?? 20,
149
- truncateStrategy: config.limits?.truncateStrategy ?? 'truncate',
147
+ maxFilesPerContext: config.limits?.maxFilesPerContext ?? 5000,
150
148
  },
151
149
  };
152
150
  }
@@ -187,30 +185,8 @@ const config: ParsemeConfigFile = ${JSON.stringify(this.userConfig, null, 2)};
187
185
  export default config;
188
186
  `;
189
187
  }
190
- mergeExcludePatterns(configPatterns, rootDir) {
191
- // Always read .gitignore patterns
192
- const gitignorePatterns = this.readGitignorePatterns(rootDir);
193
- // Merge gitignore patterns with config patterns, they are added to gitignore patterns, not replacing them
194
- const excludePatterns = [...gitignorePatterns];
195
- if (configPatterns) {
196
- excludePatterns.push(...configPatterns);
197
- }
198
- return excludePatterns;
199
- }
200
- readGitignorePatterns(rootDir) {
201
- try {
202
- const gitignorePath = join(rootDir, '.gitignore');
203
- if (!existsSync(gitignorePath)) {
204
- return [];
205
- }
206
- const gitignoreContent = readFileSync(gitignorePath, 'utf-8');
207
- return gitignoreContent
208
- .split('\n')
209
- .map((line) => line.trim())
210
- .filter((line) => line && !line.startsWith('#'));
211
- }
212
- catch {
213
- return [];
214
- }
188
+ mergeExcludePatterns(configPatterns, _rootDir) {
189
+ // Only use config patterns - git ignore is now handled by FileFilterService
190
+ return configPatterns || [];
215
191
  }
216
192
  }
@@ -12,13 +12,11 @@ interface BuildContext {
12
12
  export declare class ContextBuilder {
13
13
  private readonly config;
14
14
  constructor(config: ParsemeConfig);
15
- private truncateContent;
16
- private splitContentByLines;
17
- private splitContentByChars;
18
15
  build(context: BuildContext): ContextOutput;
19
16
  private buildMultiFile;
20
17
  private buildHeader;
21
18
  private buildProjectOverview;
19
+ private formatFrameworksList;
22
20
  private buildGitSection;
23
21
  private buildSummarySection;
24
22
  private buildFilesList;