archicore 0.1.5 → 0.1.7
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/dist/cli/commands/interactive.js +162 -16
- package/dist/code-index/ast-parser.js +34 -2
- package/dist/github/github-service.d.ts +8 -0
- package/dist/github/github-service.js +76 -7
- package/dist/server/routes/api.js +73 -25
- package/dist/server/routes/developer.js +2 -1
- package/dist/server/routes/github.js +116 -10
- package/dist/server/routes/upload.js +9 -6
- package/dist/server/services/project-service.d.ts +22 -3
- package/dist/server/services/project-service.js +160 -8
- package/dist/utils/file-utils.js +78 -6
- package/package.json +4 -2
|
@@ -21,6 +21,7 @@ import { DeadCodeDetector } from '../../analyzers/dead-code.js';
|
|
|
21
21
|
import { DuplicationDetector } from '../../analyzers/duplication.js';
|
|
22
22
|
import { SecurityAnalyzer } from '../../analyzers/security.js';
|
|
23
23
|
import { RefactoringEngine } from '../../refactoring/index.js';
|
|
24
|
+
import { GitHubService } from '../../github/github-service.js';
|
|
24
25
|
// Singleton instance
|
|
25
26
|
let instance = null;
|
|
26
27
|
export class ProjectService {
|
|
@@ -65,15 +66,21 @@ export class ProjectService {
|
|
|
65
66
|
Logger.error('Could not save projects:', error);
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
|
-
async listProjects() {
|
|
69
|
-
|
|
69
|
+
async listProjects(userId) {
|
|
70
|
+
const projects = Array.from(this.projects.values());
|
|
71
|
+
// Filter by userId if provided
|
|
72
|
+
if (userId) {
|
|
73
|
+
return projects.filter(p => p.ownerId === userId);
|
|
74
|
+
}
|
|
75
|
+
return projects;
|
|
70
76
|
}
|
|
71
|
-
async createProject(name, projectPath) {
|
|
77
|
+
async createProject(name, projectPath, ownerId) {
|
|
72
78
|
const id = uuidv4();
|
|
73
79
|
const project = {
|
|
74
80
|
id,
|
|
75
81
|
name,
|
|
76
82
|
path: projectPath,
|
|
83
|
+
ownerId,
|
|
77
84
|
createdAt: new Date().toISOString(),
|
|
78
85
|
status: 'pending'
|
|
79
86
|
};
|
|
@@ -81,11 +88,25 @@ export class ProjectService {
|
|
|
81
88
|
await this.saveProjects();
|
|
82
89
|
// Инициализируем данные проекта
|
|
83
90
|
await this.initializeProjectData(id);
|
|
84
|
-
Logger.success(`Created project: ${name} (${id})`);
|
|
91
|
+
Logger.success(`Created project: ${name} (${id}) for user ${ownerId}`);
|
|
92
|
+
return project;
|
|
93
|
+
}
|
|
94
|
+
async getProject(id, userId) {
|
|
95
|
+
const project = this.projects.get(id);
|
|
96
|
+
if (!project)
|
|
97
|
+
return null;
|
|
98
|
+
// Check ownership if userId provided
|
|
99
|
+
if (userId && project.ownerId !== userId) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
85
102
|
return project;
|
|
86
103
|
}
|
|
87
|
-
|
|
88
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Check if user owns project
|
|
106
|
+
*/
|
|
107
|
+
async isProjectOwner(projectId, userId) {
|
|
108
|
+
const project = this.projects.get(projectId);
|
|
109
|
+
return project?.ownerId === userId;
|
|
89
110
|
}
|
|
90
111
|
async initializeProjectData(projectId) {
|
|
91
112
|
const project = this.projects.get(projectId);
|
|
@@ -213,8 +234,8 @@ export class ProjectService {
|
|
|
213
234
|
label: node.name,
|
|
214
235
|
type: node.type,
|
|
215
236
|
filePath: node.filePath,
|
|
216
|
-
// Для визуализации - группировка по папкам
|
|
217
|
-
group: node.filePath.split(
|
|
237
|
+
// Для визуализации - группировка по папкам (кроссплатформенно)
|
|
238
|
+
group: node.filePath.split(/[/\\]/).slice(-2, -1)[0] || 'root'
|
|
218
239
|
}));
|
|
219
240
|
const edges = [];
|
|
220
241
|
for (const [from, edgeList] of graph.edges) {
|
|
@@ -366,6 +387,17 @@ export class ProjectService {
|
|
|
366
387
|
};
|
|
367
388
|
}
|
|
368
389
|
async deleteProject(projectId) {
|
|
390
|
+
// Disconnect GitHub repository if connected
|
|
391
|
+
try {
|
|
392
|
+
const githubService = new GitHubService();
|
|
393
|
+
const disconnected = await githubService.disconnectRepositoryByProjectId(projectId);
|
|
394
|
+
if (disconnected) {
|
|
395
|
+
Logger.info(`Disconnected GitHub repository for project: ${projectId}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
Logger.warn(`Failed to disconnect GitHub repository for project ${projectId}:`, err);
|
|
400
|
+
}
|
|
369
401
|
this.projects.delete(projectId);
|
|
370
402
|
this.projectData.delete(projectId);
|
|
371
403
|
await this.saveProjects();
|
|
@@ -689,5 +721,125 @@ export class ProjectService {
|
|
|
689
721
|
}
|
|
690
722
|
return fileContents;
|
|
691
723
|
}
|
|
724
|
+
/**
|
|
725
|
+
* Генерация документации по коду проекта
|
|
726
|
+
*/
|
|
727
|
+
async generateDocumentation(projectId, options = {}) {
|
|
728
|
+
const project = this.projects.get(projectId);
|
|
729
|
+
if (!project) {
|
|
730
|
+
throw new Error(`Project not found: ${projectId}`);
|
|
731
|
+
}
|
|
732
|
+
const data = await this.getProjectData(projectId);
|
|
733
|
+
const fileContents = await this.getFileContents(projectId);
|
|
734
|
+
const format = options.format || 'markdown';
|
|
735
|
+
const language = options.language || 'en';
|
|
736
|
+
Logger.progress(`Generating ${format} documentation for: ${project.name}`);
|
|
737
|
+
// Собираем информацию о проекте
|
|
738
|
+
const stats = project.stats;
|
|
739
|
+
const symbols = data.symbols ? Array.from(data.symbols.values()) : [];
|
|
740
|
+
// graph is available via data.graph if needed for more detailed docs
|
|
741
|
+
// Группируем символы по типу (kind)
|
|
742
|
+
const classes = symbols.filter(s => s.kind === 'class');
|
|
743
|
+
const functions = symbols.filter(s => s.kind === 'function');
|
|
744
|
+
const interfaces = symbols.filter(s => s.kind === 'interface');
|
|
745
|
+
// Формируем структуру файлов
|
|
746
|
+
const files = fileContents ? Array.from(fileContents.keys()) : [];
|
|
747
|
+
const fileStructure = this.buildFileStructure(files);
|
|
748
|
+
// Генерируем документацию
|
|
749
|
+
let documentation = '';
|
|
750
|
+
if (format === 'markdown') {
|
|
751
|
+
const title = language === 'ru' ? 'Документация проекта' : 'Project Documentation';
|
|
752
|
+
const overview = language === 'ru' ? 'Обзор' : 'Overview';
|
|
753
|
+
const structure = language === 'ru' ? 'Структура проекта' : 'Project Structure';
|
|
754
|
+
const classesTitle = language === 'ru' ? 'Классы' : 'Classes';
|
|
755
|
+
const functionsTitle = language === 'ru' ? 'Функции' : 'Functions';
|
|
756
|
+
const interfacesTitle = language === 'ru' ? 'Интерфейсы' : 'Interfaces';
|
|
757
|
+
documentation = `# ${title}: ${project.name}
|
|
758
|
+
|
|
759
|
+
## ${overview}
|
|
760
|
+
|
|
761
|
+
- **Files:** ${stats?.filesCount || files.length}
|
|
762
|
+
- **Symbols:** ${stats?.symbolsCount || symbols.length}
|
|
763
|
+
- **Classes:** ${classes.length}
|
|
764
|
+
- **Functions:** ${functions.length}
|
|
765
|
+
- **Interfaces:** ${interfaces.length}
|
|
766
|
+
|
|
767
|
+
## ${structure}
|
|
768
|
+
|
|
769
|
+
\`\`\`
|
|
770
|
+
${fileStructure}
|
|
771
|
+
\`\`\`
|
|
772
|
+
|
|
773
|
+
${classes.length > 0 ? `## ${classesTitle}
|
|
774
|
+
|
|
775
|
+
${classes.slice(0, 20).map(c => `### ${c.name}
|
|
776
|
+
|
|
777
|
+
- **File:** \`${c.filePath}\`
|
|
778
|
+
- **Line:** ${c.location?.startLine || 'N/A'}
|
|
779
|
+
`).join('\n')}
|
|
780
|
+
` : ''}
|
|
781
|
+
|
|
782
|
+
${interfaces.length > 0 ? `## ${interfacesTitle}
|
|
783
|
+
|
|
784
|
+
${interfaces.slice(0, 20).map(i => `### ${i.name}
|
|
785
|
+
|
|
786
|
+
- **File:** \`${i.filePath}\`
|
|
787
|
+
- **Line:** ${i.location?.startLine || 'N/A'}
|
|
788
|
+
`).join('\n')}
|
|
789
|
+
` : ''}
|
|
790
|
+
|
|
791
|
+
${functions.length > 0 ? `## ${functionsTitle}
|
|
792
|
+
|
|
793
|
+
${functions.slice(0, 30).map(f => `- \`${f.name}\` - ${f.filePath}:${f.location?.startLine || 'N/A'}`).join('\n')}
|
|
794
|
+
` : ''}
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
*Generated by ArchiCore*
|
|
798
|
+
`;
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
// JSON format
|
|
802
|
+
documentation = JSON.stringify({
|
|
803
|
+
project: project.name,
|
|
804
|
+
stats,
|
|
805
|
+
classes: classes.map(c => ({ name: c.name, file: c.filePath, line: c.location?.startLine })),
|
|
806
|
+
functions: functions.map(f => ({ name: f.name, file: f.filePath, line: f.location?.startLine })),
|
|
807
|
+
interfaces: interfaces.map(i => ({ name: i.name, file: i.filePath, line: i.location?.startLine })),
|
|
808
|
+
files
|
|
809
|
+
}, null, 2);
|
|
810
|
+
}
|
|
811
|
+
Logger.success('Documentation generated');
|
|
812
|
+
return { documentation, format };
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Построить текстовое представление структуры файлов
|
|
816
|
+
*/
|
|
817
|
+
buildFileStructure(files) {
|
|
818
|
+
const tree = {};
|
|
819
|
+
for (const file of files) {
|
|
820
|
+
const parts = file.split(/[/\\]/);
|
|
821
|
+
let current = tree;
|
|
822
|
+
for (const part of parts) {
|
|
823
|
+
if (!current[part]) {
|
|
824
|
+
current[part] = {};
|
|
825
|
+
}
|
|
826
|
+
current = current[part];
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const renderTree = (node, prefix = '') => {
|
|
830
|
+
const entries = Object.entries(node);
|
|
831
|
+
return entries.map(([name, children], i) => {
|
|
832
|
+
const isLast = i === entries.length - 1;
|
|
833
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
834
|
+
const childPrefix = isLast ? ' ' : '│ ';
|
|
835
|
+
const childKeys = Object.keys(children);
|
|
836
|
+
if (childKeys.length === 0) {
|
|
837
|
+
return `${prefix}${connector}${name}`;
|
|
838
|
+
}
|
|
839
|
+
return `${prefix}${connector}${name}\n${renderTree(children, prefix + childPrefix)}`;
|
|
840
|
+
}).join('\n');
|
|
841
|
+
};
|
|
842
|
+
return renderTree(tree);
|
|
843
|
+
}
|
|
692
844
|
}
|
|
693
845
|
//# sourceMappingURL=project-service.js.map
|
package/dist/utils/file-utils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile, stat } from 'fs/promises';
|
|
2
|
-
import { join, relative, extname } from 'path';
|
|
2
|
+
import { join, relative, extname, resolve } from 'path';
|
|
3
3
|
import { glob } from 'glob';
|
|
4
|
+
import { Logger } from './logger.js';
|
|
4
5
|
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB max file size for parsing
|
|
5
6
|
export class FileUtils {
|
|
6
7
|
static async readFileContent(filePath) {
|
|
@@ -20,12 +21,38 @@ export class FileUtils {
|
|
|
20
21
|
'**/*.php', // PHP
|
|
21
22
|
'**/*.rb', // Ruby
|
|
22
23
|
'**/*.cs', // C#
|
|
23
|
-
'**/*.cpp', '**/*.c', '**/*.h', '**/*.hpp' // C/C++
|
|
24
|
+
'**/*.cpp', '**/*.c', '**/*.h', '**/*.hpp', // C/C++
|
|
25
|
+
'**/*.html', '**/*.htm', // HTML
|
|
26
|
+
'**/*.css', '**/*.scss', '**/*.sass', '**/*.less', // CSS
|
|
27
|
+
'**/*.json', // JSON
|
|
28
|
+
'**/*.yaml', '**/*.yml', // YAML
|
|
29
|
+
'**/*.xml', // XML
|
|
30
|
+
'**/*.sql', // SQL
|
|
31
|
+
'**/*.sh', '**/*.bash', // Shell
|
|
32
|
+
'**/*.md', '**/*.markdown' // Markdown (for documentation)
|
|
24
33
|
]) {
|
|
34
|
+
// Resolve to absolute path
|
|
35
|
+
const resolvedPath = resolve(rootDir);
|
|
36
|
+
// Normalize for glob (use forward slashes on all platforms)
|
|
37
|
+
const absoluteRootDir = resolvedPath.replace(/\\/g, '/');
|
|
38
|
+
Logger.info(`getAllFiles: rootDir=${rootDir}, resolvedPath=${resolvedPath}, absoluteRootDir=${absoluteRootDir}`);
|
|
39
|
+
// Check if directory exists and is accessible
|
|
40
|
+
try {
|
|
41
|
+
const dirStats = await stat(resolvedPath);
|
|
42
|
+
if (!dirStats.isDirectory()) {
|
|
43
|
+
Logger.error(`getAllFiles: ${resolvedPath} is not a directory`);
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
Logger.info(`getAllFiles: Directory exists and is accessible`);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
Logger.error(`getAllFiles: Cannot access directory ${resolvedPath}: ${err}`);
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
25
52
|
const files = [];
|
|
26
53
|
for (const pattern of patterns) {
|
|
27
54
|
const matches = await glob(pattern, {
|
|
28
|
-
cwd:
|
|
55
|
+
cwd: absoluteRootDir,
|
|
29
56
|
nodir: true, // Exclude directories that match file patterns (e.g., "404.php/")
|
|
30
57
|
dot: true, // Include files in hidden directories (e.g., ".default/")
|
|
31
58
|
ignore: [
|
|
@@ -96,6 +123,28 @@ export class FileUtils {
|
|
|
96
123
|
});
|
|
97
124
|
files.push(...matches);
|
|
98
125
|
}
|
|
126
|
+
Logger.info(`getAllFiles: glob found ${files.length} raw matches`);
|
|
127
|
+
// If no files found, list directory contents for debugging
|
|
128
|
+
if (files.length === 0) {
|
|
129
|
+
try {
|
|
130
|
+
const { readdir } = await import('fs/promises');
|
|
131
|
+
const contents = await readdir(resolvedPath, { withFileTypes: true });
|
|
132
|
+
const listing = contents.map(d => `${d.isDirectory() ? '[DIR]' : '[FILE]'} ${d.name}`);
|
|
133
|
+
Logger.warn(`getAllFiles: No files found! Directory contents: ${listing.join(', ') || '(empty)'}`);
|
|
134
|
+
// If there's a single directory inside, it might be a GitHub ZIP inner folder
|
|
135
|
+
const dirs = contents.filter(d => d.isDirectory());
|
|
136
|
+
if (dirs.length === 1) {
|
|
137
|
+
Logger.info(`getAllFiles: Found single subdirectory "${dirs[0].name}", trying to scan inside...`);
|
|
138
|
+
const innerPath = join(resolvedPath, dirs[0].name);
|
|
139
|
+
const innerContents = await readdir(innerPath, { withFileTypes: true });
|
|
140
|
+
const innerListing = innerContents.slice(0, 10).map(d => `${d.isDirectory() ? '[DIR]' : '[FILE]'} ${d.name}`);
|
|
141
|
+
Logger.info(`getAllFiles: Inner directory contents (first 10): ${innerListing.join(', ')}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (listErr) {
|
|
145
|
+
Logger.error(`getAllFiles: Failed to list directory: ${listErr}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
99
148
|
// Filter out large files and directories
|
|
100
149
|
const filteredFiles = [];
|
|
101
150
|
for (const file of [...new Set(files)]) {
|
|
@@ -113,10 +162,17 @@ export class FileUtils {
|
|
|
113
162
|
// Skip files we can't stat
|
|
114
163
|
}
|
|
115
164
|
}
|
|
165
|
+
Logger.info(`getAllFiles: returning ${filteredFiles.length} files after filtering`);
|
|
166
|
+
if (filteredFiles.length > 0 && filteredFiles.length <= 20) {
|
|
167
|
+
Logger.info(`Files found: ${filteredFiles.join(', ')}`);
|
|
168
|
+
}
|
|
169
|
+
else if (filteredFiles.length > 20) {
|
|
170
|
+
Logger.info(`First 20 files: ${filteredFiles.slice(0, 20).join(', ')}`);
|
|
171
|
+
}
|
|
116
172
|
return filteredFiles;
|
|
117
173
|
}
|
|
118
174
|
static getLanguageFromExtension(filePath) {
|
|
119
|
-
const ext = extname(filePath);
|
|
175
|
+
const ext = extname(filePath).toLowerCase();
|
|
120
176
|
const map = {
|
|
121
177
|
'.ts': 'typescript',
|
|
122
178
|
'.tsx': 'typescript',
|
|
@@ -133,7 +189,22 @@ export class FileUtils {
|
|
|
133
189
|
'.cpp': 'cpp',
|
|
134
190
|
'.c': 'c',
|
|
135
191
|
'.h': 'c',
|
|
136
|
-
'.hpp': 'cpp'
|
|
192
|
+
'.hpp': 'cpp',
|
|
193
|
+
'.html': 'html',
|
|
194
|
+
'.htm': 'html',
|
|
195
|
+
'.css': 'css',
|
|
196
|
+
'.scss': 'scss',
|
|
197
|
+
'.sass': 'sass',
|
|
198
|
+
'.less': 'less',
|
|
199
|
+
'.json': 'json',
|
|
200
|
+
'.yaml': 'yaml',
|
|
201
|
+
'.yml': 'yaml',
|
|
202
|
+
'.xml': 'xml',
|
|
203
|
+
'.sql': 'sql',
|
|
204
|
+
'.sh': 'shell',
|
|
205
|
+
'.bash': 'shell',
|
|
206
|
+
'.md': 'markdown',
|
|
207
|
+
'.markdown': 'markdown'
|
|
137
208
|
};
|
|
138
209
|
return map[ext] || 'unknown';
|
|
139
210
|
}
|
|
@@ -154,7 +225,8 @@ export class FileUtils {
|
|
|
154
225
|
size: stats.size,
|
|
155
226
|
lastModified: stats.mtime
|
|
156
227
|
});
|
|
157
|
-
const
|
|
228
|
+
const parts = rel.split(/[/\\]/);
|
|
229
|
+
const dirPath = join(rootDir, ...parts.slice(0, -1));
|
|
158
230
|
structure.directories.add(dirPath);
|
|
159
231
|
}
|
|
160
232
|
return structure;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archicore",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "AI Software Architect - code analysis, impact prediction, semantic search",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -40,15 +40,16 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/sdk": "^0.32.0",
|
|
42
42
|
"@qdrant/js-client-rest": "^1.11.0",
|
|
43
|
+
"@types/unzipper": "^0.10.11",
|
|
43
44
|
"adm-zip": "^0.5.16",
|
|
44
45
|
"boxen": "^8.0.1",
|
|
45
46
|
"chalk": "^5.4.1",
|
|
46
47
|
"cli-table3": "^0.6.5",
|
|
47
48
|
"commander": "^12.1.0",
|
|
48
|
-
"figures": "^6.1.0",
|
|
49
49
|
"cors": "^2.8.5",
|
|
50
50
|
"dotenv": "^16.4.7",
|
|
51
51
|
"express": "^5.2.1",
|
|
52
|
+
"figures": "^6.1.0",
|
|
52
53
|
"glob": "^11.0.0",
|
|
53
54
|
"mime-types": "^3.0.2",
|
|
54
55
|
"multer": "^2.0.2",
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"tree-sitter-javascript": "^0.21.4",
|
|
59
60
|
"tree-sitter-python": "^0.21.0",
|
|
60
61
|
"tree-sitter-typescript": "^0.21.2",
|
|
62
|
+
"unzipper": "^0.12.3",
|
|
61
63
|
"uuid": "^11.0.3",
|
|
62
64
|
"zod": "^3.24.1"
|
|
63
65
|
},
|