archicore 0.1.2 → 0.1.3
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/auth.d.ts +38 -0
- package/dist/cli/commands/auth.js +164 -0
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/interactive.js +245 -20
- package/dist/cli/ui/prompt.js +4 -0
- package/dist/cli/utils/config.d.ts +2 -0
- package/dist/server/index.js +3 -0
- package/dist/server/routes/api.js +28 -1
- package/dist/server/routes/auth.js +43 -0
- package/dist/server/routes/device-auth.d.ts +8 -0
- package/dist/server/routes/device-auth.js +149 -0
- package/dist/server/services/project-service.d.ts +27 -1
- package/dist/server/services/project-service.js +160 -0
- package/package.json +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ArchiCore CLI - Authentication
|
|
3
|
+
*
|
|
4
|
+
* Device authorization flow for CLI
|
|
5
|
+
*/
|
|
6
|
+
interface TokenResponse {
|
|
7
|
+
accessToken: string;
|
|
8
|
+
refreshToken?: string;
|
|
9
|
+
expiresIn: number;
|
|
10
|
+
user: {
|
|
11
|
+
id: string;
|
|
12
|
+
email: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
tier: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Check if user is authenticated
|
|
19
|
+
*/
|
|
20
|
+
export declare function isAuthenticated(): Promise<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* Get current user info
|
|
23
|
+
*/
|
|
24
|
+
export declare function getCurrentUser(): Promise<TokenResponse['user'] | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Login via device flow
|
|
27
|
+
*/
|
|
28
|
+
export declare function login(): Promise<boolean>;
|
|
29
|
+
/**
|
|
30
|
+
* Logout - clear stored tokens
|
|
31
|
+
*/
|
|
32
|
+
export declare function logout(): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Require authentication - prompt login if needed
|
|
35
|
+
*/
|
|
36
|
+
export declare function requireAuth(): Promise<TokenResponse['user'] | null>;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ArchiCore CLI - Authentication
|
|
3
|
+
*
|
|
4
|
+
* Device authorization flow for CLI
|
|
5
|
+
*/
|
|
6
|
+
import { loadConfig, saveConfig } from '../utils/config.js';
|
|
7
|
+
import { colors, createSpinner, printSuccess, printError, printInfo } from '../ui/index.js';
|
|
8
|
+
/**
|
|
9
|
+
* Check if user is authenticated
|
|
10
|
+
*/
|
|
11
|
+
export async function isAuthenticated() {
|
|
12
|
+
const config = await loadConfig();
|
|
13
|
+
return !!config.accessToken;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get current user info
|
|
17
|
+
*/
|
|
18
|
+
export async function getCurrentUser() {
|
|
19
|
+
const config = await loadConfig();
|
|
20
|
+
if (!config.accessToken) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(`${config.serverUrl}/api/auth/me`, {
|
|
25
|
+
headers: {
|
|
26
|
+
'Authorization': `Bearer ${config.accessToken}`,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
// Token expired or invalid
|
|
31
|
+
await saveConfig({ accessToken: undefined, refreshToken: undefined });
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const data = await response.json();
|
|
35
|
+
return data.user;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Login via device flow
|
|
43
|
+
*/
|
|
44
|
+
export async function login() {
|
|
45
|
+
const config = await loadConfig();
|
|
46
|
+
console.log();
|
|
47
|
+
const spinner = createSpinner('Requesting authorization...').start();
|
|
48
|
+
try {
|
|
49
|
+
// Step 1: Request device code
|
|
50
|
+
const codeResponse = await fetch(`${config.serverUrl}/api/auth/device/code`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
});
|
|
54
|
+
if (!codeResponse.ok) {
|
|
55
|
+
spinner.fail('Failed to start authorization');
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const codeData = await codeResponse.json();
|
|
59
|
+
spinner.stop();
|
|
60
|
+
// Step 2: Show user the code and URL
|
|
61
|
+
console.log();
|
|
62
|
+
console.log(colors.primary.bold(' Authorization Required'));
|
|
63
|
+
console.log();
|
|
64
|
+
console.log(` ${colors.muted('1.')} Open this URL in your browser:`);
|
|
65
|
+
console.log();
|
|
66
|
+
console.log(` ${colors.accent.bold(codeData.verificationUrl)}`);
|
|
67
|
+
console.log();
|
|
68
|
+
console.log(` ${colors.muted('2.')} Enter this code:`);
|
|
69
|
+
console.log();
|
|
70
|
+
console.log(` ${colors.highlight.bold(codeData.userCode)}`);
|
|
71
|
+
console.log();
|
|
72
|
+
// Step 3: Poll for token
|
|
73
|
+
const pollSpinner = createSpinner('Waiting for authorization...').start();
|
|
74
|
+
const token = await pollForToken(config.serverUrl, codeData.deviceCode, codeData.interval * 1000, codeData.expiresIn * 1000);
|
|
75
|
+
if (!token) {
|
|
76
|
+
pollSpinner.fail('Authorization timed out or was denied');
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
// Step 4: Save token
|
|
80
|
+
await saveConfig({
|
|
81
|
+
accessToken: token.accessToken,
|
|
82
|
+
refreshToken: token.refreshToken,
|
|
83
|
+
});
|
|
84
|
+
pollSpinner.succeed('Authorized!');
|
|
85
|
+
console.log();
|
|
86
|
+
printSuccess(`Welcome, ${token.user.name || token.user.email}!`);
|
|
87
|
+
printInfo(`Plan: ${token.user.tier}`);
|
|
88
|
+
console.log();
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
spinner.fail('Authorization failed');
|
|
93
|
+
printError(String(error));
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Poll for token after user authorizes
|
|
99
|
+
*/
|
|
100
|
+
async function pollForToken(serverUrl, deviceCode, interval, timeout) {
|
|
101
|
+
const startTime = Date.now();
|
|
102
|
+
while (Date.now() - startTime < timeout) {
|
|
103
|
+
await sleep(interval);
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetch(`${serverUrl}/api/auth/device/token`, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: { 'Content-Type': 'application/json' },
|
|
108
|
+
body: JSON.stringify({ deviceCode }),
|
|
109
|
+
});
|
|
110
|
+
if (response.ok) {
|
|
111
|
+
return await response.json();
|
|
112
|
+
}
|
|
113
|
+
const error = await response.json();
|
|
114
|
+
if (error.error === 'authorization_pending') {
|
|
115
|
+
// User hasn't authorized yet, keep polling
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (error.error === 'slow_down') {
|
|
119
|
+
// Slow down polling
|
|
120
|
+
interval += 1000;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (error.error === 'expired_token' || error.error === 'access_denied') {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Network error, keep trying
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Logout - clear stored tokens
|
|
135
|
+
*/
|
|
136
|
+
export async function logout() {
|
|
137
|
+
await saveConfig({
|
|
138
|
+
accessToken: undefined,
|
|
139
|
+
refreshToken: undefined,
|
|
140
|
+
});
|
|
141
|
+
console.log();
|
|
142
|
+
printSuccess('Logged out successfully');
|
|
143
|
+
console.log();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Require authentication - prompt login if needed
|
|
147
|
+
*/
|
|
148
|
+
export async function requireAuth() {
|
|
149
|
+
const user = await getCurrentUser();
|
|
150
|
+
if (user) {
|
|
151
|
+
return user;
|
|
152
|
+
}
|
|
153
|
+
console.log();
|
|
154
|
+
printInfo('Please log in to continue');
|
|
155
|
+
const success = await login();
|
|
156
|
+
if (!success) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return getCurrentUser();
|
|
160
|
+
}
|
|
161
|
+
function sleep(ms) {
|
|
162
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -5,4 +5,5 @@ export { registerAnalyzerCommands } from './analyzers.js';
|
|
|
5
5
|
export { registerExportCommand } from './export.js';
|
|
6
6
|
export { startInteractiveMode } from './interactive.js';
|
|
7
7
|
export { initProject, isInitialized, getLocalProject } from './init.js';
|
|
8
|
+
export { login, logout, isAuthenticated, getCurrentUser, requireAuth } from './auth.js';
|
|
8
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -5,4 +5,5 @@ export { registerAnalyzerCommands } from './analyzers.js';
|
|
|
5
5
|
export { registerExportCommand } from './export.js';
|
|
6
6
|
export { startInteractiveMode } from './interactive.js';
|
|
7
7
|
export { initProject, isInitialized, getLocalProject } from './init.js';
|
|
8
|
+
export { login, logout, isAuthenticated, getCurrentUser, requireAuth } from './auth.js';
|
|
8
9
|
//# sourceMappingURL=index.js.map
|
|
@@ -7,6 +7,7 @@ import * as readline from 'readline';
|
|
|
7
7
|
import { loadConfig } from '../utils/config.js';
|
|
8
8
|
import { checkServerConnection } from '../utils/session.js';
|
|
9
9
|
import { isInitialized, getLocalProject } from './init.js';
|
|
10
|
+
import { requireAuth, logout } from './auth.js';
|
|
10
11
|
import { colors, icons, createSpinner, printHelp, printGoodbye, printSection, printSuccess, printError, printWarning, printInfo, printKeyValue, header, } from '../ui/index.js';
|
|
11
12
|
const state = {
|
|
12
13
|
running: true,
|
|
@@ -14,6 +15,7 @@ const state = {
|
|
|
14
15
|
projectName: null,
|
|
15
16
|
projectPath: process.cwd(),
|
|
16
17
|
history: [],
|
|
18
|
+
user: null,
|
|
17
19
|
};
|
|
18
20
|
export async function startInteractiveMode() {
|
|
19
21
|
// Check if initialized in current directory
|
|
@@ -37,13 +39,21 @@ export async function startInteractiveMode() {
|
|
|
37
39
|
const connected = await checkServerConnection();
|
|
38
40
|
if (!connected) {
|
|
39
41
|
spinner.fail('Cannot connect to ArchiCore server');
|
|
40
|
-
printError('Make sure the server is running
|
|
42
|
+
printError('Make sure the server is running');
|
|
41
43
|
process.exit(1);
|
|
42
44
|
}
|
|
43
45
|
spinner.succeed('Connected');
|
|
46
|
+
// Require authentication
|
|
47
|
+
const user = await requireAuth();
|
|
48
|
+
if (!user) {
|
|
49
|
+
printError('Authentication required');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
state.user = user;
|
|
44
53
|
// Print welcome
|
|
45
54
|
console.log();
|
|
46
55
|
console.log(header('ArchiCore', 'AI Software Architect'));
|
|
56
|
+
console.log(colors.muted(` User: ${colors.primary(user.name || user.email)} (${user.tier})`));
|
|
47
57
|
if (state.projectName) {
|
|
48
58
|
console.log(colors.muted(` Project: ${colors.primary(state.projectName)}`));
|
|
49
59
|
}
|
|
@@ -159,36 +169,54 @@ async function handleCommand(input) {
|
|
|
159
169
|
case 'stats':
|
|
160
170
|
await handleMetricsCommand();
|
|
161
171
|
break;
|
|
172
|
+
case 'duplication':
|
|
173
|
+
case 'duplicates':
|
|
174
|
+
await handleDuplicationCommand();
|
|
175
|
+
break;
|
|
176
|
+
case 'refactoring':
|
|
177
|
+
case 'refactor':
|
|
178
|
+
await handleRefactoringCommand();
|
|
179
|
+
break;
|
|
180
|
+
case 'rules':
|
|
181
|
+
await handleRulesCommand();
|
|
182
|
+
break;
|
|
162
183
|
case 'export':
|
|
163
184
|
await handleExportCommand(args);
|
|
164
185
|
break;
|
|
165
186
|
case 'status':
|
|
166
187
|
await handleStatusCommand();
|
|
167
188
|
break;
|
|
189
|
+
case 'logout':
|
|
190
|
+
await logout();
|
|
191
|
+
state.running = false;
|
|
192
|
+
break;
|
|
168
193
|
default:
|
|
169
194
|
printError(`Unknown command: /${command}`);
|
|
170
195
|
printInfo('Use /help to see available commands');
|
|
171
196
|
}
|
|
172
197
|
}
|
|
173
198
|
async function handleIndexCommand() {
|
|
199
|
+
const config = await loadConfig();
|
|
200
|
+
// Регистрируем проект на сервере если ещё нет ID
|
|
174
201
|
if (!state.projectId) {
|
|
175
|
-
|
|
176
|
-
const spinner = createSpinner('Registering project...').start();
|
|
202
|
+
const registerSpinner = createSpinner('Registering project...').start();
|
|
177
203
|
try {
|
|
178
|
-
const config = await loadConfig();
|
|
179
204
|
const response = await fetch(`${config.serverUrl}/api/projects`, {
|
|
180
205
|
method: 'POST',
|
|
181
|
-
headers: {
|
|
206
|
+
headers: {
|
|
207
|
+
'Content-Type': 'application/json',
|
|
208
|
+
'Authorization': `Bearer ${config.accessToken}`,
|
|
209
|
+
},
|
|
182
210
|
body: JSON.stringify({ name: state.projectName, path: state.projectPath }),
|
|
183
211
|
});
|
|
184
212
|
if (response.ok) {
|
|
185
213
|
const data = await response.json();
|
|
186
214
|
state.projectId = data.id || data.project?.id;
|
|
187
215
|
}
|
|
188
|
-
|
|
216
|
+
registerSpinner.succeed('Project registered');
|
|
189
217
|
}
|
|
190
218
|
catch {
|
|
191
|
-
|
|
219
|
+
registerSpinner.fail('Failed to register project');
|
|
192
220
|
return;
|
|
193
221
|
}
|
|
194
222
|
}
|
|
@@ -196,32 +224,81 @@ async function handleIndexCommand() {
|
|
|
196
224
|
printError('Failed to register project with server');
|
|
197
225
|
return;
|
|
198
226
|
}
|
|
199
|
-
|
|
227
|
+
// Локальная индексация
|
|
228
|
+
const indexSpinner = createSpinner('Parsing local files...').start();
|
|
200
229
|
try {
|
|
201
|
-
|
|
202
|
-
const
|
|
230
|
+
// Динамический импорт CodeIndex
|
|
231
|
+
const { CodeIndex } = await import('../../code-index/index.js');
|
|
232
|
+
const fs = await import('fs/promises');
|
|
233
|
+
const pathModule = await import('path');
|
|
234
|
+
const codeIndex = new CodeIndex(state.projectPath);
|
|
235
|
+
// Парсим AST
|
|
236
|
+
const asts = await codeIndex.parseProject();
|
|
237
|
+
indexSpinner.update(`Parsed ${asts.size} files, extracting symbols...`);
|
|
238
|
+
// Извлекаем символы
|
|
239
|
+
const symbols = codeIndex.extractSymbols(asts);
|
|
240
|
+
indexSpinner.update(`Found ${symbols.size} symbols, building graph...`);
|
|
241
|
+
// Строим граф зависимостей
|
|
242
|
+
const graph = codeIndex.buildDependencyGraph(asts, symbols);
|
|
243
|
+
indexSpinner.update('Reading file contents...');
|
|
244
|
+
// Читаем содержимое файлов
|
|
245
|
+
const fileContents = [];
|
|
246
|
+
for (const [filePath] of asts) {
|
|
247
|
+
try {
|
|
248
|
+
const fullPath = pathModule.default.isAbsolute(filePath)
|
|
249
|
+
? filePath
|
|
250
|
+
: pathModule.default.join(state.projectPath, filePath);
|
|
251
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
252
|
+
fileContents.push([filePath, content]);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// Игнорируем ошибки чтения
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
indexSpinner.update('Uploading to server...');
|
|
259
|
+
// Конвертируем Maps в массивы для JSON
|
|
260
|
+
const astsArray = Array.from(asts.entries());
|
|
261
|
+
const symbolsArray = Array.from(symbols.entries());
|
|
262
|
+
const graphData = {
|
|
263
|
+
nodes: Array.from(graph.nodes.entries()),
|
|
264
|
+
edges: Array.from(graph.edges.entries()),
|
|
265
|
+
};
|
|
266
|
+
// Отправляем на сервер
|
|
267
|
+
const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/upload-index`, {
|
|
203
268
|
method: 'POST',
|
|
269
|
+
headers: {
|
|
270
|
+
'Content-Type': 'application/json',
|
|
271
|
+
'Authorization': `Bearer ${config.accessToken}`,
|
|
272
|
+
},
|
|
273
|
+
body: JSON.stringify({
|
|
274
|
+
asts: astsArray,
|
|
275
|
+
symbols: symbolsArray,
|
|
276
|
+
graph: graphData,
|
|
277
|
+
fileContents,
|
|
278
|
+
statistics: {
|
|
279
|
+
totalFiles: asts.size,
|
|
280
|
+
totalSymbols: symbols.size,
|
|
281
|
+
},
|
|
282
|
+
}),
|
|
204
283
|
});
|
|
205
284
|
if (!response.ok) {
|
|
206
|
-
|
|
285
|
+
const errorData = await response.json().catch(() => ({}));
|
|
286
|
+
throw new Error(errorData.error || 'Upload failed');
|
|
207
287
|
}
|
|
208
288
|
const data = await response.json();
|
|
209
|
-
|
|
210
|
-
printKeyValue('Files', String(data.statistics?.
|
|
211
|
-
printKeyValue('Symbols', String(data.statistics?.
|
|
212
|
-
//
|
|
213
|
-
const { getLocalProject } = await import('./init.js');
|
|
289
|
+
indexSpinner.succeed('Project indexed and uploaded');
|
|
290
|
+
printKeyValue('Files', String(data.statistics?.filesCount || asts.size));
|
|
291
|
+
printKeyValue('Symbols', String(data.statistics?.symbolsCount || symbols.size));
|
|
292
|
+
// Обновляем локальный конфиг
|
|
214
293
|
const localProject = await getLocalProject(state.projectPath);
|
|
215
294
|
if (localProject) {
|
|
216
295
|
localProject.id = state.projectId;
|
|
217
296
|
localProject.indexed = true;
|
|
218
|
-
|
|
219
|
-
const path = await import('path');
|
|
220
|
-
await fs.writeFile(path.join(state.projectPath, '.archicore', 'project.json'), JSON.stringify(localProject, null, 2));
|
|
297
|
+
await fs.writeFile(pathModule.default.join(state.projectPath, '.archicore', 'project.json'), JSON.stringify(localProject, null, 2));
|
|
221
298
|
}
|
|
222
299
|
}
|
|
223
300
|
catch (error) {
|
|
224
|
-
|
|
301
|
+
indexSpinner.fail('Indexing failed');
|
|
225
302
|
printError(String(error));
|
|
226
303
|
}
|
|
227
304
|
}
|
|
@@ -512,6 +589,36 @@ async function handleStatusCommand() {
|
|
|
512
589
|
const connected = await checkServerConnection();
|
|
513
590
|
console.log(` Server: ${connected ? colors.success('Connected') : colors.error('Disconnected')}`);
|
|
514
591
|
console.log(` URL: ${colors.muted(config.serverUrl)}`);
|
|
592
|
+
if (state.user) {
|
|
593
|
+
console.log(` User: ${colors.primary(state.user.name || state.user.email)}`);
|
|
594
|
+
console.log(` Plan: ${colors.highlight(state.user.tier)}`);
|
|
595
|
+
// Get subscription details
|
|
596
|
+
try {
|
|
597
|
+
const response = await fetch(`${config.serverUrl}/api/auth/subscription`, {
|
|
598
|
+
headers: { 'Authorization': `Bearer ${config.accessToken}` },
|
|
599
|
+
});
|
|
600
|
+
if (response.ok) {
|
|
601
|
+
const data = await response.json();
|
|
602
|
+
if (data.expiresAt) {
|
|
603
|
+
const expiresDate = new Date(data.expiresAt);
|
|
604
|
+
const now = new Date();
|
|
605
|
+
const daysLeft = Math.ceil((expiresDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
606
|
+
if (daysLeft > 0) {
|
|
607
|
+
console.log(` Expires: ${colors.muted(expiresDate.toLocaleDateString())} (${daysLeft} days left)`);
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
console.log(` Expires: ${colors.error('Expired')}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (data.usage) {
|
|
614
|
+
console.log(` Usage: ${data.usage.used}/${data.usage.limit} requests today`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
// Ignore subscription fetch errors
|
|
620
|
+
}
|
|
621
|
+
}
|
|
515
622
|
if (state.projectId) {
|
|
516
623
|
console.log(` Project: ${colors.primary(state.projectName || state.projectId)}`);
|
|
517
624
|
}
|
|
@@ -519,4 +626,122 @@ async function handleStatusCommand() {
|
|
|
519
626
|
console.log(` Project: ${colors.muted('None selected')}`);
|
|
520
627
|
}
|
|
521
628
|
}
|
|
629
|
+
async function handleDuplicationCommand() {
|
|
630
|
+
if (!state.projectId) {
|
|
631
|
+
printError('No project indexed');
|
|
632
|
+
printInfo('Use /index first');
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const spinner = createSpinner('Analyzing code duplication...').start();
|
|
636
|
+
try {
|
|
637
|
+
const config = await loadConfig();
|
|
638
|
+
const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/duplication`);
|
|
639
|
+
if (!response.ok)
|
|
640
|
+
throw new Error('Analysis failed');
|
|
641
|
+
const data = await response.json();
|
|
642
|
+
const result = data.duplication;
|
|
643
|
+
spinner.succeed('Analysis complete');
|
|
644
|
+
printSection('Code Duplication');
|
|
645
|
+
console.log(` Code clones: ${result.clones?.length || 0}`);
|
|
646
|
+
console.log(` Duplication rate: ${(result.duplicationRate || 0).toFixed(1)}%`);
|
|
647
|
+
console.log(` Duplicated lines: ${result.duplicatedLines || 0}`);
|
|
648
|
+
if (result.clones?.length > 0) {
|
|
649
|
+
console.log();
|
|
650
|
+
console.log(colors.muted(' Top duplicates:'));
|
|
651
|
+
for (const clone of result.clones.slice(0, 5)) {
|
|
652
|
+
console.log(` ${colors.warning(icons.warning)} ${clone.files?.length || 2} files, ${clone.lines || 0} lines`);
|
|
653
|
+
if (clone.files) {
|
|
654
|
+
for (const f of clone.files.slice(0, 2)) {
|
|
655
|
+
console.log(` ${colors.dim(f)}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
spinner.fail('Analysis failed');
|
|
663
|
+
throw error;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async function handleRefactoringCommand() {
|
|
667
|
+
if (!state.projectId) {
|
|
668
|
+
printError('No project indexed');
|
|
669
|
+
printInfo('Use /index first');
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const spinner = createSpinner('Generating refactoring suggestions...').start();
|
|
673
|
+
try {
|
|
674
|
+
const config = await loadConfig();
|
|
675
|
+
const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/refactoring`);
|
|
676
|
+
if (!response.ok)
|
|
677
|
+
throw new Error('Analysis failed');
|
|
678
|
+
const data = await response.json();
|
|
679
|
+
const suggestions = data.refactoring?.suggestions || [];
|
|
680
|
+
spinner.succeed('Analysis complete');
|
|
681
|
+
printSection('Refactoring Suggestions');
|
|
682
|
+
if (suggestions.length === 0) {
|
|
683
|
+
printSuccess('No refactoring needed!');
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
console.log(` Total suggestions: ${suggestions.length}`);
|
|
687
|
+
const critical = suggestions.filter((s) => s.priority === 'critical');
|
|
688
|
+
const high = suggestions.filter((s) => s.priority === 'high');
|
|
689
|
+
if (critical.length > 0) {
|
|
690
|
+
console.log(` ${colors.critical(`Critical: ${critical.length}`)}`);
|
|
691
|
+
}
|
|
692
|
+
if (high.length > 0) {
|
|
693
|
+
console.log(` ${colors.high(`High: ${high.length}`)}`);
|
|
694
|
+
}
|
|
695
|
+
console.log();
|
|
696
|
+
console.log(colors.muted(' Top suggestions:'));
|
|
697
|
+
for (const s of suggestions.slice(0, 5)) {
|
|
698
|
+
const priorityColor = s.priority === 'critical' ? colors.critical :
|
|
699
|
+
s.priority === 'high' ? colors.high : colors.muted;
|
|
700
|
+
console.log(` ${priorityColor(`[${s.priority}]`)} ${s.type}: ${s.description}`);
|
|
701
|
+
if (s.file) {
|
|
702
|
+
console.log(` ${colors.dim(s.file)}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
spinner.fail('Analysis failed');
|
|
708
|
+
throw error;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async function handleRulesCommand() {
|
|
712
|
+
if (!state.projectId) {
|
|
713
|
+
printError('No project indexed');
|
|
714
|
+
printInfo('Use /index first');
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const spinner = createSpinner('Checking architectural rules...').start();
|
|
718
|
+
try {
|
|
719
|
+
const config = await loadConfig();
|
|
720
|
+
const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/rules`);
|
|
721
|
+
if (!response.ok)
|
|
722
|
+
throw new Error('Analysis failed');
|
|
723
|
+
const data = await response.json();
|
|
724
|
+
const violations = data.rules?.violations || [];
|
|
725
|
+
spinner.succeed('Analysis complete');
|
|
726
|
+
printSection('Architectural Rules');
|
|
727
|
+
if (violations.length === 0) {
|
|
728
|
+
printSuccess('All rules passed!');
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
console.log(` Violations: ${violations.length}`);
|
|
732
|
+
console.log();
|
|
733
|
+
for (const v of violations.slice(0, 10)) {
|
|
734
|
+
const severityColor = v.severity === 'error' ? colors.error :
|
|
735
|
+
v.severity === 'warning' ? colors.warning : colors.muted;
|
|
736
|
+
console.log(` ${severityColor(icons.error)} ${v.rule}: ${v.message}`);
|
|
737
|
+
if (v.file) {
|
|
738
|
+
console.log(` ${colors.dim(v.file)}`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
spinner.fail('Analysis failed');
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
522
747
|
//# sourceMappingURL=interactive.js.map
|
package/dist/cli/ui/prompt.js
CHANGED
|
@@ -75,8 +75,12 @@ export function printHelp() {
|
|
|
75
75
|
['/dead-code', 'Find dead code'],
|
|
76
76
|
['/security', 'Security analysis'],
|
|
77
77
|
['/metrics', 'Code metrics'],
|
|
78
|
+
['/duplication', 'Find code duplicates'],
|
|
79
|
+
['/refactoring', 'Refactoring suggestions'],
|
|
80
|
+
['/rules', 'Check architecture rules'],
|
|
78
81
|
['/export [format]', 'Export (json/html/md)'],
|
|
79
82
|
['/status', 'Show current status'],
|
|
83
|
+
['/logout', 'Log out'],
|
|
80
84
|
['/clear', 'Clear screen'],
|
|
81
85
|
['/help', 'Show this help'],
|
|
82
86
|
['/exit', 'Exit ArchiCore'],
|
package/dist/server/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { authRouter } from './routes/auth.js';
|
|
|
20
20
|
import { adminRouter } from './routes/admin.js';
|
|
21
21
|
import { developerRouter } from './routes/developer.js';
|
|
22
22
|
import { githubRouter } from './routes/github.js';
|
|
23
|
+
import deviceAuthRouter from './routes/device-auth.js';
|
|
23
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
25
|
const __dirname = path.dirname(__filename);
|
|
25
26
|
export class ArchiCoreServer {
|
|
@@ -55,6 +56,8 @@ export class ArchiCoreServer {
|
|
|
55
56
|
setupRoutes() {
|
|
56
57
|
// Auth routes
|
|
57
58
|
this.app.use('/api/auth', authRouter);
|
|
59
|
+
// Device auth for CLI
|
|
60
|
+
this.app.use('/api/auth/device', deviceAuthRouter);
|
|
58
61
|
// Admin routes
|
|
59
62
|
this.app.use('/api/admin', adminRouter);
|
|
60
63
|
// Developer API routes (for API key management and public API)
|
|
@@ -64,7 +64,7 @@ apiRouter.get('/projects/:id', async (req, res) => {
|
|
|
64
64
|
});
|
|
65
65
|
/**
|
|
66
66
|
* POST /api/projects/:id/index
|
|
67
|
-
* Запустить индексацию проекта
|
|
67
|
+
* Запустить индексацию проекта (только для локальных проектов на сервере)
|
|
68
68
|
*/
|
|
69
69
|
apiRouter.post('/projects/:id/index', async (req, res) => {
|
|
70
70
|
try {
|
|
@@ -77,6 +77,33 @@ apiRouter.post('/projects/:id/index', async (req, res) => {
|
|
|
77
77
|
res.status(500).json({ error: 'Failed to index project' });
|
|
78
78
|
}
|
|
79
79
|
});
|
|
80
|
+
/**
|
|
81
|
+
* POST /api/projects/:id/upload-index
|
|
82
|
+
* Загрузить индексированные данные с CLI
|
|
83
|
+
* CLI выполняет индексацию локально и отправляет результаты сюда
|
|
84
|
+
*/
|
|
85
|
+
apiRouter.post('/projects/:id/upload-index', async (req, res) => {
|
|
86
|
+
try {
|
|
87
|
+
const { id } = req.params;
|
|
88
|
+
const { asts, symbols, graph, fileContents, statistics } = req.body;
|
|
89
|
+
if (!asts || !symbols || !graph) {
|
|
90
|
+
res.status(400).json({ error: 'asts, symbols, and graph are required' });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const result = await projectService.uploadIndexedData(id, {
|
|
94
|
+
asts,
|
|
95
|
+
symbols,
|
|
96
|
+
graph,
|
|
97
|
+
fileContents,
|
|
98
|
+
statistics
|
|
99
|
+
});
|
|
100
|
+
res.json(result);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
Logger.error('Failed to upload indexed data:', error);
|
|
104
|
+
res.status(500).json({ error: 'Failed to upload indexed data' });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
80
107
|
/**
|
|
81
108
|
* GET /api/projects/:id/architecture
|
|
82
109
|
* Получить архитектурную модель (Digital Twin)
|
|
@@ -188,4 +188,47 @@ authRouter.get('/usage', authMiddleware, async (req, res) => {
|
|
|
188
188
|
res.status(500).json({ error: 'Failed to get usage' });
|
|
189
189
|
}
|
|
190
190
|
});
|
|
191
|
+
/**
|
|
192
|
+
* GET /api/auth/subscription
|
|
193
|
+
* Get current user's subscription details (for CLI /status)
|
|
194
|
+
*/
|
|
195
|
+
authRouter.get('/subscription', authMiddleware, async (req, res) => {
|
|
196
|
+
try {
|
|
197
|
+
if (!req.user) {
|
|
198
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const user = await authService.getUser(req.user.id);
|
|
202
|
+
if (!user) {
|
|
203
|
+
res.status(404).json({ error: 'User not found' });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Определяем лимиты по тарифу
|
|
207
|
+
const tierLimits = {
|
|
208
|
+
free: { requests: 50, projects: 2 },
|
|
209
|
+
pro: { requests: 500, projects: 10 },
|
|
210
|
+
team: { requests: 2000, projects: 50 },
|
|
211
|
+
enterprise: { requests: -1, projects: -1 }, // unlimited
|
|
212
|
+
admin: { requests: -1, projects: -1 }, // unlimited
|
|
213
|
+
};
|
|
214
|
+
const limits = tierLimits[user.tier] || tierLimits.free;
|
|
215
|
+
res.json({
|
|
216
|
+
tier: user.tier,
|
|
217
|
+
expiresAt: user.subscription?.endDate || null,
|
|
218
|
+
status: user.subscription?.status || 'active',
|
|
219
|
+
usage: {
|
|
220
|
+
used: user.usage?.requestsToday || 0,
|
|
221
|
+
limit: limits.requests,
|
|
222
|
+
},
|
|
223
|
+
limits: {
|
|
224
|
+
requestsPerDay: limits.requests,
|
|
225
|
+
maxProjects: limits.projects,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
Logger.error('Get subscription error:', error);
|
|
231
|
+
res.status(500).json({ error: 'Failed to get subscription' });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
191
234
|
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Authorization Flow for CLI
|
|
3
|
+
*
|
|
4
|
+
* OAuth 2.0 Device Authorization Grant (RFC 8628)
|
|
5
|
+
*/
|
|
6
|
+
import { Router } from 'express';
|
|
7
|
+
import { randomBytes } from 'crypto';
|
|
8
|
+
const router = Router();
|
|
9
|
+
const pendingAuths = new Map();
|
|
10
|
+
// Generate random codes
|
|
11
|
+
function generateDeviceCode() {
|
|
12
|
+
return randomBytes(32).toString('hex');
|
|
13
|
+
}
|
|
14
|
+
function generateUserCode() {
|
|
15
|
+
// User-friendly code: XXXX-XXXX
|
|
16
|
+
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Exclude confusing chars
|
|
17
|
+
let code = '';
|
|
18
|
+
for (let i = 0; i < 8; i++) {
|
|
19
|
+
if (i === 4)
|
|
20
|
+
code += '-';
|
|
21
|
+
code += chars[Math.floor(Math.random() * chars.length)];
|
|
22
|
+
}
|
|
23
|
+
return code;
|
|
24
|
+
}
|
|
25
|
+
// Clean up expired codes
|
|
26
|
+
function cleanupExpired() {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
for (const [code, auth] of pendingAuths.entries()) {
|
|
29
|
+
if (auth.expiresAt < now) {
|
|
30
|
+
pendingAuths.delete(code);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* POST /api/auth/device/code
|
|
36
|
+
* Request a new device code
|
|
37
|
+
*/
|
|
38
|
+
router.post('/code', (_req, res) => {
|
|
39
|
+
cleanupExpired();
|
|
40
|
+
const deviceCode = generateDeviceCode();
|
|
41
|
+
const userCode = generateUserCode();
|
|
42
|
+
const expiresIn = 600; // 10 minutes
|
|
43
|
+
const auth = {
|
|
44
|
+
deviceCode,
|
|
45
|
+
userCode,
|
|
46
|
+
expiresAt: Date.now() + expiresIn * 1000,
|
|
47
|
+
interval: 5,
|
|
48
|
+
status: 'pending',
|
|
49
|
+
};
|
|
50
|
+
pendingAuths.set(deviceCode, auth);
|
|
51
|
+
// Also index by user code for lookup
|
|
52
|
+
pendingAuths.set(userCode, auth);
|
|
53
|
+
const serverUrl = process.env.PUBLIC_URL || `http://${process.env.HOST || 'localhost'}:${process.env.PORT || 3000}`;
|
|
54
|
+
res.json({
|
|
55
|
+
deviceCode,
|
|
56
|
+
userCode,
|
|
57
|
+
verificationUrl: `${serverUrl}/auth/device?code=${userCode}`,
|
|
58
|
+
expiresIn,
|
|
59
|
+
interval: auth.interval,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* POST /api/auth/device/token
|
|
64
|
+
* Poll for token (called by CLI)
|
|
65
|
+
*/
|
|
66
|
+
router.post('/token', (req, res) => {
|
|
67
|
+
const { deviceCode } = req.body;
|
|
68
|
+
if (!deviceCode) {
|
|
69
|
+
res.status(400).json({ error: 'invalid_request', message: 'Missing device_code' });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const auth = pendingAuths.get(deviceCode);
|
|
73
|
+
if (!auth) {
|
|
74
|
+
res.status(400).json({ error: 'invalid_grant', message: 'Invalid or expired device code' });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (auth.expiresAt < Date.now()) {
|
|
78
|
+
pendingAuths.delete(deviceCode);
|
|
79
|
+
res.status(400).json({ error: 'expired_token', message: 'Device code expired' });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (auth.status === 'denied') {
|
|
83
|
+
pendingAuths.delete(deviceCode);
|
|
84
|
+
pendingAuths.delete(auth.userCode);
|
|
85
|
+
res.status(400).json({ error: 'access_denied', message: 'User denied authorization' });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (auth.status === 'pending') {
|
|
89
|
+
res.status(400).json({ error: 'authorization_pending', message: 'Waiting for user authorization' });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Authorized - return token
|
|
93
|
+
pendingAuths.delete(deviceCode);
|
|
94
|
+
pendingAuths.delete(auth.userCode);
|
|
95
|
+
res.json({
|
|
96
|
+
accessToken: auth.accessToken,
|
|
97
|
+
expiresIn: 86400 * 30, // 30 days
|
|
98
|
+
user: {
|
|
99
|
+
id: auth.userId,
|
|
100
|
+
email: 'user@example.com', // TODO: Get from user service
|
|
101
|
+
tier: 'free', // TODO: Get from user service
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
/**
|
|
106
|
+
* GET /api/auth/device/verify
|
|
107
|
+
* Get device auth info by user code (for web page)
|
|
108
|
+
*/
|
|
109
|
+
router.get('/verify/:userCode', (req, res) => {
|
|
110
|
+
const { userCode } = req.params;
|
|
111
|
+
const auth = pendingAuths.get(userCode.toUpperCase());
|
|
112
|
+
if (!auth || auth.expiresAt < Date.now()) {
|
|
113
|
+
res.status(404).json({ error: 'not_found', message: 'Invalid or expired code' });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
res.json({
|
|
117
|
+
userCode: auth.userCode,
|
|
118
|
+
status: auth.status,
|
|
119
|
+
expiresIn: Math.floor((auth.expiresAt - Date.now()) / 1000),
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
/**
|
|
123
|
+
* POST /api/auth/device/authorize
|
|
124
|
+
* Authorize device (called from web after user logs in)
|
|
125
|
+
*/
|
|
126
|
+
router.post('/authorize', (req, res) => {
|
|
127
|
+
const { userCode, userId, accessToken, action } = req.body;
|
|
128
|
+
if (!userCode) {
|
|
129
|
+
res.status(400).json({ error: 'invalid_request', message: 'Missing user_code' });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const auth = pendingAuths.get(userCode.toUpperCase());
|
|
133
|
+
if (!auth || auth.expiresAt < Date.now()) {
|
|
134
|
+
res.status(404).json({ error: 'not_found', message: 'Invalid or expired code' });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (action === 'deny') {
|
|
138
|
+
auth.status = 'denied';
|
|
139
|
+
res.json({ success: true, message: 'Authorization denied' });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Authorize
|
|
143
|
+
auth.status = 'authorized';
|
|
144
|
+
auth.userId = userId;
|
|
145
|
+
auth.accessToken = accessToken;
|
|
146
|
+
res.json({ success: true, message: 'Device authorized' });
|
|
147
|
+
});
|
|
148
|
+
export default router;
|
|
149
|
+
//# sourceMappingURL=device-auth.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Сервис для управления проектами в ArchiCore.
|
|
5
5
|
* Связывает API с основными модулями системы.
|
|
6
6
|
*/
|
|
7
|
-
import { ChangeImpact } from '../../types/index.js';
|
|
7
|
+
import { ChangeImpact, Symbol, ASTNode } from '../../types/index.js';
|
|
8
8
|
import { RulesCheckResult } from '../../rules-engine/index.js';
|
|
9
9
|
import { ProjectMetrics } from '../../metrics/index.js';
|
|
10
10
|
import { DeadCodeResult } from '../../analyzers/dead-code.js';
|
|
@@ -76,6 +76,30 @@ export declare class ProjectService {
|
|
|
76
76
|
memoryStats: unknown;
|
|
77
77
|
}>;
|
|
78
78
|
deleteProject(projectId: string): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Загрузить индексированные данные с CLI
|
|
81
|
+
* CLI выполняет парсинг локально и отправляет результаты на сервер
|
|
82
|
+
*/
|
|
83
|
+
uploadIndexedData(projectId: string, indexedData: {
|
|
84
|
+
asts: Array<[string, ASTNode]>;
|
|
85
|
+
symbols: Array<[string, Symbol]>;
|
|
86
|
+
graph: {
|
|
87
|
+
nodes: Array<[string, unknown]>;
|
|
88
|
+
edges: Array<[string, unknown[]]>;
|
|
89
|
+
};
|
|
90
|
+
fileContents?: Array<[string, string]>;
|
|
91
|
+
statistics: {
|
|
92
|
+
totalFiles: number;
|
|
93
|
+
totalSymbols: number;
|
|
94
|
+
};
|
|
95
|
+
}): Promise<{
|
|
96
|
+
success: boolean;
|
|
97
|
+
statistics: ProjectStats;
|
|
98
|
+
}>;
|
|
99
|
+
/**
|
|
100
|
+
* Загрузить сохранённые индексные данные с диска
|
|
101
|
+
*/
|
|
102
|
+
loadIndexedDataFromDisk(projectId: string): Promise<boolean>;
|
|
79
103
|
/**
|
|
80
104
|
* Получить метрики кода
|
|
81
105
|
*/
|
|
@@ -117,6 +141,8 @@ export declare class ProjectService {
|
|
|
117
141
|
}>;
|
|
118
142
|
/**
|
|
119
143
|
* Получить содержимое файлов проекта
|
|
144
|
+
* Сначала пробуем загрузить из сохранённого файла (для CLI),
|
|
145
|
+
* затем пробуем читать из файловой системы (для локальных проектов)
|
|
120
146
|
*/
|
|
121
147
|
private getFileContents;
|
|
122
148
|
}
|
|
@@ -371,6 +371,150 @@ export class ProjectService {
|
|
|
371
371
|
await this.saveProjects();
|
|
372
372
|
Logger.info(`Deleted project: ${projectId}`);
|
|
373
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Загрузить индексированные данные с CLI
|
|
376
|
+
* CLI выполняет парсинг локально и отправляет результаты на сервер
|
|
377
|
+
*/
|
|
378
|
+
async uploadIndexedData(projectId, indexedData) {
|
|
379
|
+
const project = this.projects.get(projectId);
|
|
380
|
+
if (!project) {
|
|
381
|
+
throw new Error(`Project not found: ${projectId}`);
|
|
382
|
+
}
|
|
383
|
+
project.status = 'indexing';
|
|
384
|
+
await this.saveProjects();
|
|
385
|
+
try {
|
|
386
|
+
const data = await this.getProjectData(projectId);
|
|
387
|
+
Logger.progress(`Uploading indexed data for project: ${project.name}`);
|
|
388
|
+
// Восстанавливаем Maps из массивов
|
|
389
|
+
const asts = new Map(indexedData.asts);
|
|
390
|
+
const symbols = new Map(indexedData.symbols);
|
|
391
|
+
// Восстанавливаем граф с правильными типами
|
|
392
|
+
const graphNodes = new Map();
|
|
393
|
+
for (const [key, value] of indexedData.graph.nodes) {
|
|
394
|
+
const node = value;
|
|
395
|
+
graphNodes.set(key, {
|
|
396
|
+
id: node.id,
|
|
397
|
+
name: node.name,
|
|
398
|
+
type: node.type || 'file',
|
|
399
|
+
filePath: node.filePath,
|
|
400
|
+
metadata: node.metadata || {}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
const graphEdges = new Map();
|
|
404
|
+
for (const [key, edges] of indexedData.graph.edges) {
|
|
405
|
+
graphEdges.set(key, edges.map(e => ({
|
|
406
|
+
from: key,
|
|
407
|
+
to: e.to,
|
|
408
|
+
type: e.type,
|
|
409
|
+
weight: e.weight || 1
|
|
410
|
+
})));
|
|
411
|
+
}
|
|
412
|
+
const graph = {
|
|
413
|
+
nodes: graphNodes,
|
|
414
|
+
edges: graphEdges
|
|
415
|
+
};
|
|
416
|
+
// Сохраняем в данные проекта
|
|
417
|
+
data.asts = asts;
|
|
418
|
+
data.symbols = symbols;
|
|
419
|
+
data.graph = graph;
|
|
420
|
+
// Сохраняем содержимое файлов если передано
|
|
421
|
+
if (indexedData.fileContents) {
|
|
422
|
+
const fileContentsMap = new Map(indexedData.fileContents);
|
|
423
|
+
// Сохраняем в файловую систему сервера для анализаторов
|
|
424
|
+
const projectDataDir = path.join(this.dataDir, projectId);
|
|
425
|
+
if (!existsSync(projectDataDir)) {
|
|
426
|
+
await mkdir(projectDataDir, { recursive: true });
|
|
427
|
+
}
|
|
428
|
+
await writeFile(path.join(projectDataDir, 'file-contents.json'), JSON.stringify(Array.from(fileContentsMap.entries())));
|
|
429
|
+
}
|
|
430
|
+
// Индексация в семантическую память (если доступна)
|
|
431
|
+
if (data.semanticMemory) {
|
|
432
|
+
await data.semanticMemory.initialize();
|
|
433
|
+
await data.semanticMemory.indexSymbols(symbols, asts);
|
|
434
|
+
}
|
|
435
|
+
// Сохраняем индексные данные на диск
|
|
436
|
+
const projectDataDir = path.join(this.dataDir, projectId);
|
|
437
|
+
if (!existsSync(projectDataDir)) {
|
|
438
|
+
await mkdir(projectDataDir, { recursive: true });
|
|
439
|
+
}
|
|
440
|
+
await writeFile(path.join(projectDataDir, 'asts.json'), JSON.stringify(indexedData.asts));
|
|
441
|
+
await writeFile(path.join(projectDataDir, 'symbols.json'), JSON.stringify(indexedData.symbols));
|
|
442
|
+
await writeFile(path.join(projectDataDir, 'graph.json'), JSON.stringify(indexedData.graph));
|
|
443
|
+
// Обновляем статус и статистику
|
|
444
|
+
const stats = {
|
|
445
|
+
filesCount: indexedData.statistics.totalFiles,
|
|
446
|
+
symbolsCount: indexedData.statistics.totalSymbols,
|
|
447
|
+
nodesCount: graph.nodes.size,
|
|
448
|
+
edgesCount: Array.from(graph.edges.values()).reduce((sum, edges) => sum + edges.length, 0)
|
|
449
|
+
};
|
|
450
|
+
project.status = 'ready';
|
|
451
|
+
project.lastIndexedAt = new Date().toISOString();
|
|
452
|
+
project.stats = stats;
|
|
453
|
+
await this.saveProjects();
|
|
454
|
+
Logger.success(`Index uploaded: ${stats.filesCount} files, ${stats.symbolsCount} symbols`);
|
|
455
|
+
return { success: true, statistics: stats };
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
project.status = 'error';
|
|
459
|
+
await this.saveProjects();
|
|
460
|
+
Logger.error('Upload indexing failed:', error);
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Загрузить сохранённые индексные данные с диска
|
|
466
|
+
*/
|
|
467
|
+
async loadIndexedDataFromDisk(projectId) {
|
|
468
|
+
const project = this.projects.get(projectId);
|
|
469
|
+
if (!project)
|
|
470
|
+
return false;
|
|
471
|
+
const projectDataDir = path.join(this.dataDir, projectId);
|
|
472
|
+
const astsPath = path.join(projectDataDir, 'asts.json');
|
|
473
|
+
const symbolsPath = path.join(projectDataDir, 'symbols.json');
|
|
474
|
+
const graphPath = path.join(projectDataDir, 'graph.json');
|
|
475
|
+
if (!existsSync(astsPath) || !existsSync(symbolsPath) || !existsSync(graphPath)) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
const data = await this.getProjectData(projectId);
|
|
480
|
+
const astsData = JSON.parse(await readFile(astsPath, 'utf-8'));
|
|
481
|
+
const symbolsData = JSON.parse(await readFile(symbolsPath, 'utf-8'));
|
|
482
|
+
const graphData = JSON.parse(await readFile(graphPath, 'utf-8'));
|
|
483
|
+
data.asts = new Map(astsData);
|
|
484
|
+
data.symbols = new Map(symbolsData);
|
|
485
|
+
// Восстанавливаем граф с правильными типами
|
|
486
|
+
const graphNodes = new Map();
|
|
487
|
+
for (const [key, value] of graphData.nodes) {
|
|
488
|
+
const node = value;
|
|
489
|
+
graphNodes.set(key, {
|
|
490
|
+
id: node.id,
|
|
491
|
+
name: node.name,
|
|
492
|
+
type: node.type || 'file',
|
|
493
|
+
filePath: node.filePath,
|
|
494
|
+
metadata: node.metadata || {}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
const graphEdges = new Map();
|
|
498
|
+
for (const [key, edges] of graphData.edges) {
|
|
499
|
+
graphEdges.set(key, edges.map(e => ({
|
|
500
|
+
from: key,
|
|
501
|
+
to: e.to,
|
|
502
|
+
type: e.type,
|
|
503
|
+
weight: e.weight || 1
|
|
504
|
+
})));
|
|
505
|
+
}
|
|
506
|
+
data.graph = {
|
|
507
|
+
nodes: graphNodes,
|
|
508
|
+
edges: graphEdges
|
|
509
|
+
};
|
|
510
|
+
Logger.info(`Loaded indexed data from disk for project: ${project.name}`);
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
Logger.error('Failed to load indexed data from disk:', error);
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
374
518
|
/**
|
|
375
519
|
* Получить метрики кода
|
|
376
520
|
*/
|
|
@@ -503,12 +647,28 @@ export class ProjectService {
|
|
|
503
647
|
}
|
|
504
648
|
/**
|
|
505
649
|
* Получить содержимое файлов проекта
|
|
650
|
+
* Сначала пробуем загрузить из сохранённого файла (для CLI),
|
|
651
|
+
* затем пробуем читать из файловой системы (для локальных проектов)
|
|
506
652
|
*/
|
|
507
653
|
async getFileContents(projectId) {
|
|
508
654
|
const project = this.projects.get(projectId);
|
|
509
655
|
if (!project) {
|
|
510
656
|
throw new Error(`Project not found: ${projectId}`);
|
|
511
657
|
}
|
|
658
|
+
// Сначала пробуем загрузить из сохранённого файла (для CLI проектов)
|
|
659
|
+
const projectDataDir = path.join(this.dataDir, projectId);
|
|
660
|
+
const savedContentsPath = path.join(projectDataDir, 'file-contents.json');
|
|
661
|
+
if (existsSync(savedContentsPath)) {
|
|
662
|
+
try {
|
|
663
|
+
const contents = JSON.parse(await readFile(savedContentsPath, 'utf-8'));
|
|
664
|
+
Logger.debug(`Loaded file contents from saved data for project: ${project.name}`);
|
|
665
|
+
return new Map(contents);
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
Logger.warn('Failed to load saved file contents, trying filesystem');
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
// Fallback: читаем из файловой системы (для локальных проектов на сервере)
|
|
512
672
|
const data = await this.getProjectData(projectId);
|
|
513
673
|
const fileContents = new Map();
|
|
514
674
|
if (data.asts) {
|