archicore 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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: npm run server');
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
- // Try to register project first
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: { 'Content-Type': 'application/json' },
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
- spinner.stop();
216
+ registerSpinner.succeed('Project registered');
189
217
  }
190
218
  catch {
191
- spinner.fail('Failed to register project');
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
- const spinner = createSpinner('Indexing project...').start();
227
+ // Локальная индексация
228
+ const indexSpinner = createSpinner('Parsing local files...').start();
200
229
  try {
201
- const config = await loadConfig();
202
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/index`, {
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
- throw new Error('Indexing failed');
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
- spinner.succeed('Project indexed');
210
- printKeyValue('Files', String(data.statistics?.totalFiles || 0));
211
- printKeyValue('Symbols', String(data.statistics?.totalSymbols || 0));
212
- // Update local config
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
- const fs = await import('fs/promises');
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
- spinner.fail('Indexing failed');
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
@@ -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'],
@@ -5,6 +5,8 @@ export interface CLIConfig {
5
5
  activeProjectId?: string;
6
6
  activeProjectPath?: string;
7
7
  serverUrl: string;
8
+ accessToken?: string;
9
+ refreshToken?: string;
8
10
  colorOutput: boolean;
9
11
  verboseOutput: boolean;
10
12
  llmProvider?: 'openai' | 'anthropic' | 'deepseek';
@@ -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)
@@ -74,6 +77,11 @@ export class ArchiCoreServer {
74
77
  timestamp: new Date().toISOString()
75
78
  });
76
79
  });
80
+ // Device authorization page for CLI
81
+ this.app.get('/auth/device', (_req, res) => {
82
+ const deviceAuthPath = path.join(__dirname, '../../public/device-auth.html');
83
+ res.sendFile(deviceAuthPath);
84
+ });
77
85
  // SPA fallback - все остальные маршруты отдают index.html
78
86
  this.app.get('/*splat', (_req, res) => {
79
87
  const indexPath = path.join(__dirname, '../../public/index.html');
@@ -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,8 @@
1
+ /**
2
+ * Device Authorization Flow for CLI
3
+ *
4
+ * OAuth 2.0 Device Authorization Grant (RFC 8628)
5
+ */
6
+ declare const router: import("express-serve-static-core").Router;
7
+ export default router;
8
+ //# sourceMappingURL=device-auth.d.ts.map
@@ -0,0 +1,164 @@
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
+ import { AuthService } from '../services/auth-service.js';
9
+ const router = Router();
10
+ const authService = new AuthService();
11
+ const pendingAuths = new Map();
12
+ // Generate random codes
13
+ function generateDeviceCode() {
14
+ return randomBytes(32).toString('hex');
15
+ }
16
+ function generateUserCode() {
17
+ // User-friendly code: XXXX-XXXX
18
+ const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Exclude confusing chars
19
+ let code = '';
20
+ for (let i = 0; i < 8; i++) {
21
+ if (i === 4)
22
+ code += '-';
23
+ code += chars[Math.floor(Math.random() * chars.length)];
24
+ }
25
+ return code;
26
+ }
27
+ // Clean up expired codes
28
+ function cleanupExpired() {
29
+ const now = Date.now();
30
+ for (const [code, auth] of pendingAuths.entries()) {
31
+ if (auth.expiresAt < now) {
32
+ pendingAuths.delete(code);
33
+ }
34
+ }
35
+ }
36
+ /**
37
+ * POST /api/auth/device/code
38
+ * Request a new device code
39
+ */
40
+ router.post('/code', (_req, res) => {
41
+ cleanupExpired();
42
+ const deviceCode = generateDeviceCode();
43
+ const userCode = generateUserCode();
44
+ const expiresIn = 600; // 10 minutes
45
+ const auth = {
46
+ deviceCode,
47
+ userCode,
48
+ expiresAt: Date.now() + expiresIn * 1000,
49
+ interval: 5,
50
+ status: 'pending',
51
+ };
52
+ pendingAuths.set(deviceCode, auth);
53
+ // Also index by user code for lookup
54
+ pendingAuths.set(userCode, auth);
55
+ const serverUrl = process.env.PUBLIC_URL || `http://${process.env.HOST || 'localhost'}:${process.env.PORT || 3000}`;
56
+ res.json({
57
+ deviceCode,
58
+ userCode,
59
+ verificationUrl: `${serverUrl}/auth/device?code=${userCode}`,
60
+ expiresIn,
61
+ interval: auth.interval,
62
+ });
63
+ });
64
+ /**
65
+ * POST /api/auth/device/token
66
+ * Poll for token (called by CLI)
67
+ */
68
+ router.post('/token', (req, res) => {
69
+ const { deviceCode } = req.body;
70
+ if (!deviceCode) {
71
+ res.status(400).json({ error: 'invalid_request', message: 'Missing device_code' });
72
+ return;
73
+ }
74
+ const auth = pendingAuths.get(deviceCode);
75
+ if (!auth) {
76
+ res.status(400).json({ error: 'invalid_grant', message: 'Invalid or expired device code' });
77
+ return;
78
+ }
79
+ if (auth.expiresAt < Date.now()) {
80
+ pendingAuths.delete(deviceCode);
81
+ res.status(400).json({ error: 'expired_token', message: 'Device code expired' });
82
+ return;
83
+ }
84
+ if (auth.status === 'denied') {
85
+ pendingAuths.delete(deviceCode);
86
+ pendingAuths.delete(auth.userCode);
87
+ res.status(400).json({ error: 'access_denied', message: 'User denied authorization' });
88
+ return;
89
+ }
90
+ if (auth.status === 'pending') {
91
+ res.status(400).json({ error: 'authorization_pending', message: 'Waiting for user authorization' });
92
+ return;
93
+ }
94
+ // Authorized - return token
95
+ pendingAuths.delete(deviceCode);
96
+ pendingAuths.delete(auth.userCode);
97
+ res.json({
98
+ accessToken: auth.accessToken,
99
+ expiresIn: 86400 * 30, // 30 days
100
+ user: {
101
+ id: auth.userId,
102
+ email: auth.userEmail || 'user@example.com',
103
+ name: auth.userName,
104
+ tier: auth.userTier || 'free',
105
+ },
106
+ });
107
+ });
108
+ /**
109
+ * GET /api/auth/device/verify
110
+ * Get device auth info by user code (for web page)
111
+ */
112
+ router.get('/verify/:userCode', (req, res) => {
113
+ const { userCode } = req.params;
114
+ const auth = pendingAuths.get(userCode.toUpperCase());
115
+ if (!auth || auth.expiresAt < Date.now()) {
116
+ res.status(404).json({ error: 'not_found', message: 'Invalid or expired code' });
117
+ return;
118
+ }
119
+ res.json({
120
+ userCode: auth.userCode,
121
+ status: auth.status,
122
+ expiresIn: Math.floor((auth.expiresAt - Date.now()) / 1000),
123
+ });
124
+ });
125
+ /**
126
+ * POST /api/auth/device/authorize
127
+ * Authorize device (called from web after user logs in)
128
+ */
129
+ router.post('/authorize', async (req, res) => {
130
+ const { userCode, userId, accessToken, action } = req.body;
131
+ if (!userCode) {
132
+ res.status(400).json({ error: 'invalid_request', message: 'Missing user_code' });
133
+ return;
134
+ }
135
+ const auth = pendingAuths.get(userCode.toUpperCase());
136
+ if (!auth || auth.expiresAt < Date.now()) {
137
+ res.status(404).json({ error: 'not_found', message: 'Invalid or expired code' });
138
+ return;
139
+ }
140
+ if (action === 'deny') {
141
+ auth.status = 'denied';
142
+ res.json({ success: true, message: 'Authorization denied' });
143
+ return;
144
+ }
145
+ // Get user info from token
146
+ try {
147
+ const user = await authService.validateToken(accessToken);
148
+ if (user) {
149
+ auth.userEmail = user.email;
150
+ auth.userName = user.username;
151
+ auth.userTier = user.tier;
152
+ }
153
+ }
154
+ catch {
155
+ // Ignore errors, use userId from request
156
+ }
157
+ // Authorize
158
+ auth.status = 'authorized';
159
+ auth.userId = userId;
160
+ auth.accessToken = accessToken;
161
+ res.json({ success: true, message: 'Device authorized' });
162
+ });
163
+ export default router;
164
+ //# 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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archicore",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "AI Software Architect - code analysis, impact prediction, semantic search",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",