archicore 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/README.md +530 -0
- package/dist/analyzers/dead-code.d.ts +95 -0
- package/dist/analyzers/dead-code.js +327 -0
- package/dist/analyzers/duplication.d.ts +90 -0
- package/dist/analyzers/duplication.js +344 -0
- package/dist/analyzers/security.d.ts +79 -0
- package/dist/analyzers/security.js +484 -0
- package/dist/architecture/index.d.ts +35 -0
- package/dist/architecture/index.js +249 -0
- package/dist/cli/commands/analyzers.d.ts +6 -0
- package/dist/cli/commands/analyzers.js +431 -0
- package/dist/cli/commands/export.d.ts +6 -0
- package/dist/cli/commands/export.js +78 -0
- package/dist/cli/commands/index.d.ts +8 -0
- package/dist/cli/commands/index.js +8 -0
- package/dist/cli/commands/init.d.ts +26 -0
- package/dist/cli/commands/init.js +140 -0
- package/dist/cli/commands/interactive.d.ts +7 -0
- package/dist/cli/commands/interactive.js +522 -0
- package/dist/cli/commands/projects.d.ts +6 -0
- package/dist/cli/commands/projects.js +249 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/ui/box.d.ts +17 -0
- package/dist/cli/ui/box.js +62 -0
- package/dist/cli/ui/colors.d.ts +49 -0
- package/dist/cli/ui/colors.js +86 -0
- package/dist/cli/ui/index.d.ts +9 -0
- package/dist/cli/ui/index.js +9 -0
- package/dist/cli/ui/prompt.d.ts +34 -0
- package/dist/cli/ui/prompt.js +122 -0
- package/dist/cli/ui/spinner.d.ts +29 -0
- package/dist/cli/ui/spinner.js +80 -0
- package/dist/cli/ui/table.d.ts +33 -0
- package/dist/cli/ui/table.js +84 -0
- package/dist/cli/utils/config.d.ts +23 -0
- package/dist/cli/utils/config.js +73 -0
- package/dist/cli/utils/index.d.ts +6 -0
- package/dist/cli/utils/index.js +6 -0
- package/dist/cli/utils/session.d.ts +27 -0
- package/dist/cli/utils/session.js +117 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +295 -0
- package/dist/code-index/ast-parser.d.ts +16 -0
- package/dist/code-index/ast-parser.js +330 -0
- package/dist/code-index/dependency-graph.d.ts +16 -0
- package/dist/code-index/dependency-graph.js +161 -0
- package/dist/code-index/index.d.ts +44 -0
- package/dist/code-index/index.js +124 -0
- package/dist/code-index/symbol-extractor.d.ts +13 -0
- package/dist/code-index/symbol-extractor.js +150 -0
- package/dist/export/index.d.ts +92 -0
- package/dist/export/index.js +676 -0
- package/dist/github/github-service.d.ts +146 -0
- package/dist/github/github-service.js +609 -0
- package/dist/impact-engine/index.d.ts +25 -0
- package/dist/impact-engine/index.js +284 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +149 -0
- package/dist/metrics/index.d.ts +136 -0
- package/dist/metrics/index.js +525 -0
- package/dist/orchestrator/deepseek-optimizer.d.ts +67 -0
- package/dist/orchestrator/deepseek-optimizer.js +320 -0
- package/dist/orchestrator/index.d.ts +34 -0
- package/dist/orchestrator/index.js +305 -0
- package/dist/pr-guardian/index.d.ts +143 -0
- package/dist/pr-guardian/index.js +553 -0
- package/dist/refactoring/index.d.ts +108 -0
- package/dist/refactoring/index.js +580 -0
- package/dist/rules-engine/index.d.ts +129 -0
- package/dist/rules-engine/index.js +482 -0
- package/dist/semantic-memory/embedding-service.d.ts +24 -0
- package/dist/semantic-memory/embedding-service.js +120 -0
- package/dist/semantic-memory/index.d.ts +45 -0
- package/dist/semantic-memory/index.js +206 -0
- package/dist/semantic-memory/vector-store.d.ts +27 -0
- package/dist/semantic-memory/vector-store.js +166 -0
- package/dist/server/index.d.ts +28 -0
- package/dist/server/index.js +141 -0
- package/dist/server/middleware/api-auth.d.ts +43 -0
- package/dist/server/middleware/api-auth.js +256 -0
- package/dist/server/routes/admin.d.ts +5 -0
- package/dist/server/routes/admin.js +123 -0
- package/dist/server/routes/api.d.ts +7 -0
- package/dist/server/routes/api.js +362 -0
- package/dist/server/routes/auth.d.ts +16 -0
- package/dist/server/routes/auth.js +191 -0
- package/dist/server/routes/developer.d.ts +8 -0
- package/dist/server/routes/developer.js +439 -0
- package/dist/server/routes/github.d.ts +7 -0
- package/dist/server/routes/github.js +495 -0
- package/dist/server/routes/upload.d.ts +7 -0
- package/dist/server/routes/upload.js +196 -0
- package/dist/server/services/api-key-service.d.ts +81 -0
- package/dist/server/services/api-key-service.js +281 -0
- package/dist/server/services/auth-service.d.ts +40 -0
- package/dist/server/services/auth-service.js +315 -0
- package/dist/server/services/project-service.d.ts +123 -0
- package/dist/server/services/project-service.js +533 -0
- package/dist/server/services/token-service.d.ts +107 -0
- package/dist/server/services/token-service.js +416 -0
- package/dist/server/services/upload-service.d.ts +93 -0
- package/dist/server/services/upload-service.js +464 -0
- package/dist/types/api.d.ts +188 -0
- package/dist/types/api.js +86 -0
- package/dist/types/github.d.ts +335 -0
- package/dist/types/github.js +5 -0
- package/dist/types/index.d.ts +265 -0
- package/dist/types/index.js +32 -0
- package/dist/types/user.d.ts +69 -0
- package/dist/types/user.js +42 -0
- package/dist/utils/file-utils.d.ts +20 -0
- package/dist/utils/file-utils.js +163 -0
- package/dist/utils/logger.d.ts +17 -0
- package/dist/utils/logger.js +41 -0
- package/dist/watcher/index.d.ts +125 -0
- package/dist/watcher/index.js +397 -0
- package/package.json +71 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Integration Routes for ArchiCore
|
|
3
|
+
*
|
|
4
|
+
* OAuth flow, repository management, webhooks
|
|
5
|
+
*/
|
|
6
|
+
import { Router } from 'express';
|
|
7
|
+
import { randomBytes } from 'crypto';
|
|
8
|
+
import { GitHubService } from '../../github/github-service.js';
|
|
9
|
+
import { ProjectService } from '../services/project-service.js';
|
|
10
|
+
import { authMiddleware } from './auth.js';
|
|
11
|
+
import { Logger } from '../../utils/logger.js';
|
|
12
|
+
export const githubRouter = Router();
|
|
13
|
+
// Services
|
|
14
|
+
const githubService = new GitHubService();
|
|
15
|
+
const projectService = new ProjectService();
|
|
16
|
+
// Store OAuth states (in production, use Redis)
|
|
17
|
+
const oauthStates = new Map();
|
|
18
|
+
// ===== OAUTH FLOW =====
|
|
19
|
+
/**
|
|
20
|
+
* GET /api/github/auth
|
|
21
|
+
* Start OAuth flow - redirect to GitHub
|
|
22
|
+
*/
|
|
23
|
+
githubRouter.get('/auth', authMiddleware, async (req, res) => {
|
|
24
|
+
try {
|
|
25
|
+
if (!req.user) {
|
|
26
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!githubService.isConfigured()) {
|
|
30
|
+
res.status(503).json({
|
|
31
|
+
error: 'GitHub integration not configured',
|
|
32
|
+
message: 'Set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET environment variables'
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Generate state token
|
|
37
|
+
const state = randomBytes(32).toString('hex');
|
|
38
|
+
oauthStates.set(state, {
|
|
39
|
+
userId: req.user.id,
|
|
40
|
+
expiresAt: new Date(Date.now() + 10 * 60 * 1000) // 10 minutes
|
|
41
|
+
});
|
|
42
|
+
// Get authorization URL
|
|
43
|
+
const authUrl = githubService.getAuthorizationUrl(state);
|
|
44
|
+
res.json({ url: authUrl });
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
Logger.error('GitHub auth error:', error);
|
|
48
|
+
res.status(500).json({ error: 'Failed to start GitHub authorization' });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* GET /api/github/callback
|
|
53
|
+
* OAuth callback from GitHub
|
|
54
|
+
*/
|
|
55
|
+
githubRouter.get('/callback', async (req, res) => {
|
|
56
|
+
try {
|
|
57
|
+
const { code, state, error } = req.query;
|
|
58
|
+
if (error) {
|
|
59
|
+
res.redirect(`/?github_error=${encodeURIComponent(error)}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!code || !state) {
|
|
63
|
+
res.redirect('/?github_error=missing_params');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Validate state
|
|
67
|
+
const stateData = oauthStates.get(state);
|
|
68
|
+
if (!stateData || stateData.expiresAt < new Date()) {
|
|
69
|
+
oauthStates.delete(state);
|
|
70
|
+
res.redirect('/?github_error=invalid_state');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
oauthStates.delete(state);
|
|
74
|
+
// Exchange code for token
|
|
75
|
+
const tokenResponse = await githubService.exchangeCodeForToken(code);
|
|
76
|
+
// Get GitHub user info
|
|
77
|
+
const githubUser = await githubService.getGitHubUser(tokenResponse.access_token);
|
|
78
|
+
// Save connection
|
|
79
|
+
await githubService.saveConnection(stateData.userId, tokenResponse, githubUser);
|
|
80
|
+
// Redirect to app with success
|
|
81
|
+
res.redirect('/?github_connected=true');
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
Logger.error('GitHub callback error:', error);
|
|
85
|
+
res.redirect('/?github_error=auth_failed');
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
/**
|
|
89
|
+
* GET /api/github/status
|
|
90
|
+
* Check GitHub connection status
|
|
91
|
+
*/
|
|
92
|
+
githubRouter.get('/status', authMiddleware, async (req, res) => {
|
|
93
|
+
try {
|
|
94
|
+
if (!req.user) {
|
|
95
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const connection = await githubService.getConnection(req.user.id);
|
|
99
|
+
if (!connection) {
|
|
100
|
+
res.json({
|
|
101
|
+
connected: false,
|
|
102
|
+
configured: githubService.isConfigured()
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Get connected repositories count
|
|
107
|
+
const repos = await githubService.getConnectedRepositories(req.user.id);
|
|
108
|
+
res.json({
|
|
109
|
+
connected: true,
|
|
110
|
+
username: connection.githubUsername,
|
|
111
|
+
connectedAt: connection.createdAt,
|
|
112
|
+
repositories: repos.length
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
Logger.error('GitHub status error:', error);
|
|
117
|
+
res.status(500).json({ error: 'Failed to get GitHub status' });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
/**
|
|
121
|
+
* POST /api/github/disconnect
|
|
122
|
+
* Disconnect GitHub account
|
|
123
|
+
*/
|
|
124
|
+
githubRouter.post('/disconnect', authMiddleware, async (req, res) => {
|
|
125
|
+
try {
|
|
126
|
+
if (!req.user) {
|
|
127
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
await githubService.disconnect(req.user.id);
|
|
131
|
+
res.json({ success: true });
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
Logger.error('GitHub disconnect error:', error);
|
|
135
|
+
res.status(500).json({ error: 'Failed to disconnect GitHub' });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
// ===== REPOSITORIES =====
|
|
139
|
+
/**
|
|
140
|
+
* GET /api/github/repositories
|
|
141
|
+
* List available GitHub repositories
|
|
142
|
+
*/
|
|
143
|
+
githubRouter.get('/repositories', authMiddleware, async (req, res) => {
|
|
144
|
+
try {
|
|
145
|
+
if (!req.user) {
|
|
146
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const connection = await githubService.getConnection(req.user.id);
|
|
150
|
+
if (!connection) {
|
|
151
|
+
res.status(400).json({ error: 'GitHub not connected' });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Get all user repos from GitHub
|
|
155
|
+
const githubRepos = await githubService.listUserRepositories(req.user.id);
|
|
156
|
+
// Get connected repos
|
|
157
|
+
const connectedRepos = await githubService.getConnectedRepositories(req.user.id);
|
|
158
|
+
const connectedIds = new Set(connectedRepos.map(r => r.githubRepoId));
|
|
159
|
+
// Format response
|
|
160
|
+
const repositories = githubRepos.map(repo => ({
|
|
161
|
+
id: repo.id,
|
|
162
|
+
name: repo.name,
|
|
163
|
+
fullName: repo.full_name,
|
|
164
|
+
description: repo.description,
|
|
165
|
+
private: repo.private,
|
|
166
|
+
language: repo.language,
|
|
167
|
+
stars: repo.stargazers_count,
|
|
168
|
+
forks: repo.forks_count,
|
|
169
|
+
size: repo.size,
|
|
170
|
+
defaultBranch: repo.default_branch,
|
|
171
|
+
updatedAt: repo.updated_at,
|
|
172
|
+
connected: connectedIds.has(repo.id),
|
|
173
|
+
projectId: connectedRepos.find(r => r.githubRepoId === repo.id)?.projectId
|
|
174
|
+
}));
|
|
175
|
+
res.json({ repositories });
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
Logger.error('List repositories error:', error);
|
|
179
|
+
res.status(500).json({ error: 'Failed to list repositories' });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
/**
|
|
183
|
+
* GET /api/github/repositories/connected
|
|
184
|
+
* List connected repositories
|
|
185
|
+
*/
|
|
186
|
+
githubRouter.get('/repositories/connected', authMiddleware, async (req, res) => {
|
|
187
|
+
try {
|
|
188
|
+
if (!req.user) {
|
|
189
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const repositories = await githubService.getConnectedRepositories(req.user.id);
|
|
193
|
+
res.json({
|
|
194
|
+
repositories: repositories.map(repo => ({
|
|
195
|
+
id: repo.id,
|
|
196
|
+
fullName: repo.fullName,
|
|
197
|
+
name: repo.name,
|
|
198
|
+
owner: repo.owner,
|
|
199
|
+
private: repo.private,
|
|
200
|
+
projectId: repo.projectId,
|
|
201
|
+
autoAnalyze: repo.autoAnalyze,
|
|
202
|
+
analyzePRs: repo.analyzePRs,
|
|
203
|
+
status: repo.status,
|
|
204
|
+
lastAnalyzedAt: repo.lastAnalyzedAt,
|
|
205
|
+
createdAt: repo.createdAt
|
|
206
|
+
}))
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
Logger.error('List connected repositories error:', error);
|
|
211
|
+
res.status(500).json({ error: 'Failed to list connected repositories' });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
/**
|
|
215
|
+
* POST /api/github/repositories/connect
|
|
216
|
+
* Connect a repository to ArchiCore
|
|
217
|
+
*/
|
|
218
|
+
githubRouter.post('/repositories/connect', authMiddleware, async (req, res) => {
|
|
219
|
+
try {
|
|
220
|
+
if (!req.user) {
|
|
221
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const { repositoryId, autoAnalyze, analyzePRs, createProject, projectName } = req.body;
|
|
225
|
+
if (!repositoryId) {
|
|
226
|
+
res.status(400).json({ error: 'repositoryId is required' });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// Connect repository
|
|
230
|
+
const connectedRepo = await githubService.connectRepository(req.user.id, repositoryId, {
|
|
231
|
+
autoAnalyze: autoAnalyze ?? true,
|
|
232
|
+
analyzePRs: analyzePRs ?? true
|
|
233
|
+
});
|
|
234
|
+
// Optionally create ArchiCore project
|
|
235
|
+
let projectId;
|
|
236
|
+
if (createProject !== false) {
|
|
237
|
+
try {
|
|
238
|
+
const project = await projectService.createProject(projectName || connectedRepo.name, connectedRepo.cloneUrl);
|
|
239
|
+
projectId = project.id;
|
|
240
|
+
// Update connected repo with project ID
|
|
241
|
+
connectedRepo.projectId = projectId;
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
Logger.warn(`Failed to create project for ${connectedRepo.fullName}: ${e}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
res.json({
|
|
248
|
+
success: true,
|
|
249
|
+
repository: {
|
|
250
|
+
id: connectedRepo.id,
|
|
251
|
+
fullName: connectedRepo.fullName,
|
|
252
|
+
projectId,
|
|
253
|
+
webhookActive: !!connectedRepo.webhookId
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
Logger.error('Connect repository error:', error);
|
|
259
|
+
res.status(500).json({ error: 'Failed to connect repository' });
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
/**
|
|
263
|
+
* DELETE /api/github/repositories/:id
|
|
264
|
+
* Disconnect a repository
|
|
265
|
+
*/
|
|
266
|
+
githubRouter.delete('/repositories/:id', authMiddleware, async (req, res) => {
|
|
267
|
+
try {
|
|
268
|
+
if (!req.user) {
|
|
269
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const success = await githubService.disconnectRepository(req.user.id, req.params.id);
|
|
273
|
+
if (!success) {
|
|
274
|
+
res.status(404).json({ error: 'Repository not found' });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
res.json({ success: true });
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
Logger.error('Disconnect repository error:', error);
|
|
281
|
+
res.status(500).json({ error: 'Failed to disconnect repository' });
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
/**
|
|
285
|
+
* POST /api/github/repositories/:id/analyze
|
|
286
|
+
* Trigger analysis for a repository
|
|
287
|
+
*/
|
|
288
|
+
githubRouter.post('/repositories/:id/analyze', authMiddleware, async (req, res) => {
|
|
289
|
+
try {
|
|
290
|
+
if (!req.user) {
|
|
291
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const repo = await githubService.getConnectedRepository(req.params.id);
|
|
295
|
+
if (!repo || repo.userId !== req.user.id) {
|
|
296
|
+
res.status(404).json({ error: 'Repository not found' });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (!repo.projectId) {
|
|
300
|
+
res.status(400).json({ error: 'Repository not linked to a project' });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// Update status
|
|
304
|
+
await githubService.updateRepositoryStatus(repo.id, 'syncing');
|
|
305
|
+
// Trigger project analysis
|
|
306
|
+
try {
|
|
307
|
+
const result = await projectService.runFullAnalysis(repo.projectId);
|
|
308
|
+
await githubService.updateLastAnalyzed(repo.id);
|
|
309
|
+
await githubService.updateRepositoryStatus(repo.id, 'active');
|
|
310
|
+
res.json({ success: true, result });
|
|
311
|
+
}
|
|
312
|
+
catch (e) {
|
|
313
|
+
await githubService.updateRepositoryStatus(repo.id, 'error', String(e));
|
|
314
|
+
throw e;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
Logger.error('Analyze repository error:', error);
|
|
319
|
+
res.status(500).json({ error: 'Failed to analyze repository' });
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
// ===== WEBHOOKS =====
|
|
323
|
+
/**
|
|
324
|
+
* POST /api/github/webhook
|
|
325
|
+
* Receive GitHub webhooks
|
|
326
|
+
*/
|
|
327
|
+
githubRouter.post('/webhook', async (req, res) => {
|
|
328
|
+
try {
|
|
329
|
+
const event = req.headers['x-github-event'];
|
|
330
|
+
// signature available at req.headers['x-hub-signature-256'] for verification
|
|
331
|
+
const payload = req.body;
|
|
332
|
+
if (!event || !payload) {
|
|
333
|
+
res.status(400).json({ error: 'Invalid webhook' });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
// Get repository from payload
|
|
337
|
+
const fullName = payload.repository?.full_name;
|
|
338
|
+
if (!fullName) {
|
|
339
|
+
res.status(200).json({ message: 'No repository in payload' });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// Find connected repository
|
|
343
|
+
const repo = await githubService.findRepositoryByWebhook(fullName);
|
|
344
|
+
if (!repo) {
|
|
345
|
+
res.status(200).json({ message: 'Repository not connected' });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
// Verify signature (if webhook secret is set)
|
|
349
|
+
// Note: In production, you'd verify the signature properly using:
|
|
350
|
+
// githubService.verifyWebhookSignature(JSON.stringify(req.body), signature, secret)
|
|
351
|
+
Logger.info(`Webhook received: ${event} for ${fullName}`);
|
|
352
|
+
// Handle different events
|
|
353
|
+
switch (event) {
|
|
354
|
+
case 'push':
|
|
355
|
+
await handlePushEvent(repo, payload);
|
|
356
|
+
break;
|
|
357
|
+
case 'pull_request':
|
|
358
|
+
await handlePullRequestEvent(repo, payload);
|
|
359
|
+
break;
|
|
360
|
+
default:
|
|
361
|
+
Logger.debug(`Unhandled webhook event: ${event}`);
|
|
362
|
+
}
|
|
363
|
+
res.status(200).json({ received: true });
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
Logger.error('Webhook error:', error);
|
|
367
|
+
res.status(500).json({ error: 'Webhook processing failed' });
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
/**
|
|
371
|
+
* Handle push event
|
|
372
|
+
*/
|
|
373
|
+
async function handlePushEvent(repo, payload) {
|
|
374
|
+
if (!repo || !repo.autoAnalyze || !repo.projectId)
|
|
375
|
+
return;
|
|
376
|
+
const branch = payload.ref?.replace('refs/heads/', '');
|
|
377
|
+
if (branch !== repo.defaultBranch) {
|
|
378
|
+
Logger.debug(`Push to non-default branch ${branch}, skipping analysis`);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
Logger.info(`Auto-analyzing ${repo.fullName} after push`);
|
|
382
|
+
try {
|
|
383
|
+
await githubService.updateRepositoryStatus(repo.id, 'syncing');
|
|
384
|
+
await projectService.runFullAnalysis(repo.projectId);
|
|
385
|
+
await githubService.updateLastAnalyzed(repo.id);
|
|
386
|
+
await githubService.updateRepositoryStatus(repo.id, 'active');
|
|
387
|
+
}
|
|
388
|
+
catch (e) {
|
|
389
|
+
Logger.error(`Analysis failed for ${repo.fullName}: ${e}`);
|
|
390
|
+
await githubService.updateRepositoryStatus(repo.id, 'error', String(e));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Handle pull request event
|
|
395
|
+
*/
|
|
396
|
+
async function handlePullRequestEvent(repo, payload) {
|
|
397
|
+
if (!repo || !repo.analyzePRs || !repo.projectId)
|
|
398
|
+
return;
|
|
399
|
+
const action = payload.action;
|
|
400
|
+
const pr = payload.pull_request;
|
|
401
|
+
if (!['opened', 'synchronize', 'reopened'].includes(action)) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
Logger.info(`Analyzing PR #${pr.number} for ${repo.fullName}`);
|
|
405
|
+
try {
|
|
406
|
+
// Get PR files
|
|
407
|
+
const connection = await githubService.getConnection(repo.userId);
|
|
408
|
+
if (!connection)
|
|
409
|
+
return;
|
|
410
|
+
const files = await githubService.getPullRequestFiles(repo.userId, repo.fullName, pr.number);
|
|
411
|
+
// Analyze impact
|
|
412
|
+
const impact = await projectService.analyzeImpact(repo.projectId, {
|
|
413
|
+
description: pr.title,
|
|
414
|
+
files: files.map(f => f.filename),
|
|
415
|
+
symbols: [],
|
|
416
|
+
type: 'modify'
|
|
417
|
+
});
|
|
418
|
+
// Generate comment
|
|
419
|
+
const riskEmoji = {
|
|
420
|
+
low: '✅',
|
|
421
|
+
medium: '⚠️',
|
|
422
|
+
high: '🔶',
|
|
423
|
+
critical: '🚨'
|
|
424
|
+
};
|
|
425
|
+
const riskLevel = impact.risks.length > 0
|
|
426
|
+
? impact.risks.reduce((max, r) => ['critical', 'high', 'medium', 'low'].indexOf(r.severity) <
|
|
427
|
+
['critical', 'high', 'medium', 'low'].indexOf(max) ? r.severity : max, 'low')
|
|
428
|
+
: 'low';
|
|
429
|
+
const comment = `## ArchiCore Analysis ${riskEmoji[riskLevel]}
|
|
430
|
+
|
|
431
|
+
**Risk Level:** ${riskLevel.toUpperCase()}
|
|
432
|
+
**Affected Components:** ${impact.affectedNodes.length}
|
|
433
|
+
**Files Changed:** ${files.length}
|
|
434
|
+
|
|
435
|
+
### Impact Summary
|
|
436
|
+
|
|
437
|
+
${impact.affectedNodes.slice(0, 5).map(n => `- **${n.name}** (${n.type}) - ${n.reason}`).join('\n') || 'No significant impact detected.'}
|
|
438
|
+
|
|
439
|
+
${impact.risks.length > 0 ? `### Risks
|
|
440
|
+
|
|
441
|
+
${impact.risks.slice(0, 3).map(r => `- ${riskEmoji[r.severity]} **${r.category}**: ${r.description}`).join('\n')}` : ''}
|
|
442
|
+
|
|
443
|
+
${impact.recommendations.length > 0 ? `### Recommendations
|
|
444
|
+
|
|
445
|
+
${impact.recommendations.slice(0, 3).map(r => `- ${r.description}`).join('\n')}` : ''}
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
*Analyzed by [ArchiCore](https://archicore.io)*`;
|
|
449
|
+
// Post comment
|
|
450
|
+
await githubService.postPRComment(repo.userId, repo.fullName, pr.number, comment);
|
|
451
|
+
}
|
|
452
|
+
catch (e) {
|
|
453
|
+
Logger.error(`PR analysis failed for ${repo.fullName}#${pr.number}: ${e}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// ===== BRANCHES =====
|
|
457
|
+
/**
|
|
458
|
+
* GET /api/github/repositories/:id/branches
|
|
459
|
+
* List branches for a repository
|
|
460
|
+
*/
|
|
461
|
+
githubRouter.get('/repositories/:id/branches', authMiddleware, async (req, res) => {
|
|
462
|
+
try {
|
|
463
|
+
if (!req.user) {
|
|
464
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const repo = await githubService.getConnectedRepository(req.params.id);
|
|
468
|
+
if (!repo || repo.userId !== req.user.id) {
|
|
469
|
+
res.status(404).json({ error: 'Repository not found' });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const branches = await githubService.listBranches(req.user.id, repo.fullName);
|
|
473
|
+
res.json({
|
|
474
|
+
branches: branches.map(b => ({
|
|
475
|
+
name: b.name,
|
|
476
|
+
protected: b.protected,
|
|
477
|
+
isDefault: b.name === repo.defaultBranch
|
|
478
|
+
}))
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
Logger.error('List branches error:', error);
|
|
483
|
+
res.status(500).json({ error: 'Failed to list branches' });
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
// Cleanup expired OAuth states periodically
|
|
487
|
+
setInterval(() => {
|
|
488
|
+
const now = new Date();
|
|
489
|
+
for (const [state, data] of oauthStates.entries()) {
|
|
490
|
+
if (data.expiresAt < now) {
|
|
491
|
+
oauthStates.delete(state);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}, 60000); // Every minute
|
|
495
|
+
//# sourceMappingURL=github.js.map
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload Routes
|
|
3
|
+
*
|
|
4
|
+
* API для загрузки проектов через ZIP архивы.
|
|
5
|
+
*/
|
|
6
|
+
import { Router } from 'express';
|
|
7
|
+
import multer from 'multer';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { UploadService } from '../services/upload-service.js';
|
|
10
|
+
import { ProjectService } from '../services/project-service.js';
|
|
11
|
+
import { Logger } from '../../utils/logger.js';
|
|
12
|
+
export const uploadRouter = Router();
|
|
13
|
+
// Singleton сервисов
|
|
14
|
+
const uploadService = new UploadService({
|
|
15
|
+
maxFileSize: parseInt(process.env.MAX_UPLOAD_SIZE || '104857600', 10), // 100MB
|
|
16
|
+
maxExtractedSize: parseInt(process.env.MAX_EXTRACTED_SIZE || '524288000', 10), // 500MB
|
|
17
|
+
uploadsDir: process.env.UPLOADS_DIR || '.archicore/uploads',
|
|
18
|
+
extractDir: process.env.PROJECTS_DIR || '.archicore/projects'
|
|
19
|
+
});
|
|
20
|
+
const projectService = new ProjectService();
|
|
21
|
+
// Multer config для загрузки в память
|
|
22
|
+
const upload = multer({
|
|
23
|
+
storage: multer.memoryStorage(),
|
|
24
|
+
limits: {
|
|
25
|
+
fileSize: parseInt(process.env.MAX_UPLOAD_SIZE || '104857600', 10) // 100MB
|
|
26
|
+
},
|
|
27
|
+
fileFilter: (_req, file, cb) => {
|
|
28
|
+
const ext = path.extname(file.originalname).toLowerCase();
|
|
29
|
+
// Разрешаем только ZIP
|
|
30
|
+
if (ext === '.zip') {
|
|
31
|
+
cb(null, true);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
cb(new Error(`Unsupported file type: ${ext}. Only .zip files are allowed.`));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* POST /api/upload
|
|
40
|
+
* Загрузить и распаковать ZIP архив с проектом
|
|
41
|
+
*
|
|
42
|
+
* Body: multipart/form-data
|
|
43
|
+
* - file: ZIP файл
|
|
44
|
+
* - name: (опционально) Имя проекта
|
|
45
|
+
*
|
|
46
|
+
* Response:
|
|
47
|
+
* - uploadId: ID загрузки
|
|
48
|
+
* - projectId: ID созданного проекта
|
|
49
|
+
* - projectPath: Путь к распакованному проекту
|
|
50
|
+
* - stats: Статистика загрузки
|
|
51
|
+
*/
|
|
52
|
+
uploadRouter.post('/', upload.single('file'), async (req, res) => {
|
|
53
|
+
try {
|
|
54
|
+
if (!req.file) {
|
|
55
|
+
res.status(400).json({ error: 'No file uploaded' });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
Logger.progress(`Processing upload: ${req.file.originalname} (${formatBytes(req.file.size)})`);
|
|
59
|
+
// Обрабатываем загрузку
|
|
60
|
+
const uploadResult = await uploadService.processUpload(req.file.buffer, req.file.originalname);
|
|
61
|
+
if (!uploadResult.success) {
|
|
62
|
+
res.status(400).json({
|
|
63
|
+
error: uploadResult.error,
|
|
64
|
+
warnings: uploadResult.warnings,
|
|
65
|
+
stats: uploadResult.stats
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Создаём проект в системе
|
|
70
|
+
const projectName = req.body.name ||
|
|
71
|
+
path.basename(req.file.originalname, '.zip');
|
|
72
|
+
const project = await projectService.createProject(projectName, uploadResult.projectPath);
|
|
73
|
+
Logger.success(`Upload complete: ${projectName} (${uploadResult.stats.fileCount} files)`);
|
|
74
|
+
res.json({
|
|
75
|
+
success: true,
|
|
76
|
+
uploadId: uploadResult.uploadId,
|
|
77
|
+
projectId: project.id,
|
|
78
|
+
projectName: project.name,
|
|
79
|
+
projectPath: uploadResult.projectPath,
|
|
80
|
+
warnings: uploadResult.warnings,
|
|
81
|
+
stats: uploadResult.stats
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
Logger.error('Upload failed:', error);
|
|
86
|
+
if (error instanceof multer.MulterError) {
|
|
87
|
+
if (error.code === 'LIMIT_FILE_SIZE') {
|
|
88
|
+
res.status(413).json({
|
|
89
|
+
error: 'File too large',
|
|
90
|
+
maxSize: process.env.MAX_UPLOAD_SIZE || '100MB'
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
res.status(500).json({
|
|
96
|
+
error: error instanceof Error ? error.message : 'Upload failed'
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
/**
|
|
101
|
+
* POST /api/upload/scan
|
|
102
|
+
* Сканировать архив на угрозы без распаковки
|
|
103
|
+
*
|
|
104
|
+
* Body: multipart/form-data
|
|
105
|
+
* - file: ZIP файл
|
|
106
|
+
*
|
|
107
|
+
* Response:
|
|
108
|
+
* - safe: boolean
|
|
109
|
+
* - threats: SecurityThreat[]
|
|
110
|
+
* - warnings: string[]
|
|
111
|
+
*/
|
|
112
|
+
uploadRouter.post('/scan', upload.single('file'), async (req, res) => {
|
|
113
|
+
try {
|
|
114
|
+
if (!req.file) {
|
|
115
|
+
res.status(400).json({ error: 'No file uploaded' });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Сохраняем временно для сканирования
|
|
119
|
+
const fs = await import('fs/promises');
|
|
120
|
+
const tempPath = `.archicore/temp_scan_${Date.now()}.zip`;
|
|
121
|
+
await fs.mkdir('.archicore', { recursive: true });
|
|
122
|
+
await fs.writeFile(tempPath, req.file.buffer);
|
|
123
|
+
try {
|
|
124
|
+
const scanResult = await uploadService.scanArchive(tempPath);
|
|
125
|
+
res.json({
|
|
126
|
+
safe: scanResult.safe,
|
|
127
|
+
threats: scanResult.threats,
|
|
128
|
+
warnings: scanResult.warnings,
|
|
129
|
+
fileInfo: {
|
|
130
|
+
name: req.file.originalname,
|
|
131
|
+
size: req.file.size
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
// Удаляем временный файл
|
|
137
|
+
await fs.rm(tempPath, { force: true });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
Logger.error('Scan failed:', error);
|
|
142
|
+
res.status(500).json({
|
|
143
|
+
error: error instanceof Error ? error.message : 'Scan failed'
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
/**
|
|
148
|
+
* GET /api/upload/projects
|
|
149
|
+
* Получить список загруженных проектов
|
|
150
|
+
*/
|
|
151
|
+
uploadRouter.get('/projects', async (_req, res) => {
|
|
152
|
+
try {
|
|
153
|
+
const projects = await uploadService.listUploadedProjects();
|
|
154
|
+
res.json({ projects });
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
Logger.error('Failed to list uploaded projects:', error);
|
|
158
|
+
res.status(500).json({ error: 'Failed to list projects' });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
/**
|
|
162
|
+
* DELETE /api/upload/:uploadId
|
|
163
|
+
* Удалить загруженный проект
|
|
164
|
+
*/
|
|
165
|
+
uploadRouter.delete('/:uploadId', async (req, res) => {
|
|
166
|
+
try {
|
|
167
|
+
const { uploadId } = req.params;
|
|
168
|
+
await uploadService.deleteProject(uploadId);
|
|
169
|
+
res.json({ success: true });
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
Logger.error('Failed to delete upload:', error);
|
|
173
|
+
res.status(500).json({ error: 'Failed to delete upload' });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
/**
|
|
177
|
+
* GET /api/upload/limits
|
|
178
|
+
* Получить лимиты загрузки
|
|
179
|
+
*/
|
|
180
|
+
uploadRouter.get('/limits', (_req, res) => {
|
|
181
|
+
res.json({
|
|
182
|
+
maxFileSize: parseInt(process.env.MAX_UPLOAD_SIZE || '104857600', 10),
|
|
183
|
+
maxExtractedSize: parseInt(process.env.MAX_EXTRACTED_SIZE || '524288000', 10),
|
|
184
|
+
allowedFormats: ['.zip'],
|
|
185
|
+
maxCompressionRatio: 100
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
function formatBytes(bytes) {
|
|
189
|
+
if (bytes === 0)
|
|
190
|
+
return '0 Bytes';
|
|
191
|
+
const k = 1024;
|
|
192
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
193
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
194
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=upload.js.map
|