@parseme/cli 0.0.2

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.
@@ -0,0 +1,180 @@
1
+ export class FrameworkDetector {
2
+ config;
3
+ constructor(config) {
4
+ this.config = config;
5
+ }
6
+ async detect(projectInfo) {
7
+ const deps = { ...projectInfo.dependencies, ...projectInfo.devDependencies };
8
+ // Check for frameworks in dependencies
9
+ if (deps['@nestjs/core'] || deps['@nestjs/common']) {
10
+ return this.detectNestJS(deps);
11
+ }
12
+ if (deps['fastify']) {
13
+ return this.detectFastify(deps);
14
+ }
15
+ if (deps['express']) {
16
+ return this.detectExpress(deps);
17
+ }
18
+ if (deps['koa']) {
19
+ return this.detectKoa(deps);
20
+ }
21
+ if (deps['@hapi/hapi']) {
22
+ return this.detectHapi(deps);
23
+ }
24
+ return {
25
+ name: 'unknown',
26
+ features: [],
27
+ };
28
+ }
29
+ detectExpress(deps) {
30
+ const features = [];
31
+ // Detect common Express features
32
+ if (deps['express-session']) {
33
+ features.push('sessions');
34
+ }
35
+ if (deps['passport']) {
36
+ features.push('authentication');
37
+ }
38
+ if (deps['express-rate-limit']) {
39
+ features.push('rate-limiting');
40
+ }
41
+ if (deps['helmet']) {
42
+ features.push('security');
43
+ }
44
+ if (deps['cors']) {
45
+ features.push('cors');
46
+ }
47
+ if (deps['body-parser']) {
48
+ features.push('body-parsing');
49
+ }
50
+ if (deps['express-validator']) {
51
+ features.push('validation');
52
+ }
53
+ if (deps['multer']) {
54
+ features.push('file-upload');
55
+ }
56
+ if (deps['express-static']) {
57
+ features.push('static-files');
58
+ }
59
+ return {
60
+ name: 'express',
61
+ version: deps['express'],
62
+ features,
63
+ };
64
+ }
65
+ detectFastify(deps) {
66
+ const features = [];
67
+ if (deps['@fastify/cors']) {
68
+ features.push('cors');
69
+ }
70
+ if (deps['@fastify/helmet']) {
71
+ features.push('security');
72
+ }
73
+ if (deps['@fastify/rate-limit']) {
74
+ features.push('rate-limiting');
75
+ }
76
+ if (deps['@fastify/multipart']) {
77
+ features.push('file-upload');
78
+ }
79
+ if (deps['@fastify/static']) {
80
+ features.push('static-files');
81
+ }
82
+ if (deps['@fastify/jwt']) {
83
+ features.push('jwt');
84
+ }
85
+ if (deps['@fastify/session']) {
86
+ features.push('sessions');
87
+ }
88
+ return {
89
+ name: 'fastify',
90
+ version: deps['fastify'],
91
+ features,
92
+ };
93
+ }
94
+ detectNestJS(deps) {
95
+ const features = [];
96
+ if (deps['@nestjs/typeorm'] || deps['@nestjs/mongoose']) {
97
+ features.push('orm');
98
+ }
99
+ if (deps['@nestjs/passport']) {
100
+ features.push('authentication');
101
+ }
102
+ if (deps['@nestjs/jwt']) {
103
+ features.push('jwt');
104
+ }
105
+ if (deps['@nestjs/swagger']) {
106
+ features.push('swagger');
107
+ }
108
+ if (deps['@nestjs/graphql']) {
109
+ features.push('graphql');
110
+ }
111
+ if (deps['@nestjs/websockets']) {
112
+ features.push('websockets');
113
+ }
114
+ if (deps['@nestjs/microservices']) {
115
+ features.push('microservices');
116
+ }
117
+ if (deps['@nestjs/testing']) {
118
+ features.push('testing');
119
+ }
120
+ // NestJS uses decorators by default
121
+ features.push('decorators', 'dependency-injection', 'modules');
122
+ return {
123
+ name: 'nestjs',
124
+ version: deps['@nestjs/core'],
125
+ features,
126
+ };
127
+ }
128
+ detectKoa(deps) {
129
+ const features = [];
130
+ if (deps['@koa/cors']) {
131
+ features.push('cors');
132
+ }
133
+ if (deps['koa-helmet']) {
134
+ features.push('security');
135
+ }
136
+ if (deps['koa-ratelimit']) {
137
+ features.push('rate-limiting');
138
+ }
139
+ if (deps['koa-multer']) {
140
+ features.push('file-upload');
141
+ }
142
+ if (deps['koa-static']) {
143
+ features.push('static-files');
144
+ }
145
+ if (deps['koa-session']) {
146
+ features.push('sessions');
147
+ }
148
+ if (deps['koa-bodyparser']) {
149
+ features.push('body-parsing');
150
+ }
151
+ return {
152
+ name: 'koa',
153
+ version: deps['koa'],
154
+ features,
155
+ };
156
+ }
157
+ detectHapi(deps) {
158
+ const features = [];
159
+ if (deps['@hapi/cookie']) {
160
+ features.push('sessions');
161
+ }
162
+ if (deps['@hapi/jwt']) {
163
+ features.push('jwt');
164
+ }
165
+ if (deps['@hapi/inert']) {
166
+ features.push('static-files');
167
+ }
168
+ if (deps['@hapi/vision']) {
169
+ features.push('templates');
170
+ }
171
+ if (deps['@hapi/boom']) {
172
+ features.push('error-handling');
173
+ }
174
+ return {
175
+ name: 'hapi',
176
+ version: deps['@hapi/hapi'],
177
+ features,
178
+ };
179
+ }
180
+ }
@@ -0,0 +1,13 @@
1
+ import type { ParsemeConfig } from '../config.js';
2
+ import type { GitInfo } from '../types.js';
3
+ export declare class GitAnalyzer {
4
+ private readonly config;
5
+ constructor(config: ParsemeConfig);
6
+ analyze(rootDir: string): Promise<GitInfo | undefined>;
7
+ private getCurrentBranch;
8
+ private getLastCommit;
9
+ private getStatus;
10
+ private getChangedFiles;
11
+ getFileLastModified(filePath: string, rootDir: string): Promise<string | undefined>;
12
+ getFileHistory(filePath: string, rootDir: string, limit?: number): Promise<string[]>;
13
+ }
@@ -0,0 +1,92 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ const execAsync = promisify(exec);
4
+ export class GitAnalyzer {
5
+ config;
6
+ constructor(config) {
7
+ this.config = config;
8
+ }
9
+ async analyze(rootDir) {
10
+ try {
11
+ // Check if this is a git repository
12
+ await execAsync('git rev-parse --git-dir', { cwd: rootDir });
13
+ const [branch, lastCommit, status, changedFiles] = await Promise.all([
14
+ this.getCurrentBranch(rootDir),
15
+ this.getLastCommit(rootDir),
16
+ this.getStatus(rootDir),
17
+ this.getChangedFiles(rootDir),
18
+ ]);
19
+ return {
20
+ branch,
21
+ lastCommit,
22
+ status,
23
+ changedFiles,
24
+ };
25
+ }
26
+ catch {
27
+ // Not a git repository or git not available
28
+ return undefined;
29
+ }
30
+ }
31
+ async getCurrentBranch(rootDir) {
32
+ try {
33
+ const { stdout } = await execAsync('git branch --show-current', { cwd: rootDir });
34
+ return stdout.trim();
35
+ }
36
+ catch {
37
+ return 'unknown';
38
+ }
39
+ }
40
+ async getLastCommit(rootDir) {
41
+ try {
42
+ const { stdout } = await execAsync('git log -1 --format="%H %s"', { cwd: rootDir });
43
+ return stdout.trim();
44
+ }
45
+ catch {
46
+ return 'No commits';
47
+ }
48
+ }
49
+ async getStatus(rootDir) {
50
+ try {
51
+ const { stdout } = await execAsync('git status --porcelain', { cwd: rootDir });
52
+ return stdout.trim() ? 'dirty' : 'clean';
53
+ }
54
+ catch {
55
+ return 'clean';
56
+ }
57
+ }
58
+ async getChangedFiles(rootDir) {
59
+ try {
60
+ const { stdout } = await execAsync('git status --porcelain', { cwd: rootDir });
61
+ return stdout
62
+ .split('\n')
63
+ .filter((line) => line.trim())
64
+ .map((line) => line.substring(3)); // Remove status prefix
65
+ }
66
+ catch {
67
+ return [];
68
+ }
69
+ }
70
+ async getFileLastModified(filePath, rootDir) {
71
+ try {
72
+ const { stdout } = await execAsync(`git log -1 --format="%H %ai" -- "${filePath}"`, {
73
+ cwd: rootDir,
74
+ });
75
+ return stdout.trim();
76
+ }
77
+ catch {
78
+ return undefined;
79
+ }
80
+ }
81
+ async getFileHistory(filePath, rootDir, limit = 5) {
82
+ try {
83
+ const { stdout } = await execAsync(`git log --oneline -${limit} -- "${filePath}"`, {
84
+ cwd: rootDir,
85
+ });
86
+ return stdout.split('\n').filter((line) => line.trim());
87
+ }
88
+ catch {
89
+ return [];
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,58 @@
1
+ import * as t from '@babel/types';
2
+ import type { ParsemeConfig } from '../config.js';
3
+ import type { ComponentInfo, RouteInfo } from '../types.js';
4
+ export interface PatternAnalysis {
5
+ endpoints: EndpointInfo[];
6
+ components: ComponentInfo[];
7
+ services: ServiceInfo[];
8
+ models: ModelInfo[];
9
+ configs: ConfigInfo[];
10
+ middleware: MiddlewareInfo[];
11
+ utilities: UtilityInfo[];
12
+ }
13
+ export interface EndpointInfo extends RouteInfo {
14
+ type: 'rest' | 'graphql' | 'websocket' | 'rpc' | 'unknown';
15
+ framework?: string;
16
+ decorator?: string;
17
+ }
18
+ export interface ServiceInfo {
19
+ name: string;
20
+ file: string;
21
+ line: number;
22
+ methods: string[];
23
+ dependencies: string[];
24
+ type: 'class' | 'function' | 'object';
25
+ }
26
+ export interface ModelInfo {
27
+ name: string;
28
+ file: string;
29
+ line: number;
30
+ fields: string[];
31
+ type: 'interface' | 'type' | 'class' | 'schema';
32
+ }
33
+ export interface ConfigInfo {
34
+ name: string;
35
+ file: string;
36
+ line: number;
37
+ keys: string[];
38
+ type: 'object' | 'class' | 'env';
39
+ }
40
+ export interface MiddlewareInfo {
41
+ name: string;
42
+ file: string;
43
+ line: number;
44
+ type: 'function' | 'class' | 'decorator';
45
+ }
46
+ export interface UtilityInfo {
47
+ name: string;
48
+ file: string;
49
+ line: number;
50
+ functions: string[];
51
+ type: 'helper' | 'lib' | 'hook' | 'composable';
52
+ }
53
+ export declare class PatternDetector {
54
+ private readonly config;
55
+ constructor(config: ParsemeConfig);
56
+ analyzePatterns(ast: t.File, filePath: string, _content: string): PatternAnalysis;
57
+ private hasJSXReturn;
58
+ }
@@ -0,0 +1,174 @@
1
+ import traverse from '@babel/traverse';
2
+ import * as t from '@babel/types';
3
+ export class PatternDetector {
4
+ config;
5
+ constructor(config) {
6
+ this.config = config;
7
+ }
8
+ analyzePatterns(ast, filePath, _content) {
9
+ const analysis = {
10
+ endpoints: [],
11
+ components: [],
12
+ services: [],
13
+ models: [],
14
+ configs: [],
15
+ middleware: [],
16
+ utilities: [],
17
+ };
18
+ // Use a single traverse call to detect all patterns
19
+ 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
+ // Detect decorator-based routes: @Get(), @Post(), etc.
40
+ ClassMethod: (path) => {
41
+ const decorators = path.node.decorators;
42
+ if (decorators) {
43
+ decorators.forEach((decorator) => {
44
+ if (t.isDecorator(decorator) && t.isCallExpression(decorator.expression)) {
45
+ const callee = decorator.expression.callee;
46
+ if (t.isIdentifier(callee)) {
47
+ const httpMethods = ['Get', 'Post', 'Put', 'Delete', 'Patch', 'Options', 'Head'];
48
+ if (httpMethods.includes(callee.name)) {
49
+ const route = decorator.expression.arguments[0];
50
+ const routePath = t.isStringLiteral(route) ? route.value : '/';
51
+ analysis.endpoints.push({
52
+ method: callee.name.toUpperCase(),
53
+ path: routePath,
54
+ handler: t.isIdentifier(path.node.key) ? path.node.key.name : 'anonymous',
55
+ file: filePath,
56
+ line: path.node.loc?.start.line || 0,
57
+ type: 'rest',
58
+ decorator: callee.name,
59
+ });
60
+ }
61
+ }
62
+ }
63
+ });
64
+ }
65
+ },
66
+ // Detect React components
67
+ FunctionDeclaration: (path) => {
68
+ const functionName = path.node.id?.name;
69
+ if (functionName) {
70
+ // React hooks
71
+ if (functionName.startsWith('use') && functionName.length > 3) {
72
+ analysis.utilities.push({
73
+ name: functionName,
74
+ file: filePath,
75
+ line: path.node.loc?.start.line || 0,
76
+ functions: [functionName],
77
+ type: 'hook',
78
+ });
79
+ }
80
+ // React components (check for JSX return)
81
+ if (this.hasJSXReturn(path.node.body)) {
82
+ analysis.components.push({
83
+ name: functionName,
84
+ file: filePath,
85
+ line: path.node.loc?.start.line || 0,
86
+ });
87
+ }
88
+ // Middleware functions (req, res, next pattern)
89
+ const params = path.node.params;
90
+ if (params.length >= 3) {
91
+ const paramNames = params
92
+ .filter((param) => t.isIdentifier(param))
93
+ .map((param) => param.name);
94
+ const isMiddleware = paramNames.some((name) => ['req', 'request', 'res', 'response', 'next', 'ctx', 'context'].includes(name.toLowerCase()));
95
+ if (isMiddleware) {
96
+ analysis.middleware.push({
97
+ name: functionName,
98
+ file: filePath,
99
+ line: path.node.loc?.start.line || 0,
100
+ type: 'function',
101
+ });
102
+ }
103
+ }
104
+ }
105
+ },
106
+ // Detect service classes and components
107
+ ClassDeclaration: (path) => {
108
+ const className = path.node.id?.name;
109
+ if (!className) {
110
+ return;
111
+ }
112
+ // Service classes
113
+ const hasInjectableDecorator = path.node.decorators?.some((decorator) => t.isDecorator(decorator) &&
114
+ t.isCallExpression(decorator.expression) &&
115
+ t.isIdentifier(decorator.expression.callee) &&
116
+ decorator.expression.callee.name === 'Injectable');
117
+ const isServiceClass = className.endsWith('Service') ||
118
+ className.endsWith('Repository') ||
119
+ className.endsWith('Manager') ||
120
+ hasInjectableDecorator;
121
+ if (isServiceClass) {
122
+ const methods = path.node.body.body
123
+ .filter((member) => t.isClassMethod(member) && t.isIdentifier(member.key))
124
+ .map((member) => member.key.name);
125
+ analysis.services.push({
126
+ name: className,
127
+ file: filePath,
128
+ line: path.node.loc?.start.line || 0,
129
+ methods,
130
+ dependencies: [],
131
+ type: 'class',
132
+ });
133
+ }
134
+ // React class components (check for render method)
135
+ const hasRenderMethod = path.node.body.body.some((member) => t.isClassMethod(member) && t.isIdentifier(member.key) && member.key.name === 'render');
136
+ if (hasRenderMethod) {
137
+ analysis.components.push({
138
+ name: className,
139
+ file: filePath,
140
+ line: path.node.loc?.start.line || 0,
141
+ });
142
+ }
143
+ },
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
+ });
169
+ return analysis;
170
+ }
171
+ hasJSXReturn(body) {
172
+ return body.body.some((stmt) => t.isReturnStatement(stmt) && stmt.argument && t.isJSXElement(stmt.argument));
173
+ }
174
+ }
@@ -0,0 +1,14 @@
1
+ import type { ParsemeConfig } from '../config.js';
2
+ import type { ProjectInfo } from '../types.js';
3
+ export declare class ProjectAnalyzer {
4
+ private readonly config;
5
+ constructor(config: ParsemeConfig);
6
+ analyze(rootDir: string): Promise<ProjectInfo>;
7
+ private detectProjectType;
8
+ private detectPackageManager;
9
+ private getFilesRecursive;
10
+ private detectProjectCategory;
11
+ private detectEntryPoints;
12
+ private detectOutputTargets;
13
+ private hasAppDependencies;
14
+ }