skrypt-ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +200 -0
- package/dist/autofix/index.d.ts +46 -0
- package/dist/autofix/index.js +240 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +40 -0
- package/dist/commands/autofix.d.ts +2 -0
- package/dist/commands/autofix.js +143 -0
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.js +320 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +56 -0
- package/dist/commands/review-pr.d.ts +2 -0
- package/dist/commands/review-pr.js +117 -0
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +142 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +2 -0
- package/dist/config/loader.d.ts +9 -0
- package/dist/config/loader.js +82 -0
- package/dist/config/types.d.ts +24 -0
- package/dist/config/types.js +34 -0
- package/dist/generator/generator.d.ts +15 -0
- package/dist/generator/generator.js +144 -0
- package/dist/generator/index.d.ts +4 -0
- package/dist/generator/index.js +4 -0
- package/dist/generator/organizer.d.ts +29 -0
- package/dist/generator/organizer.js +222 -0
- package/dist/generator/types.d.ts +83 -0
- package/dist/generator/types.js +1 -0
- package/dist/generator/writer.d.ts +28 -0
- package/dist/generator/writer.js +320 -0
- package/dist/github/pr-comments.d.ts +40 -0
- package/dist/github/pr-comments.js +308 -0
- package/dist/llm/anthropic-client.d.ts +16 -0
- package/dist/llm/anthropic-client.js +92 -0
- package/dist/llm/index.d.ts +53 -0
- package/dist/llm/index.js +400 -0
- package/dist/llm/llm.manual-test.d.ts +1 -0
- package/dist/llm/llm.manual-test.js +112 -0
- package/dist/llm/llm.mock-test.d.ts +4 -0
- package/dist/llm/llm.mock-test.js +79 -0
- package/dist/llm/openai-client.d.ts +17 -0
- package/dist/llm/openai-client.js +90 -0
- package/dist/llm/types.d.ts +60 -0
- package/dist/llm/types.js +20 -0
- package/dist/scanner/content-type.d.ts +39 -0
- package/dist/scanner/content-type.js +194 -0
- package/dist/scanner/content-type.test.d.ts +1 -0
- package/dist/scanner/content-type.test.js +231 -0
- package/dist/scanner/go.d.ts +20 -0
- package/dist/scanner/go.js +269 -0
- package/dist/scanner/index.d.ts +21 -0
- package/dist/scanner/index.js +137 -0
- package/dist/scanner/python.d.ts +6 -0
- package/dist/scanner/python.js +57 -0
- package/dist/scanner/python_parser.py +230 -0
- package/dist/scanner/rust.d.ts +23 -0
- package/dist/scanner/rust.js +304 -0
- package/dist/scanner/scanner.test.d.ts +1 -0
- package/dist/scanner/scanner.test.js +210 -0
- package/dist/scanner/types.d.ts +50 -0
- package/dist/scanner/types.js +1 -0
- package/dist/scanner/typescript.d.ts +34 -0
- package/dist/scanner/typescript.js +327 -0
- package/dist/scanner/typescript.manual-test.d.ts +1 -0
- package/dist/scanner/typescript.manual-test.js +112 -0
- package/dist/template/docs.json +32 -0
- package/dist/template/mdx-components.tsx +62 -0
- package/dist/template/next-env.d.ts +6 -0
- package/dist/template/next.config.mjs +17 -0
- package/dist/template/package.json +39 -0
- package/dist/template/postcss.config.mjs +5 -0
- package/dist/template/public/search-index.json +1 -0
- package/dist/template/scripts/build-search-index.mjs +120 -0
- package/dist/template/src/app/api/mock/[...path]/route.ts +224 -0
- package/dist/template/src/app/api/openapi/route.ts +48 -0
- package/dist/template/src/app/api/rate-limit/route.ts +84 -0
- package/dist/template/src/app/docs/[...slug]/page.tsx +81 -0
- package/dist/template/src/app/docs/layout.tsx +9 -0
- package/dist/template/src/app/docs/page.mdx +67 -0
- package/dist/template/src/app/error.tsx +63 -0
- package/dist/template/src/app/layout.tsx +71 -0
- package/dist/template/src/app/page.tsx +18 -0
- package/dist/template/src/app/reference/route.ts +36 -0
- package/dist/template/src/app/robots.ts +14 -0
- package/dist/template/src/app/sitemap.ts +64 -0
- package/dist/template/src/components/breadcrumbs.tsx +41 -0
- package/dist/template/src/components/copy-button.tsx +29 -0
- package/dist/template/src/components/docs-layout.tsx +35 -0
- package/dist/template/src/components/edit-link.tsx +39 -0
- package/dist/template/src/components/feedback.tsx +52 -0
- package/dist/template/src/components/header.tsx +66 -0
- package/dist/template/src/components/mdx/accordion.tsx +48 -0
- package/dist/template/src/components/mdx/api-badge.tsx +57 -0
- package/dist/template/src/components/mdx/callout.tsx +111 -0
- package/dist/template/src/components/mdx/card.tsx +62 -0
- package/dist/template/src/components/mdx/changelog.tsx +57 -0
- package/dist/template/src/components/mdx/code-block.tsx +42 -0
- package/dist/template/src/components/mdx/code-group.tsx +125 -0
- package/dist/template/src/components/mdx/code-playground.tsx +322 -0
- package/dist/template/src/components/mdx/go-playground.tsx +235 -0
- package/dist/template/src/components/mdx/heading.tsx +37 -0
- package/dist/template/src/components/mdx/highlighted-code.tsx +89 -0
- package/dist/template/src/components/mdx/index.tsx +15 -0
- package/dist/template/src/components/mdx/param-table.tsx +71 -0
- package/dist/template/src/components/mdx/python-playground.tsx +293 -0
- package/dist/template/src/components/mdx/steps.tsx +43 -0
- package/dist/template/src/components/mdx/tabs.tsx +81 -0
- package/dist/template/src/components/rate-limit-display.tsx +183 -0
- package/dist/template/src/components/search-dialog.tsx +178 -0
- package/dist/template/src/components/sidebar.tsx +129 -0
- package/dist/template/src/components/syntax-theme-selector.tsx +50 -0
- package/dist/template/src/components/table-of-contents.tsx +84 -0
- package/dist/template/src/components/theme-toggle.tsx +46 -0
- package/dist/template/src/components/version-selector.tsx +61 -0
- package/dist/template/src/contexts/syntax-theme.tsx +52 -0
- package/dist/template/src/lib/highlight.ts +83 -0
- package/dist/template/src/lib/search-types.ts +37 -0
- package/dist/template/src/lib/search.ts +125 -0
- package/dist/template/src/lib/utils.ts +6 -0
- package/dist/template/src/styles/globals.css +152 -0
- package/dist/template/tsconfig.json +25 -0
- package/dist/template/tsconfig.tsbuildinfo +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { classifyElement, classifyElements, getRecommendedStructure, getPromptForContentType } from './content-type.js';
|
|
3
|
+
function createMockElement(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
kind: 'function',
|
|
6
|
+
name: 'testFunction',
|
|
7
|
+
signature: 'function testFunction()',
|
|
8
|
+
parameters: [],
|
|
9
|
+
filePath: '/src/test.ts',
|
|
10
|
+
lineNumber: 1,
|
|
11
|
+
isExported: true,
|
|
12
|
+
isPublic: true,
|
|
13
|
+
...overrides,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
describe('Content Type Detection', () => {
|
|
17
|
+
describe('classifyElement', () => {
|
|
18
|
+
it('should classify API elements with multiple parameters', () => {
|
|
19
|
+
const element = createMockElement({
|
|
20
|
+
name: 'fetchData',
|
|
21
|
+
parameters: [
|
|
22
|
+
{ name: 'url', type: 'string' },
|
|
23
|
+
{ name: 'options', type: 'RequestOptions' },
|
|
24
|
+
{ name: 'timeout', type: 'number' },
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
const result = classifyElement(element);
|
|
28
|
+
expect(result.type).toBe('api');
|
|
29
|
+
expect(result.confidence).toBeGreaterThan(0);
|
|
30
|
+
});
|
|
31
|
+
it('should classify handler/endpoint patterns as API', () => {
|
|
32
|
+
const element = createMockElement({
|
|
33
|
+
name: 'userHandler',
|
|
34
|
+
kind: 'method',
|
|
35
|
+
parentClass: 'UserController',
|
|
36
|
+
filePath: '/src/handlers/user.ts',
|
|
37
|
+
parameters: [
|
|
38
|
+
{ name: 'req', type: 'Request' },
|
|
39
|
+
{ name: 'res', type: 'Response' },
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
const result = classifyElement(element);
|
|
43
|
+
expect(result.type).toBe('api');
|
|
44
|
+
});
|
|
45
|
+
it('should classify HTTP methods as API', () => {
|
|
46
|
+
const element = createMockElement({
|
|
47
|
+
signature: 'async function handleGET(request: Request): Promise<Response>',
|
|
48
|
+
docstring: 'Handles GET requests for user data',
|
|
49
|
+
});
|
|
50
|
+
const result = classifyElement(element);
|
|
51
|
+
expect(result.type).toBe('api');
|
|
52
|
+
});
|
|
53
|
+
it('should classify config/setup functions as guide', () => {
|
|
54
|
+
const element = createMockElement({
|
|
55
|
+
name: 'configureDatabase',
|
|
56
|
+
});
|
|
57
|
+
const result = classifyElement(element);
|
|
58
|
+
expect(result.type).toBe('guide');
|
|
59
|
+
});
|
|
60
|
+
it('should classify init functions as guide', () => {
|
|
61
|
+
const element = createMockElement({
|
|
62
|
+
name: 'initializeApp',
|
|
63
|
+
});
|
|
64
|
+
const result = classifyElement(element);
|
|
65
|
+
expect(result.type).toBe('guide');
|
|
66
|
+
});
|
|
67
|
+
it('should classify example files as tutorial', () => {
|
|
68
|
+
const element = createMockElement({
|
|
69
|
+
name: 'main',
|
|
70
|
+
filePath: '/examples/quickstart.ts',
|
|
71
|
+
});
|
|
72
|
+
const result = classifyElement(element);
|
|
73
|
+
expect(result.type).toBe('tutorial');
|
|
74
|
+
});
|
|
75
|
+
it('should classify demo functions as tutorial', () => {
|
|
76
|
+
const element = createMockElement({
|
|
77
|
+
name: 'demoFeature',
|
|
78
|
+
});
|
|
79
|
+
const result = classifyElement(element);
|
|
80
|
+
expect(result.type).toBe('tutorial');
|
|
81
|
+
});
|
|
82
|
+
it('should classify simple functions with few params as tutorial', () => {
|
|
83
|
+
const element = createMockElement({
|
|
84
|
+
name: 'simpleFunction',
|
|
85
|
+
kind: 'function',
|
|
86
|
+
parameters: [{ name: 'input', type: 'string' }],
|
|
87
|
+
});
|
|
88
|
+
const result = classifyElement(element);
|
|
89
|
+
// Simple functions default to tutorial
|
|
90
|
+
expect(result.type).toBe('tutorial');
|
|
91
|
+
});
|
|
92
|
+
it('should classify async API patterns correctly', () => {
|
|
93
|
+
const element = createMockElement({
|
|
94
|
+
signature: 'async function createUser(data: UserData): Promise<User>',
|
|
95
|
+
parameters: [
|
|
96
|
+
{ name: 'data', type: 'UserData' },
|
|
97
|
+
{ name: 'options', type: 'CreateOptions' },
|
|
98
|
+
{ name: 'validate', type: 'boolean' },
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
const result = classifyElement(element);
|
|
102
|
+
expect(result.type).toBe('api');
|
|
103
|
+
});
|
|
104
|
+
it('should return tutorial for simple functions with no indicators', () => {
|
|
105
|
+
// Simple functions with few params default to tutorial
|
|
106
|
+
const element = createMockElement({
|
|
107
|
+
name: 'x',
|
|
108
|
+
kind: 'function',
|
|
109
|
+
parameters: [],
|
|
110
|
+
docstring: undefined,
|
|
111
|
+
filePath: '/src/utils.ts',
|
|
112
|
+
});
|
|
113
|
+
const result = classifyElement(element);
|
|
114
|
+
// Gets tutorial because simple function with <=1 params
|
|
115
|
+
expect(result.type).toBe('tutorial');
|
|
116
|
+
});
|
|
117
|
+
it('should return overview for class elements with no indicators', () => {
|
|
118
|
+
// Classes without other indicators get overview
|
|
119
|
+
const element = createMockElement({
|
|
120
|
+
name: 'X',
|
|
121
|
+
kind: 'class',
|
|
122
|
+
parameters: [],
|
|
123
|
+
docstring: undefined,
|
|
124
|
+
filePath: '/src/utils.ts',
|
|
125
|
+
});
|
|
126
|
+
const result = classifyElement(element);
|
|
127
|
+
expect(result.type).toBe('overview');
|
|
128
|
+
});
|
|
129
|
+
it('should include reasons for classification', () => {
|
|
130
|
+
const element = createMockElement({
|
|
131
|
+
name: 'handler',
|
|
132
|
+
filePath: '/src/handlers/api.ts',
|
|
133
|
+
});
|
|
134
|
+
const result = classifyElement(element);
|
|
135
|
+
expect(result.reasons.length).toBeGreaterThan(0);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('classifyElements', () => {
|
|
139
|
+
it('should group elements by content type', () => {
|
|
140
|
+
const elements = [
|
|
141
|
+
createMockElement({
|
|
142
|
+
name: 'fetchUsers',
|
|
143
|
+
kind: 'method',
|
|
144
|
+
parentClass: 'UserService',
|
|
145
|
+
parameters: [
|
|
146
|
+
{ name: 'page', type: 'number' },
|
|
147
|
+
{ name: 'limit', type: 'number' },
|
|
148
|
+
{ name: 'filter', type: 'Filter' },
|
|
149
|
+
],
|
|
150
|
+
}),
|
|
151
|
+
createMockElement({
|
|
152
|
+
name: 'configureAuth',
|
|
153
|
+
kind: 'function',
|
|
154
|
+
parameters: [
|
|
155
|
+
{ name: 'options', type: 'AuthOptions' },
|
|
156
|
+
{ name: 'provider', type: 'Provider' },
|
|
157
|
+
],
|
|
158
|
+
}),
|
|
159
|
+
createMockElement({
|
|
160
|
+
name: 'exampleUsage',
|
|
161
|
+
filePath: '/examples/demo.ts',
|
|
162
|
+
}),
|
|
163
|
+
];
|
|
164
|
+
const result = classifyElements(elements);
|
|
165
|
+
expect(result.get('api')?.length).toBeGreaterThanOrEqual(1);
|
|
166
|
+
// configureAuth should be guide (config pattern)
|
|
167
|
+
expect(result.get('guide')?.length).toBeGreaterThanOrEqual(1);
|
|
168
|
+
expect(result.get('tutorial')?.length).toBeGreaterThanOrEqual(1);
|
|
169
|
+
});
|
|
170
|
+
it('should handle empty elements array', () => {
|
|
171
|
+
const result = classifyElements([]);
|
|
172
|
+
expect(result.get('api')).toHaveLength(0);
|
|
173
|
+
expect(result.get('guide')).toHaveLength(0);
|
|
174
|
+
expect(result.get('tutorial')).toHaveLength(0);
|
|
175
|
+
expect(result.get('overview')).toHaveLength(0);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
describe('getRecommendedStructure', () => {
|
|
179
|
+
it('should return sections grouped by type', () => {
|
|
180
|
+
const elements = [
|
|
181
|
+
createMockElement({
|
|
182
|
+
name: 'createUser',
|
|
183
|
+
parameters: [
|
|
184
|
+
{ name: 'data', type: 'UserData' },
|
|
185
|
+
{ name: 'options', type: 'Options' },
|
|
186
|
+
{ name: 'callback', type: 'Function' },
|
|
187
|
+
],
|
|
188
|
+
}),
|
|
189
|
+
createMockElement({
|
|
190
|
+
name: 'setupDatabase',
|
|
191
|
+
}),
|
|
192
|
+
];
|
|
193
|
+
const result = getRecommendedStructure(elements);
|
|
194
|
+
expect(result.sections.length).toBeGreaterThan(0);
|
|
195
|
+
expect(result.stats.api + result.stats.guide + result.stats.tutorial + result.stats.overview).toBe(elements.length);
|
|
196
|
+
});
|
|
197
|
+
it('should provide stats for each type', () => {
|
|
198
|
+
const elements = [
|
|
199
|
+
createMockElement({ name: 'api1', parameters: [{ name: 'a' }, { name: 'b' }, { name: 'c' }] }),
|
|
200
|
+
createMockElement({ name: 'api2', parameters: [{ name: 'x' }, { name: 'y' }, { name: 'z' }] }),
|
|
201
|
+
createMockElement({ name: 'config1' }),
|
|
202
|
+
];
|
|
203
|
+
const result = getRecommendedStructure(elements);
|
|
204
|
+
expect(typeof result.stats.api).toBe('number');
|
|
205
|
+
expect(typeof result.stats.guide).toBe('number');
|
|
206
|
+
expect(typeof result.stats.tutorial).toBe('number');
|
|
207
|
+
expect(typeof result.stats.overview).toBe('number');
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
describe('getPromptForContentType', () => {
|
|
211
|
+
it('should return appropriate prompt for api type', () => {
|
|
212
|
+
const prompt = getPromptForContentType('api');
|
|
213
|
+
expect(prompt).toContain('API reference');
|
|
214
|
+
expect(prompt).toContain('parameter');
|
|
215
|
+
});
|
|
216
|
+
it('should return appropriate prompt for guide type', () => {
|
|
217
|
+
const prompt = getPromptForContentType('guide');
|
|
218
|
+
expect(prompt).toContain('guide');
|
|
219
|
+
expect(prompt).toContain('setup');
|
|
220
|
+
});
|
|
221
|
+
it('should return appropriate prompt for tutorial type', () => {
|
|
222
|
+
const prompt = getPromptForContentType('tutorial');
|
|
223
|
+
expect(prompt).toContain('tutorial');
|
|
224
|
+
expect(prompt).toContain('beginner');
|
|
225
|
+
});
|
|
226
|
+
it('should return appropriate prompt for overview type', () => {
|
|
227
|
+
const prompt = getPromptForContentType('overview');
|
|
228
|
+
expect(prompt).toContain('overview');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Scanner, ScanResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Scanner for Go source files
|
|
4
|
+
* Extracts: functions, methods, types (structs), interfaces
|
|
5
|
+
*/
|
|
6
|
+
export declare class GoScanner implements Scanner {
|
|
7
|
+
languages: string[];
|
|
8
|
+
canHandle(filePath: string): boolean;
|
|
9
|
+
scanFile(filePath: string): Promise<ScanResult>;
|
|
10
|
+
private extractImports;
|
|
11
|
+
private extractFunctions;
|
|
12
|
+
private extractMethods;
|
|
13
|
+
private extractTypes;
|
|
14
|
+
private parseGoParams;
|
|
15
|
+
private splitParams;
|
|
16
|
+
private parseReturnType;
|
|
17
|
+
private getLineNumber;
|
|
18
|
+
private getDocComment;
|
|
19
|
+
private getSourceContext;
|
|
20
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
/**
|
|
3
|
+
* Scanner for Go source files
|
|
4
|
+
* Extracts: functions, methods, types (structs), interfaces
|
|
5
|
+
*/
|
|
6
|
+
export class GoScanner {
|
|
7
|
+
languages = ['go'];
|
|
8
|
+
canHandle(filePath) {
|
|
9
|
+
return /\.go$/.test(filePath) && !filePath.includes('_test.go');
|
|
10
|
+
}
|
|
11
|
+
async scanFile(filePath) {
|
|
12
|
+
try {
|
|
13
|
+
const source = readFileSync(filePath, 'utf-8');
|
|
14
|
+
const elements = [];
|
|
15
|
+
const errors = [];
|
|
16
|
+
const lines = source.split('\n');
|
|
17
|
+
// Extract package name
|
|
18
|
+
const packageMatch = source.match(/^package\s+(\w+)/m);
|
|
19
|
+
const packageName = packageMatch?.[1] ?? 'unknown';
|
|
20
|
+
// Extract imports for context
|
|
21
|
+
const imports = this.extractImports(source);
|
|
22
|
+
// Find all exported functions, methods, types
|
|
23
|
+
this.extractFunctions(source, lines, filePath, packageName, imports, elements);
|
|
24
|
+
this.extractMethods(source, lines, filePath, packageName, imports, elements);
|
|
25
|
+
this.extractTypes(source, lines, filePath, packageName, imports, elements);
|
|
26
|
+
return {
|
|
27
|
+
filePath,
|
|
28
|
+
language: 'go',
|
|
29
|
+
elements,
|
|
30
|
+
errors
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
return {
|
|
35
|
+
filePath,
|
|
36
|
+
language: 'go',
|
|
37
|
+
elements: [],
|
|
38
|
+
errors: [`Failed to parse: ${err}`]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
extractImports(source) {
|
|
43
|
+
const imports = [];
|
|
44
|
+
// Single import
|
|
45
|
+
const singleMatch = source.matchAll(/^import\s+"([^"]+)"/gm);
|
|
46
|
+
for (const m of singleMatch) {
|
|
47
|
+
if (m[1])
|
|
48
|
+
imports.push(m[1]);
|
|
49
|
+
}
|
|
50
|
+
// Import block
|
|
51
|
+
const blockMatch = source.match(/import\s*\(([\s\S]*?)\)/m);
|
|
52
|
+
if (blockMatch?.[1]) {
|
|
53
|
+
const importLines = blockMatch[1].split('\n');
|
|
54
|
+
for (const line of importLines) {
|
|
55
|
+
const match = line.match(/"([^"]+)"/);
|
|
56
|
+
if (match?.[1])
|
|
57
|
+
imports.push(match[1]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return imports;
|
|
61
|
+
}
|
|
62
|
+
extractFunctions(source, lines, filePath, packageName, imports, elements) {
|
|
63
|
+
// Match: func FunctionName(params) returnType
|
|
64
|
+
// Only exported (uppercase first letter)
|
|
65
|
+
const funcRegex = /^func\s+([A-Z][a-zA-Z0-9_]*)\s*(\[[^\]]+\])?\s*\(([^)]*)\)\s*([^{]*)/gm;
|
|
66
|
+
let match;
|
|
67
|
+
while ((match = funcRegex.exec(source)) !== null) {
|
|
68
|
+
const name = match[1];
|
|
69
|
+
const paramsStr = match[3];
|
|
70
|
+
if (!name || paramsStr === undefined)
|
|
71
|
+
continue;
|
|
72
|
+
const typeParams = match[2] ?? '';
|
|
73
|
+
const returnPart = (match[4] ?? '').trim();
|
|
74
|
+
const lineNumber = this.getLineNumber(source, match.index);
|
|
75
|
+
const docstring = this.getDocComment(lines, lineNumber - 1);
|
|
76
|
+
const parameters = this.parseGoParams(paramsStr);
|
|
77
|
+
const returnType = this.parseReturnType(returnPart);
|
|
78
|
+
const signature = `func ${name}${typeParams}(${paramsStr})${returnPart ? ' ' + returnPart : ''}`;
|
|
79
|
+
elements.push({
|
|
80
|
+
kind: 'function',
|
|
81
|
+
name,
|
|
82
|
+
signature,
|
|
83
|
+
parameters,
|
|
84
|
+
returnType,
|
|
85
|
+
docstring,
|
|
86
|
+
filePath,
|
|
87
|
+
lineNumber,
|
|
88
|
+
isExported: true,
|
|
89
|
+
isPublic: true,
|
|
90
|
+
imports,
|
|
91
|
+
packageName,
|
|
92
|
+
sourceContext: this.getSourceContext(lines, lineNumber)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
extractMethods(source, lines, filePath, packageName, imports, elements) {
|
|
97
|
+
// Match: func (receiver Type) MethodName(params) returnType
|
|
98
|
+
const methodRegex = /^func\s+\((\w+)\s+\*?(\w+)\)\s+([A-Z][a-zA-Z0-9_]*)\s*(\[[^\]]+\])?\s*\(([^)]*)\)\s*([^{]*)/gm;
|
|
99
|
+
let match;
|
|
100
|
+
while ((match = methodRegex.exec(source)) !== null) {
|
|
101
|
+
const receiverVar = match[1];
|
|
102
|
+
const receiverType = match[2];
|
|
103
|
+
const name = match[3];
|
|
104
|
+
const paramsStr = match[5];
|
|
105
|
+
if (!receiverVar || !receiverType || !name || paramsStr === undefined)
|
|
106
|
+
continue;
|
|
107
|
+
const typeParams = match[4] ?? '';
|
|
108
|
+
const returnPart = (match[6] ?? '').trim();
|
|
109
|
+
const lineNumber = this.getLineNumber(source, match.index);
|
|
110
|
+
const docstring = this.getDocComment(lines, lineNumber - 1);
|
|
111
|
+
const parameters = this.parseGoParams(paramsStr);
|
|
112
|
+
const returnType = this.parseReturnType(returnPart);
|
|
113
|
+
const receiverFull = match[0].match(/\([^)]+\)/)?.[0] ?? `(${receiverVar} ${receiverType})`;
|
|
114
|
+
const signature = `func ${receiverFull} ${name}${typeParams}(${paramsStr})${returnPart ? ' ' + returnPart : ''}`;
|
|
115
|
+
elements.push({
|
|
116
|
+
kind: 'method',
|
|
117
|
+
name,
|
|
118
|
+
signature,
|
|
119
|
+
parameters,
|
|
120
|
+
returnType,
|
|
121
|
+
docstring,
|
|
122
|
+
filePath,
|
|
123
|
+
lineNumber,
|
|
124
|
+
parentClass: receiverType,
|
|
125
|
+
isExported: true,
|
|
126
|
+
isPublic: true,
|
|
127
|
+
imports,
|
|
128
|
+
packageName,
|
|
129
|
+
sourceContext: this.getSourceContext(lines, lineNumber)
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
extractTypes(source, lines, filePath, packageName, imports, elements) {
|
|
134
|
+
// Match: type TypeName struct/interface
|
|
135
|
+
const typeRegex = /^type\s+([A-Z][a-zA-Z0-9_]*)\s+(?:struct|interface)\s*\{/gm;
|
|
136
|
+
let match;
|
|
137
|
+
while ((match = typeRegex.exec(source)) !== null) {
|
|
138
|
+
const name = match[1];
|
|
139
|
+
if (!name)
|
|
140
|
+
continue;
|
|
141
|
+
const isInterface = match[0].includes('interface');
|
|
142
|
+
const lineNumber = this.getLineNumber(source, match.index);
|
|
143
|
+
const docstring = this.getDocComment(lines, lineNumber - 1);
|
|
144
|
+
const signature = `type ${name} ${isInterface ? 'interface' : 'struct'}`;
|
|
145
|
+
elements.push({
|
|
146
|
+
kind: 'class', // Using 'class' for Go types
|
|
147
|
+
name,
|
|
148
|
+
signature,
|
|
149
|
+
parameters: [],
|
|
150
|
+
docstring,
|
|
151
|
+
filePath,
|
|
152
|
+
lineNumber,
|
|
153
|
+
isExported: true,
|
|
154
|
+
isPublic: true,
|
|
155
|
+
imports,
|
|
156
|
+
packageName,
|
|
157
|
+
sourceContext: this.getSourceContext(lines, lineNumber, 15)
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
parseGoParams(paramsStr) {
|
|
162
|
+
if (!paramsStr.trim())
|
|
163
|
+
return [];
|
|
164
|
+
const params = [];
|
|
165
|
+
// Split by comma but respect parentheses
|
|
166
|
+
const parts = this.splitParams(paramsStr);
|
|
167
|
+
for (const part of parts) {
|
|
168
|
+
const trimmed = part.trim();
|
|
169
|
+
if (!trimmed)
|
|
170
|
+
continue;
|
|
171
|
+
// Format: "name type" or "name, name2 type" or just "type"
|
|
172
|
+
const lastSpace = trimmed.lastIndexOf(' ');
|
|
173
|
+
if (lastSpace > 0) {
|
|
174
|
+
const type = trimmed.slice(lastSpace + 1);
|
|
175
|
+
const names = trimmed.slice(0, lastSpace).split(',').map(n => n.trim());
|
|
176
|
+
for (const name of names) {
|
|
177
|
+
if (name)
|
|
178
|
+
params.push({ name, type });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
// Just a type (rare in Go)
|
|
183
|
+
params.push({ name: trimmed, type: undefined });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return params;
|
|
187
|
+
}
|
|
188
|
+
splitParams(str) {
|
|
189
|
+
const parts = [];
|
|
190
|
+
let depth = 0;
|
|
191
|
+
let current = '';
|
|
192
|
+
for (const char of str) {
|
|
193
|
+
if (char === '(' || char === '[' || char === '{')
|
|
194
|
+
depth++;
|
|
195
|
+
else if (char === ')' || char === ']' || char === '}')
|
|
196
|
+
depth--;
|
|
197
|
+
else if (char === ',' && depth === 0) {
|
|
198
|
+
parts.push(current);
|
|
199
|
+
current = '';
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
current += char;
|
|
203
|
+
}
|
|
204
|
+
if (current.trim())
|
|
205
|
+
parts.push(current);
|
|
206
|
+
return parts;
|
|
207
|
+
}
|
|
208
|
+
parseReturnType(returnPart) {
|
|
209
|
+
const trimmed = returnPart.trim();
|
|
210
|
+
if (!trimmed)
|
|
211
|
+
return undefined;
|
|
212
|
+
// Remove leading/trailing braces or parens
|
|
213
|
+
if (trimmed.startsWith('(') && trimmed.endsWith(')')) {
|
|
214
|
+
return trimmed;
|
|
215
|
+
}
|
|
216
|
+
return trimmed || undefined;
|
|
217
|
+
}
|
|
218
|
+
getLineNumber(source, index) {
|
|
219
|
+
return source.slice(0, index).split('\n').length;
|
|
220
|
+
}
|
|
221
|
+
getDocComment(lines, lineIndex) {
|
|
222
|
+
const comments = [];
|
|
223
|
+
let i = lineIndex - 1;
|
|
224
|
+
while (i >= 0) {
|
|
225
|
+
const lineContent = lines[i];
|
|
226
|
+
if (!lineContent)
|
|
227
|
+
break;
|
|
228
|
+
const line = lineContent.trim();
|
|
229
|
+
if (line.startsWith('//')) {
|
|
230
|
+
comments.unshift(line.replace(/^\/\/\s?/, ''));
|
|
231
|
+
i--;
|
|
232
|
+
}
|
|
233
|
+
else if (line === '') {
|
|
234
|
+
i--;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Also check for /* */ style
|
|
241
|
+
if (comments.length === 0 && lineIndex > 0) {
|
|
242
|
+
const prevLine = lines[lineIndex - 1];
|
|
243
|
+
if (prevLine?.includes('*/')) {
|
|
244
|
+
// Find start of block comment
|
|
245
|
+
let start = lineIndex - 1;
|
|
246
|
+
while (start >= 0 && !lines[start]?.includes('/*')) {
|
|
247
|
+
start--;
|
|
248
|
+
}
|
|
249
|
+
if (start >= 0 && lines[start]) {
|
|
250
|
+
const block = lines.slice(start, lineIndex).join('\n');
|
|
251
|
+
const cleaned = block
|
|
252
|
+
.replace(/\/\*\*?/, '')
|
|
253
|
+
.replace(/\*\//, '')
|
|
254
|
+
.split('\n')
|
|
255
|
+
.map(l => l.trim().replace(/^\*\s?/, ''))
|
|
256
|
+
.filter(Boolean)
|
|
257
|
+
.join('\n');
|
|
258
|
+
return cleaned || undefined;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return comments.length > 0 ? comments.join('\n') : undefined;
|
|
263
|
+
}
|
|
264
|
+
getSourceContext(lines, lineNumber, context = 5) {
|
|
265
|
+
const start = Math.max(0, lineNumber - context - 1);
|
|
266
|
+
const end = Math.min(lines.length, lineNumber + context);
|
|
267
|
+
return lines.slice(start, end).join('\n');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ScanResult } from './types.js';
|
|
2
|
+
export * from './types.js';
|
|
3
|
+
export * from './content-type.js';
|
|
4
|
+
export interface ScanOptions {
|
|
5
|
+
include?: string[];
|
|
6
|
+
exclude?: string[];
|
|
7
|
+
onProgress?: (current: number, total: number, file: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export interface ScanAllResult {
|
|
10
|
+
files: ScanResult[];
|
|
11
|
+
totalElements: number;
|
|
12
|
+
errors: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Scan a directory for all API elements
|
|
16
|
+
*/
|
|
17
|
+
export declare function scanDirectory(dir: string, options?: ScanOptions): Promise<ScanAllResult>;
|
|
18
|
+
/**
|
|
19
|
+
* Scan a single file
|
|
20
|
+
*/
|
|
21
|
+
export declare function scanFile(filePath: string): Promise<ScanResult>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readdir } from 'fs/promises';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import { PythonScanner } from './python.js';
|
|
4
|
+
import { TypeScriptScanner } from './typescript.js';
|
|
5
|
+
import { GoScanner } from './go.js';
|
|
6
|
+
import { RustScanner } from './rust.js';
|
|
7
|
+
export * from './types.js';
|
|
8
|
+
export * from './content-type.js';
|
|
9
|
+
// All available scanners
|
|
10
|
+
const SCANNERS = [
|
|
11
|
+
new PythonScanner(),
|
|
12
|
+
new TypeScriptScanner(),
|
|
13
|
+
new GoScanner(),
|
|
14
|
+
new RustScanner(),
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* Get the appropriate scanner for a file
|
|
18
|
+
*/
|
|
19
|
+
function getScannerForFile(filePath) {
|
|
20
|
+
for (const scanner of SCANNERS) {
|
|
21
|
+
if (scanner.canHandle(filePath)) {
|
|
22
|
+
return scanner;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check if a path should be excluded based on patterns
|
|
29
|
+
*/
|
|
30
|
+
function shouldExclude(filePath, excludePatterns) {
|
|
31
|
+
for (const pattern of excludePatterns) {
|
|
32
|
+
// Simple glob matching for common patterns
|
|
33
|
+
if (pattern.includes('**')) {
|
|
34
|
+
const parts = pattern.split('**');
|
|
35
|
+
if (parts.length === 2) {
|
|
36
|
+
const [prefix, suffix] = parts;
|
|
37
|
+
const prefixMatch = !prefix || filePath.includes(prefix.replace(/^\//, '').replace(/\/$/, ''));
|
|
38
|
+
const suffixMatch = !suffix || filePath.includes(suffix.replace(/^\//, '').replace(/\/$/, ''));
|
|
39
|
+
if (prefixMatch && suffixMatch)
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else if (filePath.includes(pattern.replace(/\*/g, ''))) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check if a file matches include patterns
|
|
51
|
+
*/
|
|
52
|
+
function shouldInclude(filePath, includePatterns) {
|
|
53
|
+
if (includePatterns.length === 0)
|
|
54
|
+
return true;
|
|
55
|
+
const ext = extname(filePath);
|
|
56
|
+
for (const pattern of includePatterns) {
|
|
57
|
+
// Match extension patterns like **/*.py
|
|
58
|
+
if (pattern.includes('*')) {
|
|
59
|
+
const extPattern = pattern.split('*').pop() || '';
|
|
60
|
+
if (ext === extPattern)
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Recursively find all files in a directory
|
|
68
|
+
*/
|
|
69
|
+
async function findFiles(dir, include, exclude) {
|
|
70
|
+
const files = [];
|
|
71
|
+
async function walk(currentDir) {
|
|
72
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
const fullPath = join(currentDir, entry.name);
|
|
75
|
+
if (shouldExclude(fullPath, exclude)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
await walk(fullPath);
|
|
80
|
+
}
|
|
81
|
+
else if (entry.isFile()) {
|
|
82
|
+
if (shouldInclude(fullPath, include) && getScannerForFile(fullPath)) {
|
|
83
|
+
files.push(fullPath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
await walk(dir);
|
|
89
|
+
return files;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Scan a directory for all API elements
|
|
93
|
+
*/
|
|
94
|
+
export async function scanDirectory(dir, options = {}) {
|
|
95
|
+
const include = options.include || ['**/*.py', '**/*.ts', '**/*.js', '**/*.go', '**/*.rs'];
|
|
96
|
+
const exclude = options.exclude || ['**/node_modules/**', '**/__pycache__/**', '**/dist/**'];
|
|
97
|
+
const files = await findFiles(dir, include, exclude);
|
|
98
|
+
const results = [];
|
|
99
|
+
const allErrors = [];
|
|
100
|
+
let totalElements = 0;
|
|
101
|
+
for (let i = 0; i < files.length; i++) {
|
|
102
|
+
const file = files[i];
|
|
103
|
+
options.onProgress?.(i + 1, files.length, file);
|
|
104
|
+
const scanner = getScannerForFile(file);
|
|
105
|
+
if (!scanner)
|
|
106
|
+
continue;
|
|
107
|
+
try {
|
|
108
|
+
const result = await scanner.scanFile(file);
|
|
109
|
+
results.push(result);
|
|
110
|
+
totalElements += result.elements.length;
|
|
111
|
+
allErrors.push(...result.errors);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
allErrors.push(`Error scanning ${file}: ${err}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
files: results,
|
|
119
|
+
totalElements,
|
|
120
|
+
errors: allErrors
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Scan a single file
|
|
125
|
+
*/
|
|
126
|
+
export async function scanFile(filePath) {
|
|
127
|
+
const scanner = getScannerForFile(filePath);
|
|
128
|
+
if (!scanner) {
|
|
129
|
+
return {
|
|
130
|
+
filePath,
|
|
131
|
+
language: 'python', // default
|
|
132
|
+
elements: [],
|
|
133
|
+
errors: [`No scanner available for ${filePath}`]
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return scanner.scanFile(filePath);
|
|
137
|
+
}
|