blockmine 1.22.0 → 1.23.1

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.
Files changed (108) hide show
  1. package/.claude/agents/code-architect.md +34 -0
  2. package/.claude/agents/code-explorer.md +51 -0
  3. package/.claude/agents/code-reviewer.md +46 -0
  4. package/.claude/commands/feature-dev.md +125 -0
  5. package/.claude/settings.json +5 -1
  6. package/.claude/settings.local.json +12 -1
  7. package/.claude/skills/frontend-design/SKILL.md +42 -0
  8. package/CHANGELOG.md +32 -1
  9. package/README.md +302 -152
  10. package/backend/package-lock.json +681 -9
  11. package/backend/package.json +8 -0
  12. package/backend/prisma/migrations/20251116111851_add_execution_trace/migration.sql +22 -0
  13. package/backend/prisma/migrations/20251120154914_add_panel_api_keys/migration.sql +21 -0
  14. package/backend/prisma/migrations/20251121110241_add_proxy_table/migration.sql +45 -0
  15. package/backend/prisma/schema.prisma +70 -1
  16. package/backend/src/__tests__/services/BotLifecycleService.test.js +9 -4
  17. package/backend/src/ai/plugin-assistant-system-prompt.md +788 -0
  18. package/backend/src/api/middleware/auth.js +27 -0
  19. package/backend/src/api/middleware/botAccess.js +7 -3
  20. package/backend/src/api/middleware/panelApiAuth.js +135 -0
  21. package/backend/src/api/routes/aiAssistant.js +995 -0
  22. package/backend/src/api/routes/auth.js +90 -54
  23. package/backend/src/api/routes/botCommands.js +107 -0
  24. package/backend/src/api/routes/botGroups.js +165 -0
  25. package/backend/src/api/routes/botHistory.js +108 -0
  26. package/backend/src/api/routes/botPermissions.js +99 -0
  27. package/backend/src/api/routes/botStatus.js +36 -0
  28. package/backend/src/api/routes/botUsers.js +162 -0
  29. package/backend/src/api/routes/bots.js +108 -59
  30. package/backend/src/api/routes/eventGraphs.js +4 -1
  31. package/backend/src/api/routes/logs.js +13 -3
  32. package/backend/src/api/routes/panel.js +3 -3
  33. package/backend/src/api/routes/panelApiKeys.js +179 -0
  34. package/backend/src/api/routes/pluginIde.js +1715 -135
  35. package/backend/src/api/routes/plugins.js +170 -13
  36. package/backend/src/api/routes/proxies.js +130 -0
  37. package/backend/src/api/routes/search.js +4 -0
  38. package/backend/src/api/routes/servers.js +20 -3
  39. package/backend/src/api/routes/settings.js +5 -0
  40. package/backend/src/api/routes/system.js +3 -3
  41. package/backend/src/api/routes/traces.js +131 -0
  42. package/backend/src/config/debug.config.js +36 -0
  43. package/backend/src/core/BotHistoryStore.js +180 -0
  44. package/backend/src/core/BotManager.js +14 -4
  45. package/backend/src/core/BotProcess.js +1517 -1092
  46. package/backend/src/core/EventGraphManager.js +194 -280
  47. package/backend/src/core/GraphExecutionEngine.js +1004 -321
  48. package/backend/src/core/MessageQueue.js +12 -6
  49. package/backend/src/core/PluginLoader.js +99 -5
  50. package/backend/src/core/PluginManager.js +74 -13
  51. package/backend/src/core/TaskScheduler.js +1 -1
  52. package/backend/src/core/commands/whois.js +1 -1
  53. package/backend/src/core/node-registries/actions.js +72 -2
  54. package/backend/src/core/node-registries/arrays.js +18 -0
  55. package/backend/src/core/node-registries/data.js +1 -1
  56. package/backend/src/core/node-registries/events.js +14 -0
  57. package/backend/src/core/node-registries/logic.js +17 -0
  58. package/backend/src/core/node-registries/strings.js +34 -0
  59. package/backend/src/core/node-registries/type.js +25 -0
  60. package/backend/src/core/nodes/actions/bot_look_at.js +1 -1
  61. package/backend/src/core/nodes/actions/create_command.js +189 -0
  62. package/backend/src/core/nodes/actions/delete_command.js +92 -0
  63. package/backend/src/core/nodes/actions/http_request.js +23 -4
  64. package/backend/src/core/nodes/actions/send_message.js +2 -12
  65. package/backend/src/core/nodes/actions/update_command.js +133 -0
  66. package/backend/src/core/nodes/arrays/join.js +28 -0
  67. package/backend/src/core/nodes/data/cast.js +2 -1
  68. package/backend/src/core/nodes/data/string_literal.js +2 -13
  69. package/backend/src/core/nodes/logic/not.js +22 -0
  70. package/backend/src/core/nodes/strings/starts_with.js +1 -1
  71. package/backend/src/core/nodes/strings/to_lower.js +22 -0
  72. package/backend/src/core/nodes/strings/to_upper.js +22 -0
  73. package/backend/src/core/nodes/type/to_string.js +32 -0
  74. package/backend/src/core/services/BotLifecycleService.js +835 -596
  75. package/backend/src/core/services/CommandExecutionService.js +430 -351
  76. package/backend/src/core/services/DebugSessionManager.js +347 -0
  77. package/backend/src/core/services/GraphCollaborationManager.js +501 -0
  78. package/backend/src/core/services/MinecraftBotManager.js +259 -0
  79. package/backend/src/core/services/MinecraftViewerService.js +216 -0
  80. package/backend/src/core/services/TraceCollectorService.js +545 -0
  81. package/backend/src/core/system/RuntimeCommandRegistry.js +116 -0
  82. package/backend/src/core/system/Transport.js +0 -4
  83. package/backend/src/core/validation/nodeSchemas.js +6 -6
  84. package/backend/src/real-time/botApi/handlers/graphHandlers.js +2 -2
  85. package/backend/src/real-time/botApi/handlers/graphWebSocketHandlers.js +1 -1
  86. package/backend/src/real-time/botApi/utils.js +11 -0
  87. package/backend/src/real-time/panelNamespace.js +387 -0
  88. package/backend/src/real-time/presence.js +7 -2
  89. package/backend/src/real-time/socketHandler.js +395 -4
  90. package/backend/src/server.js +18 -0
  91. package/frontend/dist/assets/index-DqzDkFsP.js +11210 -0
  92. package/frontend/dist/assets/index-t6K1u4OV.css +32 -0
  93. package/frontend/dist/index.html +2 -2
  94. package/frontend/package-lock.json +9437 -0
  95. package/frontend/package.json +8 -0
  96. package/package.json +2 -2
  97. package/screen/console.png +0 -0
  98. package/screen/dashboard.png +0 -0
  99. package/screen/graph_collabe.png +0 -0
  100. package/screen/graph_live_debug.png +0 -0
  101. package/screen/management_command.png +0 -0
  102. package/screen/node_debug_trace.png +0 -0
  103. package/screen/plugin_/320/276/320/261/320/267/320/276/321/200.png +0 -0
  104. package/screen/websocket.png +0 -0
  105. package/screen//320/275/320/260/321/201/321/202/321/200/320/276/320/271/320/272/320/270_/320/276/321/202/320/264/320/265/320/273/321/214/320/275/321/213/321/205_/320/272/320/276/320/274/320/260/320/275/320/264_/320/272/320/260/320/266/320/264/321/203_/320/272/320/276/320/274/320/260/320/275/320/273/320/264/321/203_/320/274/320/276/320/266/320/275/320/276_/320/275/320/260/321/201/321/202/321/200/320/260/320/270/320/262/320/260/321/202/321/214.png +0 -0
  106. package/screen//320/277/320/273/320/260/320/275/320/270/321/200/320/276/320/262/321/211/320/270/320/272_/320/274/320/276/320/266/320/275/320/276_/320/267/320/260/320/264/320/260/320/262/320/260/321/202/321/214_/320/264/320/265/320/271/321/201/321/202/320/262/320/270/321/217_/320/277/320/276_/320/262/321/200/320/265/320/274/320/265/320/275/320/270.png +0 -0
  107. package/frontend/dist/assets/index-CfTo92bP.css +0 -1
  108. package/frontend/dist/assets/index-CiFD5X9Z.js +0 -8344
@@ -1,10 +1,12 @@
1
1
  const express = require('express');
2
- const { authenticate, authorize } = require('../middleware/auth');
2
+ const { authenticate } = require('../middleware/auth');
3
3
  const fse = require('fs-extra');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
6
  const { PrismaClient } = require('@prisma/client');
7
7
  const slugify = require('slugify');
8
+ const { Octokit } = require('@octokit/rest');
9
+ const aiAssistantRouter = require('./aiAssistant');
8
10
 
9
11
  const prisma = new PrismaClient();
10
12
  const router = express.Router({ mergeParams: true });
@@ -12,7 +14,17 @@ const router = express.Router({ mergeParams: true });
12
14
  const DATA_DIR = path.join(os.homedir(), '.blockmine');
13
15
  const PLUGINS_BASE_DIR = path.join(DATA_DIR, 'storage', 'plugins');
14
16
 
15
- router.use(authenticate, authorize('plugin:develop'));
17
+ router.use(authenticate);
18
+
19
+ // Debug middleware для логирования всех запросов
20
+ router.use((req, res, next) => {
21
+ console.log('[Plugin IDE] Request:', req.method, req.path, 'Params:', req.params);
22
+ next();
23
+ });
24
+
25
+ // Подключаем AI Assistant роуты ПЕРЕД другими роутами
26
+ console.log('[Plugin IDE] Mounting AI Assistant router');
27
+ router.use('/:pluginName/ai', aiAssistantRouter);
16
28
 
17
29
  const resolvePluginPath = async (req, res, next) => {
18
30
  try {
@@ -20,24 +32,24 @@ const resolvePluginPath = async (req, res, next) => {
20
32
  if (!pluginName) {
21
33
  return res.status(400).json({ error: 'Имя плагина обязательно в пути.' });
22
34
  }
23
-
35
+
24
36
  const plugin = await prisma.installedPlugin.findFirst({
25
- where: {
26
- botId: parseInt(botId),
27
- name: pluginName
37
+ where: {
38
+ botId: parseInt(botId),
39
+ name: pluginName
28
40
  }
29
41
  });
30
-
42
+
31
43
  if (!plugin) {
32
44
  return res.status(404).json({ error: 'Плагин не найден в базе данных.' });
33
45
  }
34
-
46
+
35
47
  const pluginPath = plugin.path;
36
-
48
+
37
49
  if (!await fse.pathExists(pluginPath)) {
38
50
  return res.status(404).json({ error: 'Директория плагина не найдена в файловой системе.' });
39
51
  }
40
-
52
+
41
53
  req.pluginPath = pluginPath;
42
54
  req.pluginData = plugin;
43
55
  next();
@@ -61,7 +73,7 @@ router.post('/create', async (req, res) => {
61
73
  if (!name) {
62
74
  return res.status(400).json({ error: 'Имя плагина обязательно.' });
63
75
  }
64
-
76
+
65
77
  const pluginNameSlug = slugify(name, { lower: true, strict: true });
66
78
  const botPluginsDir = path.join(PLUGINS_BASE_DIR, `bot_${botId}`);
67
79
  const pluginPath = path.join(botPluginsDir, pluginNameSlug);
@@ -77,7 +89,7 @@ router.post('/create', async (req, res) => {
77
89
  if (existingPlugin) {
78
90
  return res.status(409).json({ error: `Плагин с именем "${pluginNameSlug}" уже зарегистрирован для этого бота.` });
79
91
  }
80
-
92
+
81
93
  await fse.mkdirp(pluginPath);
82
94
 
83
95
  const packageJson = {
@@ -85,8 +97,12 @@ router.post('/create', async (req, res) => {
85
97
  version,
86
98
  description,
87
99
  author,
100
+ keywords: ['blockmine', 'blockmine-plugin', 'minecraft', 'mineflayer'],
88
101
  botpanel: {
89
102
  main: 'index.js',
103
+ categories: [],
104
+ supportedHosts: [],
105
+ dependencies: [],
90
106
  settings: {
91
107
  "helloMessage": {
92
108
  "type": "string",
@@ -101,7 +117,7 @@ router.post('/create', async (req, res) => {
101
117
 
102
118
  if (template === 'command') {
103
119
  await fse.mkdirp(path.join(pluginPath, 'commands'));
104
-
120
+
105
121
  const commandContent = `module.exports = (bot) => {
106
122
  const Command = bot.api.Command;
107
123
 
@@ -227,7 +243,7 @@ module.exports = {
227
243
  isEnabled: true
228
244
  }
229
245
  });
230
-
246
+
231
247
  res.status(201).json(newPlugin);
232
248
 
233
249
  } catch (error) {
@@ -265,6 +281,74 @@ const readDirectoryRecursive = async (basePath, currentPath = '') => {
265
281
 
266
282
  router.get('/:pluginName/structure', resolvePluginPath, async (req, res) => {
267
283
  try {
284
+ // Автоматическая инициализация Git если есть repository URL
285
+ const packageJsonPath = path.join(req.pluginPath, 'package.json');
286
+ const gitPath = path.join(req.pluginPath, '.git');
287
+ const gitignorePath = path.join(req.pluginPath, '.gitignore');
288
+
289
+ if (await fse.pathExists(packageJsonPath) && !await fse.pathExists(gitPath)) {
290
+ try {
291
+ const packageJson = await fse.readJson(packageJsonPath);
292
+ const repositoryUrl = packageJson.repository?.url || packageJson.repository;
293
+
294
+ if (repositoryUrl && typeof repositoryUrl === 'string') {
295
+ const { execSync } = require('child_process');
296
+
297
+ // Валидация URL репозитория
298
+ const urlPattern = /^(https?|git):\/\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&'()*+,;=%]+$/;
299
+ const cleanUrl = repositoryUrl.replace(/^git\+/, '');
300
+
301
+ if (!urlPattern.test(cleanUrl)) {
302
+ console.warn(`[Plugin IDE] Invalid repository URL format: ${cleanUrl}`);
303
+ throw new Error('Invalid repository URL');
304
+ }
305
+
306
+ execSync('git init', { cwd: req.pluginPath });
307
+ // Используем массив аргументов для безопасности
308
+ execSync('git remote add origin ' + JSON.stringify(cleanUrl), { cwd: req.pluginPath });
309
+
310
+ console.log(`[Plugin IDE] Initialized git repository for ${req.params.pluginName} with remote: ${cleanUrl}`);
311
+ }
312
+ } catch (gitError) {
313
+ // Не критично, продолжаем без git
314
+ console.warn(`[Plugin IDE] Failed to auto-init git for ${req.params.pluginName}:`, gitError.message);
315
+ }
316
+ }
317
+
318
+ // Автоматическое создание .gitignore если его нет
319
+ if (!await fse.pathExists(gitignorePath)) {
320
+ const defaultGitignore = `# Dependencies
321
+ node_modules/
322
+ package-lock.json
323
+
324
+ # Environment variables
325
+ .env
326
+ .env.local
327
+ .env.*.local
328
+
329
+ # Logs
330
+ logs/
331
+ *.log
332
+
333
+ # IDE
334
+ .vscode/
335
+ .idea/
336
+ *.swp
337
+ *.swo
338
+ *~
339
+
340
+ # Coverage
341
+ coverage/
342
+ .nyc_output/
343
+ `;
344
+ try {
345
+ await fse.writeFile(gitignorePath, defaultGitignore, 'utf8');
346
+ console.log(`[Plugin IDE] Created .gitignore for ${req.params.pluginName}`);
347
+ } catch (gitignoreError) {
348
+ console.warn(`[Plugin IDE] Failed to create .gitignore for ${req.params.pluginName}:`, gitignoreError.message);
349
+ }
350
+ }
351
+
268
352
  const structure = await readDirectoryRecursive(req.pluginPath);
269
353
  res.json(structure);
270
354
  } catch (error) {
@@ -292,29 +376,30 @@ router.post('/:pluginName/file', resolvePluginPath, async (req, res) => {
292
376
  if (!relativePath || content === undefined) {
293
377
  return res.status(400).json({ error: 'File path and content are required.' });
294
378
  }
295
-
379
+
296
380
  const safePath = path.resolve(req.pluginPath, relativePath);
297
381
 
298
382
  if (!safePath.startsWith(req.pluginPath)) {
299
383
  return res.status(403).json({ error: 'Доступ запрещен.' });
300
384
  }
301
-
385
+
302
386
  await fse.writeFile(safePath, content, 'utf-8');
303
-
387
+
304
388
  if (relativePath === 'package.json' || relativePath.endsWith('/package.json')) {
305
389
  try {
306
390
  const packageJson = JSON.parse(content);
307
-
391
+
308
392
  const existingPlugin = await prisma.installedPlugin.findFirst({
309
393
  where: {
310
394
  botId: parseInt(req.params.botId),
311
395
  path: req.pluginPath,
312
396
  }
313
397
  });
314
-
398
+
315
399
  if (existingPlugin) {
316
400
  const newName = packageJson.name || req.params.pluginName;
317
-
401
+ const oldName = existingPlugin.name;
402
+
318
403
  const conflictingPlugin = await prisma.installedPlugin.findFirst({
319
404
  where: {
320
405
  botId: parseInt(req.params.botId),
@@ -322,7 +407,7 @@ router.post('/:pluginName/file', resolvePluginPath, async (req, res) => {
322
407
  id: { not: existingPlugin.id }
323
408
  }
324
409
  });
325
-
410
+
326
411
  if (conflictingPlugin) {
327
412
  console.warn(`[Plugin IDE] Конфликт имени плагина: ${newName} уже существует для бота ${req.params.botId}`);
328
413
  await prisma.installedPlugin.update({
@@ -334,15 +419,63 @@ router.post('/:pluginName/file', resolvePluginPath, async (req, res) => {
334
419
  }
335
420
  });
336
421
  } else {
337
- await prisma.installedPlugin.update({
338
- where: { id: existingPlugin.id },
339
- data: {
340
- name: newName,
341
- version: packageJson.version || '1.0.0',
342
- description: packageJson.description || '',
343
- manifest: JSON.stringify(packageJson.botpanel || {}),
422
+ // Если имя изменилось, переименовать папку плагина
423
+ if (oldName !== newName) {
424
+ const oldPath = req.pluginPath;
425
+ const pluginsDir = path.dirname(oldPath);
426
+ const newPath = path.join(pluginsDir, newName);
427
+
428
+ // Проверка что новая папка не существует
429
+ if (await fse.pathExists(newPath)) {
430
+ console.error(`[Plugin IDE] Папка ${newName} уже существует. Переименование невозможно.`);
431
+ return res.status(400).json({
432
+ error: `Папка с именем "${newName}" уже существует. Переименование невозможно.`
433
+ });
344
434
  }
345
- });
435
+
436
+ try {
437
+ // Переименовать папку
438
+ await fse.rename(oldPath, newPath);
439
+ console.log(`[Plugin IDE] Папка плагина переименована: ${oldName} -> ${newName}`);
440
+
441
+ // Обновить БД с новым именем и путём
442
+ await prisma.installedPlugin.update({
443
+ where: { id: existingPlugin.id },
444
+ data: {
445
+ name: newName,
446
+ path: newPath,
447
+ version: packageJson.version || '1.0.0',
448
+ description: packageJson.description || '',
449
+ manifest: JSON.stringify(packageJson.botpanel || {}),
450
+ }
451
+ });
452
+
453
+ console.log(`[Plugin IDE] БД обновлена для плагина ${oldName} -> ${newName}`);
454
+
455
+ // Вернуть информацию о переименовании
456
+ return res.status(200).json({
457
+ message: 'Файл успешно сохранен.',
458
+ renamed: true,
459
+ oldName: oldName,
460
+ newName: newName
461
+ });
462
+ } catch (renameError) {
463
+ console.error(`[Plugin IDE] Ошибка переименования папки плагина:`, renameError);
464
+ return res.status(500).json({
465
+ error: `Не удалось переименовать папку плагина: ${renameError.message}`
466
+ });
467
+ }
468
+ } else {
469
+ // Имя не изменилось, только обновляем метаданные
470
+ await prisma.installedPlugin.update({
471
+ where: { id: existingPlugin.id },
472
+ data: {
473
+ version: packageJson.version || '1.0.0',
474
+ description: packageJson.description || '',
475
+ manifest: JSON.stringify(packageJson.botpanel || {}),
476
+ }
477
+ });
478
+ }
346
479
  }
347
480
  console.log(`[Plugin IDE] Manifest обновлен для плагина ${req.params.pluginName} после сохранения package.json`);
348
481
  }
@@ -350,9 +483,9 @@ router.post('/:pluginName/file', resolvePluginPath, async (req, res) => {
350
483
  console.error(`[Plugin IDE] Ошибка обновления manifest для ${req.params.pluginName}:`, manifestError);
351
484
  }
352
485
  }
353
-
486
+
354
487
  res.status(200).json({ message: 'Файл успешно сохранен.' });
355
-
488
+
356
489
  } catch (error) {
357
490
  console.error(`[Plugin IDE Error] /file POST for ${req.params.pluginName}:`, error);
358
491
  res.status(500).json({ error: 'Не удалось сохранить файл.' });
@@ -390,14 +523,14 @@ router.post('/:pluginName/fs', resolvePluginPath, async (req, res) => {
390
523
  await fse.remove(safePath);
391
524
  res.status(200).json({ message: 'File or folder deleted successfully.' });
392
525
  break;
393
-
526
+
394
527
  case 'rename':
395
528
  if (!newPath) return res.status(400).json({ error: 'New path is required for rename operation.' });
396
529
  const safeNewPath = path.resolve(req.pluginPath, newPath);
397
530
  if (!safeNewPath.startsWith(req.pluginPath)) return res.status(403).json({ error: 'Access denied: New path is outside of plugin directory.' });
398
531
  if (!await fse.pathExists(safePath)) return res.status(404).json({ error: 'Source file or folder not found.' });
399
532
  if (await fse.pathExists(safeNewPath)) return res.status(409).json({ error: 'Destination path already exists.' });
400
-
533
+
401
534
  await fse.move(safePath, safeNewPath);
402
535
  res.status(200).json({ message: 'Renamed successfully.' });
403
536
  break;
@@ -407,7 +540,7 @@ router.post('/:pluginName/fs', resolvePluginPath, async (req, res) => {
407
540
  const safeMoveNewPath = path.resolve(req.pluginPath, newPath);
408
541
  if (!safeMoveNewPath.startsWith(req.pluginPath)) return res.status(403).json({ error: 'Access denied: New path is outside of plugin directory.' });
409
542
  if (!await fse.pathExists(safePath)) return res.status(404).json({ error: 'Source file or folder not found.' });
410
-
543
+
411
544
  if (safeMoveNewPath.startsWith(safePath + path.sep)) {
412
545
  return res.status(400).json({ error: 'Cannot move folder into itself.' });
413
546
  }
@@ -421,9 +554,9 @@ router.post('/:pluginName/fs', resolvePluginPath, async (req, res) => {
421
554
  finalPath = path.join(dir, `${base} (${counter})${ext}`);
422
555
  counter++;
423
556
  }
424
-
557
+
425
558
  await fse.move(safePath, finalPath);
426
- res.status(200).json({
559
+ res.status(200).json({
427
560
  message: 'Moved successfully.',
428
561
  newPath: path.relative(req.pluginPath, finalPath)
429
562
  });
@@ -472,16 +605,16 @@ router.post('/:pluginName/manifest', resolvePluginPath, async (req, res) => {
472
605
  url: repositoryUrl
473
606
  };
474
607
  }
475
-
608
+
476
609
  await fse.writeJson(manifestPath, newManifest, { spaces: 2 });
477
-
610
+
478
611
  const existingPlugin = await prisma.installedPlugin.findFirst({
479
612
  where: {
480
613
  botId: parseInt(req.params.botId),
481
614
  path: req.pluginPath,
482
615
  }
483
616
  });
484
-
617
+
485
618
  if (existingPlugin) {
486
619
  const conflictingPlugin = await prisma.installedPlugin.findFirst({
487
620
  where: {
@@ -490,7 +623,7 @@ router.post('/:pluginName/manifest', resolvePluginPath, async (req, res) => {
490
623
  id: { not: existingPlugin.id }
491
624
  }
492
625
  });
493
-
626
+
494
627
  if (conflictingPlugin) {
495
628
  console.warn(`[Plugin IDE] Конфликт имени плагина: ${newManifest.name} уже существует для бота ${req.params.botId}`);
496
629
  await prisma.installedPlugin.update({
@@ -530,65 +663,37 @@ router.post('/:pluginName/fork', resolvePluginPath, async (req, res) => {
530
663
  });
531
664
 
532
665
  if (!currentPlugin || currentPlugin.sourceType !== 'GITHUB') {
533
- return res.status(400).json({ error: 'Копировать можно только плагины, установленные из GitHub.' });
666
+ return res.status(400).json({ error: 'Превратить в локальный можно только плагины из GitHub.' });
534
667
  }
535
-
536
- const originalPath = req.pluginPath;
537
- let newName = `${pluginName}-copy`;
538
- let newPath = path.join(path.dirname(originalPath), newName);
539
- let counter = 1;
540
-
541
- while (await fse.pathExists(newPath) || await prisma.installedPlugin.findFirst({ where: { botId: parseInt(botId), name: newName } })) {
542
- newName = `${pluginName}-copy-${counter}`;
543
- newPath = path.join(path.dirname(originalPath), newName);
544
- counter++;
545
- }
546
-
547
- await fse.copy(originalPath, newPath);
548
668
 
549
- const packageJsonPath = path.join(newPath, 'package.json');
550
- const packageJson = await fse.readJson(packageJsonPath);
551
-
552
- packageJson.name = newName;
553
- packageJson.description = `(Forked from ${pluginName}) ${packageJson.description || ''}`;
554
-
555
- packageJson.repository = {
556
- type: 'git',
557
- url: currentPlugin.sourceUri
558
- };
559
-
560
- await fse.writeJson(packageJsonPath, packageJson, { spaces: 2 });
561
-
562
- const forkedPlugin = await prisma.installedPlugin.create({
669
+ // Просто меняем sourceType на LOCAL_IDE, без копирования
670
+ const updatedPlugin = await prisma.installedPlugin.update({
671
+ where: { id: currentPlugin.id },
563
672
  data: {
564
- botId: parseInt(botId),
565
- name: newName,
566
- version: packageJson.version,
567
- description: packageJson.description,
568
- path: newPath,
569
673
  sourceType: 'LOCAL_IDE',
570
- sourceUri: newPath,
571
- manifest: JSON.stringify(packageJson.botpanel || {}),
572
- isEnabled: false
674
+ sourceUri: currentPlugin.path, // Используем путь как sourceUri для локальных
573
675
  }
574
676
  });
575
677
 
576
- res.status(201).json(forkedPlugin);
678
+ console.log(`[Plugin IDE] Плагин ${pluginName} превращен в локальный`);
679
+ res.status(200).json(updatedPlugin);
577
680
 
578
681
  } catch (error) {
579
682
  console.error(`[Plugin IDE Error] /fork POST for ${req.params.pluginName}:`, error);
580
- res.status(500).json({ error: 'Не удалось скопировать плагин.' });
683
+ res.status(500).json({ error: 'Не удалось превратить плагин в локальный.' });
581
684
  }
582
685
  });
583
686
 
584
687
  router.post('/:pluginName/create-pr', resolvePluginPath, async (req, res) => {
585
688
  const cp = require('child_process');
586
- const { branch = 'main', commitMessage = 'Changes from local edit', repositoryUrl } = req.body;
689
+ const { token, branch = 'contribution', commitMessage = 'Changes from BlockMine IDE', prTitle, prBody } = req.body;
587
690
 
691
+ if (!token) {
692
+ return res.status(400).json({ error: 'GitHub токен обязателен.' });
693
+ }
588
694
  if (!branch) {
589
695
  return res.status(400).json({ error: 'Название ветки обязательно.' });
590
696
  }
591
- // Validate branch name: only allow letters, numbers, dashes, underscores, dots, slashes
592
697
  if (!branch.match(/^[\w\-.\/]+$/)) {
593
698
  return res.status(400).json({ error: 'Некорректное имя ветки.' });
594
699
  }
@@ -596,26 +701,17 @@ router.post('/:pluginName/create-pr', resolvePluginPath, async (req, res) => {
596
701
  try {
597
702
  cp.execSync('git --version');
598
703
  } catch (e) {
599
- return res.status(400).json({ error: 'Git не установлен на этой системе. Пожалуйста, установите Git для создания PR.' });
704
+ return res.status(400).json({ error: 'Git не установлен на этой системе.' });
600
705
  }
601
706
 
602
707
  try {
708
+ const githubToken = token;
603
709
  const manifestPath = path.join(req.pluginPath, 'package.json');
604
710
  const packageJson = await fse.readJson(manifestPath);
605
- let originalRepo = packageJson.repository?.url;
606
-
607
- if (repositoryUrl) {
608
- originalRepo = repositoryUrl;
609
-
610
- packageJson.repository = {
611
- type: 'git',
612
- url: repositoryUrl
613
- };
614
- await fse.writeJson(manifestPath, packageJson, { spaces: 2 });
615
- }
711
+ let originalRepo = packageJson.repository?.url || packageJson.repository;
616
712
 
617
713
  if (!originalRepo) {
618
- return res.status(400).json({ error: 'URL репозитория не указан.' });
714
+ return res.status(400).json({ error: 'URL репозитория не указан в package.json.' });
619
715
  }
620
716
  const cleanRepoUrl = originalRepo.replace(/^git\+/, '');
621
717
 
@@ -629,35 +725,77 @@ router.post('/:pluginName/create-pr', resolvePluginPath, async (req, res) => {
629
725
  return res.status(400).json({ error: 'Неверный формат URL репозитория.' });
630
726
  }
631
727
 
728
+ const octokit = new Octokit({ auth: githubToken });
729
+
730
+ // Получаем информацию о текущем пользователе
731
+ const { data: user } = await octokit.users.getAuthenticated();
732
+ const myUsername = user.login;
733
+
734
+ console.log(`[Plugin IDE] Создание PR для ${repoInfo.owner}/${repoInfo.repo} от пользователя ${myUsername}`);
735
+
736
+ // Проверяем, есть ли уже форк
737
+ let forkInfo;
738
+ try {
739
+ const { data: existingFork } = await octokit.repos.get({
740
+ owner: myUsername,
741
+ repo: repoInfo.repo
742
+ });
743
+
744
+ // Проверяем что это действительно форк нужного репо
745
+ if (existingFork.fork && existingFork.parent?.full_name === `${repoInfo.owner}/${repoInfo.repo}`) {
746
+ forkInfo = existingFork;
747
+ console.log(`[Plugin IDE] Используем существующий форк: ${myUsername}/${repoInfo.repo}`);
748
+ }
749
+ } catch (e) {
750
+ // Форк не найден
751
+ }
752
+
753
+ // Создаём форк если нет
754
+ if (!forkInfo) {
755
+ console.log(`[Plugin IDE] Создаём форк ${repoInfo.owner}/${repoInfo.repo}...`);
756
+ const { data: newFork } = await octokit.repos.createFork({
757
+ owner: repoInfo.owner,
758
+ repo: repoInfo.repo
759
+ });
760
+ forkInfo = newFork;
761
+
762
+ // Ждём пока форк создастся
763
+ await new Promise(resolve => setTimeout(resolve, 3000));
764
+ console.log(`[Plugin IDE] Форк создан: ${myUsername}/${repoInfo.repo}`);
765
+ }
766
+
632
767
  const cwd = req.pluginPath;
633
- const tempDir = path.join(cwd, '..', `temp-${Date.now()}`);
768
+ const tempDir = path.join(cwd, '..', `temp-pr-${Date.now()}`);
634
769
 
635
770
  try {
636
- cp.execSync(`git clone ${cleanRepoUrl} "${tempDir}"`, { stdio: 'inherit' });
637
-
771
+ // Клонируем свой форк
772
+ const forkUrl = `https://${githubToken}@github.com/${myUsername}/${repoInfo.repo}.git`;
773
+ cp.execSync(`git clone "${forkUrl}" "${tempDir}"`, { stdio: 'pipe' });
774
+
638
775
  process.chdir(tempDir);
639
-
640
- let branchExists = false;
776
+
777
+ // Добавляем оригинальный репо как upstream
778
+ cp.execSync(`git remote add upstream ${cleanRepoUrl}`, { stdio: 'pipe' });
779
+ cp.execSync('git fetch upstream', { stdio: 'pipe' });
780
+
781
+ // Определяем основную ветку оригинала
782
+ let defaultBranch = 'main';
783
+ try {
784
+ cp.execSync('git show-ref --verify refs/remotes/upstream/main', { stdio: 'pipe' });
785
+ } catch {
786
+ defaultBranch = 'master';
787
+ }
788
+
789
+ // Создаём ветку от upstream/main
641
790
  try {
642
- cp.execSync(`git checkout -b ${branch}`, { stdio: 'pipe' });
643
- console.log(`[Plugin IDE] Создана новая ветка ${branch}`);
791
+ cp.execSync(`git checkout -b ${branch} upstream/${defaultBranch}`, { stdio: 'pipe' });
644
792
  } catch (e) {
645
- try {
646
- cp.execSync(`git checkout ${branch}`, { stdio: 'pipe' });
647
- console.log(`[Plugin IDE] Переключились на существующую ветку ${branch}`);
648
- } catch (e2) {
649
- try {
650
- cp.execSync(`git fetch origin ${branch}`, { stdio: 'pipe' });
651
- cp.execSync(`git checkout -b ${branch} origin/${branch}`, { stdio: 'pipe' });
652
- branchExists = true;
653
- console.log(`[Plugin IDE] Создана ветка ${branch} из удаленной`);
654
- } catch (e3) {
655
- cp.execSync(`git checkout -B ${branch}`, { stdio: 'pipe' });
656
- console.log(`[Plugin IDE] Принудительно создана ветка ${branch}`);
657
- }
658
- }
793
+ // Ветка может уже существовать
794
+ cp.execSync(`git checkout ${branch}`, { stdio: 'pipe' });
795
+ cp.execSync(`git reset --hard upstream/${defaultBranch}`, { stdio: 'pipe' });
659
796
  }
660
-
797
+
798
+ // Копируем файлы из плагина
661
799
  const files = await fse.readdir(req.pluginPath);
662
800
  for (const file of files) {
663
801
  if (file !== '.git') {
@@ -666,10 +804,11 @@ router.post('/:pluginName/create-pr', resolvePluginPath, async (req, res) => {
666
804
  await fse.copy(sourcePath, destPath, { overwrite: true });
667
805
  }
668
806
  }
669
-
670
- cp.execSync('git add .');
807
+
808
+ // Коммитим изменения
809
+ cp.execSync('git add .', { stdio: 'pipe' });
671
810
  try {
672
- cp.execSync(`git commit -m "${commitMessage}"`);
811
+ cp.execSync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, { stdio: 'pipe' });
673
812
  } catch (e) {
674
813
  if (e.message.includes('nothing to commit')) {
675
814
  return res.status(400).json({ error: 'Нет изменений для коммита.' });
@@ -677,27 +816,52 @@ router.post('/:pluginName/create-pr', resolvePluginPath, async (req, res) => {
677
816
  throw e;
678
817
  }
679
818
 
819
+ // Пушим в свой форк
820
+ cp.execSync(`git push origin ${branch} --force`, { stdio: 'pipe' });
821
+ console.log(`[Plugin IDE] Изменения запушены в ${myUsername}/${repoInfo.repo}:${branch}`);
680
822
 
681
- if (branchExists) {
682
- cp.execFileSync('git', ['push', 'origin', branch, '--force']);
683
- console.log(`[Plugin IDE] Ветка ${branch} обновлена`);
684
- } else {
685
- cp.execFileSync('git', ['push', '-u', 'origin', branch]);
686
- console.log(`[Plugin IDE] Новая ветка ${branch} создана`);
823
+ // Создаём Pull Request через API
824
+ let prData;
825
+ try {
826
+ const { data: pr } = await octokit.pulls.create({
827
+ owner: repoInfo.owner,
828
+ repo: repoInfo.repo,
829
+ title: prTitle || `Update from BlockMine IDE`,
830
+ body: prBody || `Changes made using BlockMine IDE.\n\n${commitMessage}`,
831
+ head: `${myUsername}:${branch}`,
832
+ base: defaultBranch
833
+ });
834
+ prData = pr;
835
+ console.log(`[Plugin IDE] PR создан: ${pr.html_url}`);
836
+ } catch (prError) {
837
+ // PR может уже существовать
838
+ if (prError.status === 422) {
839
+ // Ищем существующий PR
840
+ const { data: existingPRs } = await octokit.pulls.list({
841
+ owner: repoInfo.owner,
842
+ repo: repoInfo.repo,
843
+ head: `${myUsername}:${branch}`,
844
+ state: 'open'
845
+ });
846
+
847
+ if (existingPRs.length > 0) {
848
+ prData = existingPRs[0];
849
+ console.log(`[Plugin IDE] PR уже существует: ${prData.html_url}`);
850
+ } else {
851
+ throw prError;
852
+ }
853
+ } else {
854
+ throw prError;
855
+ }
687
856
  }
688
857
 
689
- const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/pull/new/${branch}`;
858
+ res.json({
859
+ success: true,
860
+ prUrl: prData.html_url,
861
+ prNumber: prData.number,
862
+ message: 'Pull Request создан успешно'
863
+ });
690
864
 
691
- const responseData = {
692
- success: true,
693
- prUrl: prUrl,
694
- isUpdate: branchExists,
695
- message: branchExists ? 'Существующий PR обновлен' : 'Новый PR создан'
696
- };
697
-
698
- console.log(`[Plugin IDE] PR ${branchExists ? 'обновлен' : 'создан'} для плагина ${req.params.pluginName}:`, responseData);
699
- res.json(responseData);
700
-
701
865
  } finally {
702
866
  try {
703
867
  process.chdir(req.pluginPath);
@@ -713,4 +877,1420 @@ router.post('/:pluginName/create-pr', resolvePluginPath, async (req, res) => {
713
877
  }
714
878
  });
715
879
 
880
+ // Search in files
881
+ router.get('/:pluginName/search', resolvePluginPath, async (req, res) => {
882
+ try {
883
+ const { query, caseSensitive, wholeWord, useRegex } = req.query;
884
+
885
+ if (!query) {
886
+ return res.status(400).json({ error: 'Search query is required.' });
887
+ }
888
+
889
+ const results = [];
890
+
891
+ const searchInFile = async (filePath, relativePath) => {
892
+ try {
893
+ const content = await fse.readFile(filePath, 'utf-8');
894
+ const lines = content.split('\n');
895
+ const matches = [];
896
+
897
+ let pattern;
898
+ if (useRegex === 'true') {
899
+ try {
900
+ pattern = new RegExp(query, caseSensitive === 'true' ? 'g' : 'gi');
901
+ } catch (e) {
902
+ return; // Invalid regex, skip
903
+ }
904
+ } else {
905
+ const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
906
+ const wordBoundary = wholeWord === 'true' ? '\\b' : '';
907
+ pattern = new RegExp(
908
+ `${wordBoundary}${escapedQuery}${wordBoundary}`,
909
+ caseSensitive === 'true' ? 'g' : 'gi'
910
+ );
911
+ }
912
+
913
+ lines.forEach((line, index) => {
914
+ if (pattern.test(line)) {
915
+ matches.push({
916
+ line: index + 1,
917
+ preview: line.trim().substring(0, 200),
918
+ });
919
+ }
920
+ });
921
+
922
+ if (matches.length > 0) {
923
+ results.push({
924
+ file: relativePath,
925
+ matches,
926
+ });
927
+ }
928
+ } catch (error) {
929
+ // Skip files that can't be read
930
+ }
931
+ };
932
+
933
+ const searchDirectory = async (dirPath, relativePath = '') => {
934
+ const dirents = await fse.readdir(dirPath, { withFileTypes: true });
935
+
936
+ for (const dirent of dirents) {
937
+ const fullPath = path.join(dirPath, dirent.name);
938
+ const relPath = path.join(relativePath, dirent.name).replace(/\\/g, '/');
939
+
940
+ // Skip node_modules and hidden directories
941
+ if (dirent.name.startsWith('.') || dirent.name === 'node_modules') {
942
+ continue;
943
+ }
944
+
945
+ if (dirent.isDirectory()) {
946
+ await searchDirectory(fullPath, relPath);
947
+ } else {
948
+ // Only search text files
949
+ const ext = path.extname(dirent.name).toLowerCase();
950
+ const textExtensions = [
951
+ '.js', '.jsx', '.ts', '.tsx', '.json', '.md', '.txt',
952
+ '.html', '.css', '.scss', '.yaml', '.yml', '.xml'
953
+ ];
954
+ if (textExtensions.includes(ext)) {
955
+ await searchInFile(fullPath, relPath);
956
+ }
957
+ }
958
+ }
959
+ };
960
+
961
+ await searchDirectory(req.pluginPath);
962
+
963
+ res.json({ results });
964
+ } catch (error) {
965
+ console.error(`[Plugin IDE Error] /search for ${req.params.pluginName}:`, error);
966
+ res.status(500).json({ error: 'Search failed.' });
967
+ }
968
+ });
969
+
970
+ // Replace in files
971
+ router.post('/:pluginName/replace', resolvePluginPath, async (req, res) => {
972
+ try {
973
+ const { searchQuery, replaceQuery, options } = req.body;
974
+
975
+ if (!searchQuery || replaceQuery === undefined) {
976
+ return res.status(400).json({ error: 'Search and replace queries are required.' });
977
+ }
978
+
979
+ let replacedCount = 0;
980
+ let filesModified = 0;
981
+
982
+ const replaceInFile = async (filePath) => {
983
+ try {
984
+ const content = await fse.readFile(filePath, 'utf-8');
985
+
986
+ let pattern;
987
+ if (options.useRegex) {
988
+ try {
989
+ pattern = new RegExp(searchQuery, options.caseSensitive ? 'g' : 'gi');
990
+ } catch (e) {
991
+ return 0; // Invalid regex, skip
992
+ }
993
+ } else {
994
+ const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
995
+ const wordBoundary = options.wholeWord ? '\\b' : '';
996
+ pattern = new RegExp(
997
+ `${wordBoundary}${escapedQuery}${wordBoundary}`,
998
+ options.caseSensitive ? 'g' : 'gi'
999
+ );
1000
+ }
1001
+
1002
+ const matches = content.match(pattern);
1003
+ if (!matches) return 0;
1004
+
1005
+ const newContent = content.replace(pattern, replaceQuery);
1006
+ await fse.writeFile(filePath, newContent, 'utf-8');
1007
+
1008
+ return matches.length;
1009
+ } catch (error) {
1010
+ return 0;
1011
+ }
1012
+ };
1013
+
1014
+ const processDirectory = async (dirPath) => {
1015
+ const dirents = await fse.readdir(dirPath, { withFileTypes: true });
1016
+
1017
+ for (const dirent of dirents) {
1018
+ const fullPath = path.join(dirPath, dirent.name);
1019
+
1020
+ if (dirent.name.startsWith('.') || dirent.name === 'node_modules') {
1021
+ continue;
1022
+ }
1023
+
1024
+ if (dirent.isDirectory()) {
1025
+ await processDirectory(fullPath);
1026
+ } else {
1027
+ const ext = path.extname(dirent.name).toLowerCase();
1028
+ const textExtensions = [
1029
+ '.js', '.jsx', '.ts', '.tsx', '.json', '.md', '.txt',
1030
+ '.html', '.css', '.scss', '.yaml', '.yml', '.xml'
1031
+ ];
1032
+ if (textExtensions.includes(ext)) {
1033
+ const count = await replaceInFile(fullPath);
1034
+ if (count > 0) {
1035
+ replacedCount += count;
1036
+ filesModified++;
1037
+ }
1038
+ }
1039
+ }
1040
+ }
1041
+ };
1042
+
1043
+ await processDirectory(req.pluginPath);
1044
+
1045
+ res.json({ replacedCount, filesModified });
1046
+ } catch (error) {
1047
+ console.error(`[Plugin IDE Error] /replace for ${req.params.pluginName}:`, error);
1048
+ res.status(500).json({ error: 'Replace failed.' });
1049
+ }
1050
+ });
1051
+
1052
+ // Get plugin info from package.json
1053
+ router.get('/:pluginName/info', resolvePluginPath, async (req, res) => {
1054
+ try {
1055
+ const packageJsonPath = path.join(req.pluginPath, 'package.json');
1056
+
1057
+ if (!await fse.pathExists(packageJsonPath)) {
1058
+ return res.json({
1059
+ hasRepository: false,
1060
+ name: req.params.pluginName,
1061
+ message: 'package.json not found'
1062
+ });
1063
+ }
1064
+
1065
+ const packageJson = await fse.readJson(packageJsonPath);
1066
+
1067
+ const hasRepository = !!(packageJson.repository && (
1068
+ packageJson.repository.url ||
1069
+ (typeof packageJson.repository === 'string' && packageJson.repository.length > 0)
1070
+ ));
1071
+
1072
+ const repositoryInfo = hasRepository ? {
1073
+ url: typeof packageJson.repository === 'string'
1074
+ ? packageJson.repository
1075
+ : packageJson.repository.url,
1076
+ type: typeof packageJson.repository === 'object'
1077
+ ? packageJson.repository.type
1078
+ : 'git'
1079
+ } : null;
1080
+
1081
+ const files = await fse.readdir(req.pluginPath);
1082
+ const hasFiles = files.some(f => f !== 'package.json' && !f.startsWith('.'));
1083
+
1084
+ res.json({
1085
+ hasRepository,
1086
+ hasFiles,
1087
+ name: packageJson.name || req.params.pluginName,
1088
+ version: packageJson.version,
1089
+ description: packageJson.description,
1090
+ author: packageJson.author,
1091
+ license: packageJson.license,
1092
+ repository: repositoryInfo,
1093
+ botpanel: packageJson.botpanel || null
1094
+ });
1095
+ } catch (error) {
1096
+ console.error(`[Plugin IDE Error] /info for ${req.params.pluginName}:`, error);
1097
+ res.status(500).json({ error: 'Failed to read plugin info.' });
1098
+ }
1099
+ });
1100
+
1101
+ router.post('/:pluginName/clone', resolvePluginPath, async (req, res) => {
1102
+ try {
1103
+ const { gitUrl } = req.body;
1104
+
1105
+ if (!gitUrl || typeof gitUrl !== 'string' || !gitUrl.trim()) {
1106
+ return res.status(400).json({ error: 'Git URL is required.' });
1107
+ }
1108
+
1109
+ const { execSync } = require('child_process');
1110
+
1111
+ const files = await fse.readdir(req.pluginPath);
1112
+ const nonPackageFiles = files.filter(f => f !== 'package.json');
1113
+
1114
+ if (nonPackageFiles.length > 0) {
1115
+ return res.status(400).json({
1116
+ error: 'Plugin directory is not empty. Cannot clone into existing plugin.'
1117
+ });
1118
+ }
1119
+
1120
+ const tempDir = path.join(req.pluginPath, '.clone-temp');
1121
+ await fse.mkdirp(tempDir);
1122
+
1123
+ try {
1124
+ console.log(`[Plugin IDE] Cloning ${gitUrl} into ${tempDir}...`);
1125
+
1126
+ execSync(`git clone "${gitUrl}" "${tempDir}"`, {
1127
+ stdio: 'inherit',
1128
+ cwd: req.pluginPath
1129
+ });
1130
+
1131
+ const clonedFiles = await fse.readdir(tempDir);
1132
+ for (const file of clonedFiles) {
1133
+ if (file === '.git') continue;
1134
+ const srcPath = path.join(tempDir, file);
1135
+ const destPath = path.join(req.pluginPath, file);
1136
+ await fse.move(srcPath, destPath, { overwrite: true });
1137
+ }
1138
+
1139
+ await fse.remove(tempDir);
1140
+
1141
+ const packageJsonPath = path.join(req.pluginPath, 'package.json');
1142
+ if (!await fse.pathExists(packageJsonPath)) {
1143
+ throw new Error('Cloned repository does not contain package.json');
1144
+ }
1145
+
1146
+ const packageJson = await fse.readJson(packageJsonPath);
1147
+ if (!packageJson.repository) {
1148
+ packageJson.repository = {
1149
+ type: 'git',
1150
+ url: gitUrl
1151
+ };
1152
+ await fse.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1153
+ }
1154
+
1155
+ console.log(`[Plugin IDE] Successfully cloned ${gitUrl}`);
1156
+
1157
+ res.json({
1158
+ success: true,
1159
+ message: 'Plugin cloned successfully',
1160
+ repository: gitUrl
1161
+ });
1162
+ } catch (cloneError) {
1163
+ await fse.remove(tempDir).catch(() => {});
1164
+ throw cloneError;
1165
+ }
1166
+ } catch (error) {
1167
+ console.error(`[Plugin IDE Error] /clone for ${req.params.pluginName}:`, error);
1168
+ res.status(500).json({
1169
+ error: error.message || 'Failed to clone plugin from Git.'
1170
+ });
1171
+ }
1172
+ });
1173
+
1174
+ router.post('/:pluginName/github/create', resolvePluginPath, async (req, res) => {
1175
+ try {
1176
+ const { token, repoName, isPrivate = true } = req.body;
1177
+
1178
+ if (!token || !repoName) {
1179
+ return res.status(400).json({ error: 'Token and repository name are required.' });
1180
+ }
1181
+
1182
+ const octokit = new Octokit({ auth: token });
1183
+
1184
+ // Читаем package.json для получения description
1185
+ const packageJsonPath = path.join(req.pluginPath, 'package.json');
1186
+ let description = 'BlockMine plugin for Minecraft bots';
1187
+ if (await fse.pathExists(packageJsonPath)) {
1188
+ const packageJson = await fse.readJson(packageJsonPath);
1189
+ if (packageJson.description) {
1190
+ description = `${packageJson.description} | BlockMine plugin`;
1191
+ }
1192
+ }
1193
+
1194
+ console.log(`[Plugin IDE] Creating GitHub repository: ${repoName}`);
1195
+ const { data: repo } = await octokit.repos.createForAuthenticatedUser({
1196
+ name: repoName,
1197
+ description: description,
1198
+ private: isPrivate,
1199
+ auto_init: false,
1200
+ homepage: 'https://github.com/blockmineJS'
1201
+ });
1202
+
1203
+ console.log(`[Plugin IDE] Repository created: ${repo.html_url}`);
1204
+
1205
+ // Добавляем topics (теги) для поиска на GitHub
1206
+ try {
1207
+ await octokit.repos.replaceAllTopics({
1208
+ owner: repo.owner.login,
1209
+ repo: repo.name,
1210
+ names: ['blockmine', 'blockmine-plugin', 'minecraft', 'mineflayer', 'minecraft-bot']
1211
+ });
1212
+ console.log(`[Plugin IDE] Topics added to repository`);
1213
+ } catch (topicError) {
1214
+ console.warn(`[Plugin IDE] Failed to add topics:`, topicError.message);
1215
+ }
1216
+
1217
+ await uploadFilesToGitHub(octokit, repo.owner.login, repo.name, req.pluginPath);
1218
+
1219
+ // Обновляем package.json с URL репозитория и keywords
1220
+ if (await fse.pathExists(packageJsonPath)) {
1221
+ const packageJson = await fse.readJson(packageJsonPath);
1222
+ packageJson.repository = {
1223
+ type: 'git',
1224
+ url: repo.html_url
1225
+ };
1226
+
1227
+ // Добавляем keywords для npm (если будет публиковаться)
1228
+ if (!packageJson.keywords) {
1229
+ packageJson.keywords = [];
1230
+ }
1231
+ const keywords = ['blockmine', 'blockmine-plugin', 'minecraft', 'mineflayer'];
1232
+ keywords.forEach(kw => {
1233
+ if (!packageJson.keywords.includes(kw)) {
1234
+ packageJson.keywords.push(kw);
1235
+ }
1236
+ });
1237
+
1238
+ await fse.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1239
+ }
1240
+
1241
+ res.json({
1242
+ success: true,
1243
+ repository: repo.html_url,
1244
+ message: 'Plugin uploaded to new GitHub repository'
1245
+ });
1246
+ } catch (error) {
1247
+ console.error(`[Plugin IDE Error] /github/create:`, error);
1248
+ res.status(500).json({
1249
+ error: error.message || 'Failed to create repository and upload plugin.'
1250
+ });
1251
+ }
1252
+ });
1253
+
1254
+ router.post('/:pluginName/github/upload', resolvePluginPath, async (req, res) => {
1255
+ try {
1256
+ const { token, repoFullName } = req.body;
1257
+
1258
+ if (!token || !repoFullName) {
1259
+ return res.status(400).json({ error: 'Token and repository name are required.' });
1260
+ }
1261
+
1262
+ const [owner, repo] = repoFullName.split('/');
1263
+ if (!owner || !repo) {
1264
+ return res.status(400).json({ error: 'Invalid repository name format. Expected: owner/repo' });
1265
+ }
1266
+
1267
+ const octokit = new Octokit({ auth: token });
1268
+
1269
+ try {
1270
+ await octokit.repos.get({ owner, repo });
1271
+ } catch (error) {
1272
+ return res.status(404).json({ error: 'Repository not found or access denied.' });
1273
+ }
1274
+
1275
+ console.log(`[Plugin IDE] Uploading to existing repository: ${repoFullName}`);
1276
+
1277
+ await uploadFilesToGitHub(octokit, owner, repo, req.pluginPath);
1278
+
1279
+ const packageJsonPath = path.join(req.pluginPath, 'package.json');
1280
+ if (await fse.pathExists(packageJsonPath)) {
1281
+ const packageJson = await fse.readJson(packageJsonPath);
1282
+ packageJson.repository = {
1283
+ type: 'git',
1284
+ url: `https://github.com/${repoFullName}`
1285
+ };
1286
+ await fse.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1287
+ }
1288
+
1289
+ res.json({
1290
+ success: true,
1291
+ repository: `https://github.com/${repoFullName}`,
1292
+ message: 'Plugin uploaded to GitHub repository'
1293
+ });
1294
+ } catch (error) {
1295
+ console.error(`[Plugin IDE Error] /github/upload:`, error);
1296
+ res.status(500).json({
1297
+ error: error.message || 'Failed to upload plugin to repository.'
1298
+ });
1299
+ }
1300
+ });
1301
+
1302
+ router.get('/:pluginName/github/tags', resolvePluginPath, async (req, res) => {
1303
+ try {
1304
+ const { token } = req.query;
1305
+
1306
+ if (!token) {
1307
+ return res.status(400).json({ error: 'Token is required.' });
1308
+ }
1309
+
1310
+ const packageJsonPath = path.join(req.pluginPath, 'package.json');
1311
+ if (!await fse.pathExists(packageJsonPath)) {
1312
+ return res.status(404).json({ error: 'package.json not found.' });
1313
+ }
1314
+
1315
+ const packageJson = await fse.readJson(packageJsonPath);
1316
+ const repoUrl = typeof packageJson.repository === 'string'
1317
+ ? packageJson.repository
1318
+ : packageJson.repository?.url;
1319
+
1320
+ if (!repoUrl) {
1321
+ return res.status(400).json({ error: 'No repository URL in package.json.' });
1322
+ }
1323
+
1324
+ const match = repoUrl.match(/github\.com[\/:](.+?)\/(.+?)(\.git)?$/);
1325
+ if (!match) {
1326
+ return res.status(400).json({ error: 'Invalid GitHub repository URL.' });
1327
+ }
1328
+
1329
+ const [, owner, repo] = match;
1330
+ const octokit = new Octokit({ auth: token });
1331
+
1332
+ const { data: tags } = await octokit.repos.listTags({
1333
+ owner,
1334
+ repo,
1335
+ per_page: 100
1336
+ });
1337
+
1338
+ const { data: releases } = await octokit.repos.listReleases({
1339
+ owner,
1340
+ repo,
1341
+ per_page: 100
1342
+ });
1343
+
1344
+ const tagsWithReleases = tags.map(tag => {
1345
+ const release = releases.find(r => r.tag_name === tag.name);
1346
+ return {
1347
+ name: tag.name,
1348
+ commit: tag.commit.sha,
1349
+ hasRelease: !!release,
1350
+ releaseId: release?.id,
1351
+ releaseBody: release?.body,
1352
+ releaseUrl: release?.html_url,
1353
+ publishedAt: release?.published_at
1354
+ };
1355
+ });
1356
+
1357
+ res.json({
1358
+ tags: tagsWithReleases,
1359
+ latestTag: tags[0]?.name || null
1360
+ });
1361
+ } catch (error) {
1362
+ console.error(`[Plugin IDE Error] /github/tags:`, error);
1363
+ res.status(500).json({
1364
+ error: error.message || 'Failed to fetch tags from GitHub.'
1365
+ });
1366
+ }
1367
+ });
1368
+
1369
+ router.post('/:pluginName/github/release', resolvePluginPath, async (req, res) => {
1370
+ try {
1371
+ const { token, tagName, description, uploadFiles = true } = req.body;
1372
+
1373
+ if (!token || !tagName) {
1374
+ return res.status(400).json({ error: 'Token and tag name are required.' });
1375
+ }
1376
+
1377
+ const packageJsonPath = path.join(req.pluginPath, 'package.json');
1378
+ if (!await fse.pathExists(packageJsonPath)) {
1379
+ return res.status(404).json({ error: 'package.json not found.' });
1380
+ }
1381
+
1382
+ const packageJson = await fse.readJson(packageJsonPath);
1383
+ const repoUrl = typeof packageJson.repository === 'string'
1384
+ ? packageJson.repository
1385
+ : packageJson.repository?.url;
1386
+
1387
+ if (!repoUrl) {
1388
+ return res.status(400).json({ error: 'No repository URL in package.json.' });
1389
+ }
1390
+
1391
+ const match = repoUrl.match(/github\.com[\/:](.+?)\/(.+?)(\.git)?$/);
1392
+ if (!match) {
1393
+ return res.status(400).json({ error: 'Invalid GitHub repository URL.' });
1394
+ }
1395
+
1396
+ const [, owner, repo] = match;
1397
+ const octokit = new Octokit({ auth: token });
1398
+
1399
+ // Проверяем, имеет ли пользователь права на запись в этот репозиторий
1400
+ const { data: user } = await octokit.users.getAuthenticated();
1401
+ const myUsername = user.login;
1402
+
1403
+ // Проверяем права доступа
1404
+ try {
1405
+ const { data: repoInfo } = await octokit.repos.get({ owner, repo });
1406
+
1407
+ // Если не владелец и не имеет прав push
1408
+ if (repoInfo.permissions && !repoInfo.permissions.push) {
1409
+ return res.status(403).json({
1410
+ error: `У вас нет прав на создание релиза в репозитории ${owner}/${repo}. Используйте "Создать Pull Request" для внесения изменений в чужие плагины.`,
1411
+ suggestPR: true
1412
+ });
1413
+ }
1414
+ } catch (permError) {
1415
+ // Если не можем получить инфо о репо, скорее всего нет доступа
1416
+ if (permError.status === 404) {
1417
+ return res.status(403).json({
1418
+ error: `Репозиторий ${owner}/${repo} не найден или у вас нет доступа. Для внесения изменений в чужие плагины используйте "Создать Pull Request".`,
1419
+ suggestPR: true
1420
+ });
1421
+ }
1422
+ throw permError;
1423
+ }
1424
+
1425
+ if (uploadFiles) {
1426
+ console.log(`[Plugin IDE] Uploading files before creating release ${tagName}`);
1427
+ await uploadFilesToGitHub(octokit, owner, repo, req.pluginPath);
1428
+ }
1429
+
1430
+ const { data: ref } = await octokit.git.getRef({
1431
+ owner,
1432
+ repo,
1433
+ ref: 'heads/main'
1434
+ });
1435
+ const commitSha = ref.object.sha;
1436
+
1437
+ console.log(`[Plugin IDE] Creating tag ${tagName}`);
1438
+ const { data: tagObject } = await octokit.git.createTag({
1439
+ owner,
1440
+ repo,
1441
+ tag: tagName,
1442
+ message: `Release ${tagName}`,
1443
+ object: commitSha,
1444
+ type: 'commit'
1445
+ });
1446
+
1447
+ await octokit.git.createRef({
1448
+ owner,
1449
+ repo,
1450
+ ref: `refs/tags/${tagName}`,
1451
+ sha: tagObject.sha
1452
+ });
1453
+
1454
+ console.log(`[Plugin IDE] Creating release for ${tagName}`);
1455
+ const { data: release } = await octokit.repos.createRelease({
1456
+ owner,
1457
+ repo,
1458
+ tag_name: tagName,
1459
+ name: tagName,
1460
+ body: description || '',
1461
+ draft: false,
1462
+ prerelease: false
1463
+ });
1464
+
1465
+ res.json({
1466
+ success: true,
1467
+ tag: tagName,
1468
+ releaseUrl: release.html_url,
1469
+ message: `Release ${tagName} created successfully`
1470
+ });
1471
+ } catch (error) {
1472
+ console.error(`[Plugin IDE Error] /github/release:`, error);
1473
+ res.status(500).json({
1474
+ error: error.message || 'Failed to create release on GitHub.'
1475
+ });
1476
+ }
1477
+ });
1478
+
1479
+ router.get('/:pluginName/git/status', resolvePluginPath, async (req, res) => {
1480
+ try {
1481
+ const { execSync } = require('child_process');
1482
+
1483
+ const gitDir = path.join(req.pluginPath, '.git');
1484
+ if (!await fse.pathExists(gitDir)) {
1485
+ return res.json({
1486
+ isGitRepo: false,
1487
+ message: 'Not a git repository'
1488
+ });
1489
+ }
1490
+
1491
+ const statusOutput = execSync('git status --porcelain', {
1492
+ cwd: req.pluginPath,
1493
+ encoding: 'utf8'
1494
+ });
1495
+
1496
+ const staged = [];
1497
+ const unstaged = [];
1498
+ const lines = statusOutput.split('\n').filter(line => line.trim());
1499
+
1500
+ for (const line of lines) {
1501
+ const statusCodes = line.substring(0, 2);
1502
+ const filePath = line.substring(3);
1503
+
1504
+ const x = statusCodes[0];
1505
+ const y = statusCodes[1];
1506
+
1507
+ let status = 'M';
1508
+ if (x === 'A' || y === 'A') status = 'A';
1509
+ if (x === 'D' || y === 'D') status = 'D';
1510
+ if (x === 'R') status = 'R';
1511
+ if (x === '?' && y === '?') status = '?';
1512
+
1513
+ if (x !== ' ' && x !== '?') {
1514
+ staged.push({
1515
+ path: filePath,
1516
+ status: x
1517
+ });
1518
+ }
1519
+
1520
+ if (y !== ' ') {
1521
+ unstaged.push({
1522
+ path: filePath,
1523
+ status: y === '?' ? '?' : y
1524
+ });
1525
+ }
1526
+ }
1527
+
1528
+ let branch = 'main';
1529
+ try {
1530
+ branch = execSync('git rev-parse --abbrev-ref HEAD', {
1531
+ cwd: req.pluginPath,
1532
+ encoding: 'utf8'
1533
+ }).trim();
1534
+ } catch (branchError) {
1535
+ // Если HEAD не существует (нет коммитов), пытаемся получить дефолтную ветку
1536
+ try {
1537
+ branch = execSync('git branch --show-current', {
1538
+ cwd: req.pluginPath,
1539
+ encoding: 'utf8'
1540
+ }).trim() || 'master';
1541
+ } catch {
1542
+ // Если и это не работает, остается 'main'
1543
+ }
1544
+ }
1545
+
1546
+ res.json({
1547
+ isGitRepo: true,
1548
+ branch,
1549
+ staged,
1550
+ unstaged
1551
+ });
1552
+ } catch (error) {
1553
+ console.error(`[Plugin IDE Error] /git/status:`, error);
1554
+ res.status(500).json({
1555
+ error: error.message || 'Failed to get git status.'
1556
+ });
1557
+ }
1558
+ });
1559
+
1560
+ router.post('/:pluginName/git/add', resolvePluginPath, async (req, res) => {
1561
+ try {
1562
+ const { files } = req.body;
1563
+ const { execSync } = require('child_process');
1564
+
1565
+ if (!files || !Array.isArray(files) || files.length === 0) {
1566
+ return res.status(400).json({ error: 'Files array is required.' });
1567
+ }
1568
+
1569
+ for (const file of files) {
1570
+ execSync(`git add "${file}"`, {
1571
+ cwd: req.pluginPath
1572
+ });
1573
+ }
1574
+
1575
+ res.json({
1576
+ success: true,
1577
+ message: `Staged ${files.length} file(s)`
1578
+ });
1579
+ } catch (error) {
1580
+ console.error(`[Plugin IDE Error] /git/add:`, error);
1581
+ res.status(500).json({
1582
+ error: error.message || 'Failed to stage files.'
1583
+ });
1584
+ }
1585
+ });
1586
+
1587
+ router.post('/:pluginName/git/reset', resolvePluginPath, async (req, res) => {
1588
+ try {
1589
+ const { files } = req.body;
1590
+ const { execSync } = require('child_process');
1591
+
1592
+ if (!files || !Array.isArray(files) || files.length === 0) {
1593
+ return res.status(400).json({ error: 'Files array is required.' });
1594
+ }
1595
+
1596
+ // Check if HEAD exists (repo has commits)
1597
+ let hasCommits = true;
1598
+ try {
1599
+ execSync('git rev-parse --verify HEAD', {
1600
+ cwd: req.pluginPath,
1601
+ stdio: 'ignore'
1602
+ });
1603
+ } catch {
1604
+ hasCommits = false;
1605
+ }
1606
+
1607
+ // Reset files
1608
+ for (const file of files) {
1609
+ if (hasCommits) {
1610
+ execSync(`git reset HEAD "${file}"`, {
1611
+ cwd: req.pluginPath
1612
+ });
1613
+ } else {
1614
+ execSync(`git rm --cached -f "${file}"`, {
1615
+ cwd: req.pluginPath
1616
+ });
1617
+ }
1618
+ }
1619
+
1620
+ res.json({
1621
+ success: true,
1622
+ message: `Unstaged ${files.length} file(s)`
1623
+ });
1624
+ } catch (error) {
1625
+ console.error(`[Plugin IDE Error] /git/reset:`, error);
1626
+ res.status(500).json({
1627
+ error: error.message || 'Failed to unstage files.'
1628
+ });
1629
+ }
1630
+ });
1631
+
1632
+ router.post('/:pluginName/git/commit', resolvePluginPath, async (req, res) => {
1633
+ try {
1634
+ const { message } = req.body;
1635
+ const { execSync } = require('child_process');
1636
+
1637
+ if (!message || !message.trim()) {
1638
+ return res.status(400).json({ error: 'Commit message is required.' });
1639
+ }
1640
+
1641
+ const output = execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
1642
+ cwd: req.pluginPath,
1643
+ encoding: 'utf8'
1644
+ });
1645
+
1646
+ res.json({
1647
+ success: true,
1648
+ message: 'Commit created successfully',
1649
+ output
1650
+ });
1651
+ } catch (error) {
1652
+ console.error(`[Plugin IDE Error] /git/commit:`, error);
1653
+ res.status(500).json({
1654
+ error: error.message || 'Failed to create commit.'
1655
+ });
1656
+ }
1657
+ });
1658
+
1659
+ router.post('/:pluginName/git/push', resolvePluginPath, async (req, res) => {
1660
+ try {
1661
+ const { execSync } = require('child_process');
1662
+
1663
+ const output = execSync('git push origin main', {
1664
+ cwd: req.pluginPath,
1665
+ encoding: 'utf8'
1666
+ });
1667
+
1668
+ res.json({
1669
+ success: true,
1670
+ message: 'Pushed to GitHub successfully',
1671
+ output
1672
+ });
1673
+ } catch (error) {
1674
+ console.error(`[Plugin IDE Error] /git/push:`, error);
1675
+ res.status(500).json({
1676
+ error: error.message || 'Failed to push to GitHub.'
1677
+ });
1678
+ }
1679
+ });
1680
+
1681
+ router.post('/:pluginName/git/pull', resolvePluginPath, async (req, res) => {
1682
+ try {
1683
+ const { execSync } = require('child_process');
1684
+
1685
+ execSync('git fetch origin', { cwd: req.pluginPath });
1686
+
1687
+ let mainBranch = 'main';
1688
+ try {
1689
+ execSync('git show-ref --verify refs/remotes/origin/main', {
1690
+ cwd: req.pluginPath,
1691
+ stdio: 'ignore'
1692
+ });
1693
+ } catch {
1694
+ mainBranch = 'master';
1695
+ }
1696
+
1697
+ const output = execSync(`git pull origin ${mainBranch}`, {
1698
+ cwd: req.pluginPath,
1699
+ encoding: 'utf8'
1700
+ });
1701
+
1702
+ res.json({
1703
+ success: true,
1704
+ message: `Pulled from GitHub (branch: ${mainBranch})`,
1705
+ branch: mainBranch,
1706
+ output
1707
+ });
1708
+ } catch (error) {
1709
+ console.error(`[Plugin IDE Error] /git/pull:`, error);
1710
+ res.status(500).json({
1711
+ error: error.message || 'Failed to pull from GitHub.'
1712
+ });
1713
+ }
1714
+ });
1715
+
1716
+ router.post('/:pluginName/git/sync', resolvePluginPath, async (req, res) => {
1717
+ try {
1718
+ const { execSync } = require('child_process');
1719
+
1720
+ execSync('git fetch origin', { cwd: req.pluginPath });
1721
+
1722
+ let mainBranch = 'main';
1723
+ try {
1724
+ execSync('git show-ref --verify refs/remotes/origin/main', {
1725
+ cwd: req.pluginPath,
1726
+ stdio: 'ignore'
1727
+ });
1728
+ } catch {
1729
+ mainBranch = 'master';
1730
+ }
1731
+
1732
+ let currentBranch = '';
1733
+ try {
1734
+ currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
1735
+ cwd: req.pluginPath,
1736
+ encoding: 'utf8'
1737
+ }).trim();
1738
+ } catch {
1739
+
1740
+ }
1741
+
1742
+
1743
+ execSync(`git reset --hard origin/${mainBranch}`, { cwd: req.pluginPath });
1744
+
1745
+ if (!currentBranch || currentBranch === 'HEAD') {
1746
+ execSync(`git branch -M ${mainBranch}`, { cwd: req.pluginPath });
1747
+ }
1748
+
1749
+ res.json({
1750
+ success: true,
1751
+ message: `Синхронизировано с GitHub (ветка: ${mainBranch})`,
1752
+ branch: mainBranch
1753
+ });
1754
+ } catch (error) {
1755
+ console.error(`[Plugin IDE Error] /git/sync:`, error);
1756
+ res.status(500).json({
1757
+ error: error.message || 'Не удалось синхронизировать с GitHub.'
1758
+ });
1759
+ }
1760
+ });
1761
+
1762
+ router.get('/:pluginName/git/log', resolvePluginPath, async (req, res) => {
1763
+ try {
1764
+ const { execSync } = require('child_process');
1765
+ const { limit = 20 } = req.query;
1766
+
1767
+ let output = '';
1768
+ try {
1769
+ output = execSync(`git log --pretty=format:"%H|||%an|||%ae|||%at|||%s" -n ${limit}`, {
1770
+ cwd: req.pluginPath,
1771
+ encoding: 'utf8'
1772
+ });
1773
+ } catch (logError) {
1774
+ // Если нет коммитов, возвращаем пустой массив
1775
+ if (logError.message.includes('does not have any commits yet') ||
1776
+ logError.message.includes('bad default revision')) {
1777
+ return res.json({ commits: [] });
1778
+ }
1779
+ throw logError;
1780
+ }
1781
+
1782
+ const commits = output.split('\n').filter(line => line.trim()).map(line => {
1783
+ const [hash, author, email, timestamp, message] = line.split('|||');
1784
+ return {
1785
+ hash,
1786
+ author,
1787
+ email,
1788
+ date: new Date(parseInt(timestamp) * 1000).toISOString(),
1789
+ message
1790
+ };
1791
+ });
1792
+
1793
+ res.json({ commits });
1794
+ } catch (error) {
1795
+ console.error(`[Plugin IDE Error] /git/log:`, error);
1796
+ res.status(500).json({
1797
+ error: error.message || 'Failed to get git log.'
1798
+ });
1799
+ }
1800
+ });
1801
+
1802
+ router.get('/:pluginName/git/diff', resolvePluginPath, async (req, res) => {
1803
+ try {
1804
+ const { execSync } = require('child_process');
1805
+ const { path: filePath, staged, status } = req.query;
1806
+
1807
+ if (!filePath) {
1808
+ return res.status(400).json({ error: 'File path is required' });
1809
+ }
1810
+
1811
+ let diff = '';
1812
+
1813
+ if (status === '?' || status === 'A') {
1814
+ try {
1815
+ const fullPath = path.join(req.pluginPath, filePath);
1816
+ const content = await fse.readFile(fullPath, 'utf8');
1817
+ const lines = content.split('\n');
1818
+ diff = `--- /dev/null\n+++ b/${filePath}\n@@ -0,0 +1,${lines.length} @@\n`;
1819
+ diff += lines.map(line => '+' + line).join('\n');
1820
+ } catch (readError) {
1821
+ return res.status(500).json({ error: 'Failed to read file content' });
1822
+ }
1823
+ } else {
1824
+
1825
+ const command = staged === 'true'
1826
+ ? `git diff --cached "${filePath}"`
1827
+ : `git diff "${filePath}"`;
1828
+
1829
+ try {
1830
+ diff = execSync(command, {
1831
+ cwd: req.pluginPath,
1832
+ encoding: 'utf8'
1833
+ });
1834
+
1835
+ if (!diff && status === 'M') {
1836
+ diff = 'No changes detected (this might be a git issue)';
1837
+ }
1838
+ } catch (diffError) {
1839
+ try {
1840
+ const fullPath = path.join(req.pluginPath, filePath);
1841
+ const content = await fse.readFile(fullPath, 'utf8');
1842
+ const lines = content.split('\n');
1843
+ diff = `--- /dev/null\n+++ b/${filePath}\n@@ -0,0 +1,${lines.length} @@\n`;
1844
+ diff += lines.map(line => '+' + line).join('\n');
1845
+ } catch {
1846
+ diff = `Error getting diff: ${diffError.message}`;
1847
+ }
1848
+ }
1849
+ }
1850
+
1851
+ res.json({ diff: diff || 'No changes', path: filePath });
1852
+ } catch (error) {
1853
+ console.error(`[Plugin IDE Error] /git/diff:`, error);
1854
+ res.status(500).json({
1855
+ error: error.message || 'Failed to get file diff.'
1856
+ });
1857
+ }
1858
+ });
1859
+
1860
+ async function uploadFilesToGitHub(octokit, owner, repo, pluginPath) {
1861
+ const filesToUpload = [];
1862
+
1863
+ const collectFiles = async (dir, baseDir = pluginPath) => {
1864
+ const entries = await fse.readdir(dir, { withFileTypes: true });
1865
+
1866
+ for (const entry of entries) {
1867
+ const fullPath = path.join(dir, entry.name);
1868
+ const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
1869
+
1870
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') {
1871
+ continue;
1872
+ }
1873
+
1874
+ if (entry.isDirectory()) {
1875
+ await collectFiles(fullPath, baseDir);
1876
+ } else {
1877
+ const content = await fse.readFile(fullPath);
1878
+ filesToUpload.push({
1879
+ path: relativePath,
1880
+ content: content.toString('base64')
1881
+ });
1882
+ }
1883
+ }
1884
+ };
1885
+
1886
+ await collectFiles(pluginPath);
1887
+
1888
+ console.log(`[Plugin IDE] Uploading ${filesToUpload.length} files to ${owner}/${repo}`);
1889
+
1890
+ let isEmptyRepo = false;
1891
+ try {
1892
+ await octokit.git.getRef({
1893
+ owner,
1894
+ repo,
1895
+ ref: 'heads/main'
1896
+ });
1897
+ } catch (error) {
1898
+ isEmptyRepo = true;
1899
+ console.log('[Plugin IDE] Empty repository detected, using Contents API');
1900
+ }
1901
+
1902
+ if (isEmptyRepo) {
1903
+ for (const file of filesToUpload) {
1904
+ await octokit.repos.createOrUpdateFileContents({
1905
+ owner,
1906
+ repo,
1907
+ path: file.path,
1908
+ message: `Add ${file.path}`,
1909
+ content: file.content,
1910
+ branch: 'main'
1911
+ });
1912
+ }
1913
+ console.log(`[Plugin IDE] Successfully uploaded ${filesToUpload.length} files via Contents API`);
1914
+ } else {
1915
+ const { data: ref } = await octokit.git.getRef({
1916
+ owner,
1917
+ repo,
1918
+ ref: 'heads/main'
1919
+ });
1920
+ const parentSha = ref.object.sha;
1921
+
1922
+ const { data: commit } = await octokit.git.getCommit({
1923
+ owner,
1924
+ repo,
1925
+ commit_sha: parentSha
1926
+ });
1927
+ const baseTreeSha = commit.tree.sha;
1928
+
1929
+ const blobs = await Promise.all(
1930
+ filesToUpload.map(async (file) => {
1931
+ const { data } = await octokit.git.createBlob({
1932
+ owner,
1933
+ repo,
1934
+ content: file.content,
1935
+ encoding: 'base64'
1936
+ });
1937
+ return {
1938
+ path: file.path,
1939
+ mode: '100644',
1940
+ type: 'blob',
1941
+ sha: data.sha
1942
+ };
1943
+ })
1944
+ );
1945
+
1946
+ const { data: tree } = await octokit.git.createTree({
1947
+ owner,
1948
+ repo,
1949
+ tree: blobs,
1950
+ base_tree: baseTreeSha
1951
+ });
1952
+
1953
+ const { data: newCommit } = await octokit.git.createCommit({
1954
+ owner,
1955
+ repo,
1956
+ message: 'Update plugin files from BlockMine IDE',
1957
+ tree: tree.sha,
1958
+ parents: [parentSha]
1959
+ });
1960
+
1961
+ await octokit.git.updateRef({
1962
+ owner,
1963
+ repo,
1964
+ ref: 'heads/main',
1965
+ sha: newCommit.sha
1966
+ });
1967
+
1968
+ console.log(`[Plugin IDE] Successfully uploaded ${filesToUpload.length} files via Git Tree API`);
1969
+ }
1970
+ }
1971
+
1972
+ /**
1973
+ * POST /:pluginName/submit-to-official-list
1974
+ * Создает PR в blockmineJS/official-plugins-list для добавления плагина
1975
+ */
1976
+ router.post('/:pluginName/submit-to-official-list', resolvePluginPath, async (req, res) => {
1977
+ try {
1978
+ const { token, pluginDisplayName, icon } = req.body;
1979
+
1980
+ if (!token) {
1981
+ return res.status(400).json({ error: 'GitHub token is required.' });
1982
+ }
1983
+
1984
+ if (!pluginDisplayName || !icon) {
1985
+ return res.status(400).json({ error: 'Plugin name and icon are required.' });
1986
+ }
1987
+
1988
+ // Читаем package.json плагина
1989
+ const packageJsonPath = path.join(req.pluginPath, 'package.json');
1990
+ if (!await fse.pathExists(packageJsonPath)) {
1991
+ return res.status(404).json({ error: 'package.json not found.' });
1992
+ }
1993
+
1994
+ const packageJson = await fse.readJson(packageJsonPath);
1995
+ const repoUrl = typeof packageJson.repository === 'string'
1996
+ ? packageJson.repository
1997
+ : packageJson.repository?.url;
1998
+
1999
+ if (!repoUrl) {
2000
+ return res.status(400).json({ error: 'No repository URL in package.json.' });
2001
+ }
2002
+
2003
+ // Извлекаем owner/repo из URL
2004
+ const match = repoUrl.match(/github\.com[\/:](.+?)\/(.+?)(\.git)?$/);
2005
+ if (!match) {
2006
+ return res.status(400).json({ error: 'Invalid GitHub repository URL.' });
2007
+ }
2008
+
2009
+ const [, owner, repoName] = match;
2010
+ const cleanRepoName = repoName.replace('.git', '');
2011
+
2012
+ const octokit = new Octokit({ auth: token });
2013
+
2014
+ // Получаем latest tag
2015
+ let latestTag = null;
2016
+ try {
2017
+ const { data: tags } = await octokit.repos.listTags({
2018
+ owner,
2019
+ repo: cleanRepoName,
2020
+ per_page: 1
2021
+ });
2022
+ if (tags.length > 0) {
2023
+ latestTag = tags[0].name;
2024
+ }
2025
+ } catch (error) {
2026
+ console.warn('[Plugin IDE] Could not fetch tags:', error.message);
2027
+ }
2028
+
2029
+ if (!latestTag) {
2030
+ return res.status(400).json({ error: 'Plugin must have at least one release tag.' });
2031
+ }
2032
+
2033
+ // Сохраняем иконку в package.json если её там нет
2034
+ if (!packageJson.botpanel) {
2035
+ packageJson.botpanel = {};
2036
+ }
2037
+
2038
+ if (!packageJson.botpanel.icon) {
2039
+ packageJson.botpanel.icon = icon;
2040
+ await fse.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2041
+ console.log(`[Plugin IDE] Saved icon "${icon}" to package.json`);
2042
+ }
2043
+
2044
+ // Берём категории, зависимости и supportedHosts из package.json
2045
+ const categories = packageJson.botpanel?.categories || [];
2046
+ const supportedHosts = packageJson.botpanel?.supportedHosts || [];
2047
+ const dependencies = packageJson.botpanel?.dependencies || [];
2048
+
2049
+ // Формируем entry для официального списка
2050
+ const pluginEntry = {
2051
+ id: cleanRepoName,
2052
+ name: pluginDisplayName,
2053
+ author: packageJson.author || owner,
2054
+ description: packageJson.description || '',
2055
+ repoUrl: repoUrl.replace('.git', ''),
2056
+ icon: icon,
2057
+ latestTag: latestTag,
2058
+ categories: categories,
2059
+ supportedHosts: supportedHosts,
2060
+ dependencies: dependencies
2061
+ };
2062
+
2063
+ // Работаем с official-plugins-list репозиторием
2064
+ const listOwner = 'blockmineJS';
2065
+ const listRepo = 'official-plugins-list';
2066
+
2067
+ console.log(`[Plugin IDE] Creating PR for plugin ${cleanRepoName} in official list`);
2068
+
2069
+ // Получаем default branch (main или master)
2070
+ const { data: repoInfo } = await octokit.repos.get({
2071
+ owner: listOwner,
2072
+ repo: listRepo
2073
+ });
2074
+ const defaultBranch = repoInfo.default_branch;
2075
+
2076
+ // Получаем SHA основной ветки
2077
+ const { data: refData } = await octokit.git.getRef({
2078
+ owner: listOwner,
2079
+ repo: listRepo,
2080
+ ref: `heads/${defaultBranch}`
2081
+ });
2082
+ const baseSha = refData.object.sha;
2083
+
2084
+ // Получаем текущий index.json для проверки, существует ли плагин
2085
+ const { data: fileData } = await octokit.repos.getContent({
2086
+ owner: listOwner,
2087
+ repo: listRepo,
2088
+ path: 'index.json',
2089
+ ref: defaultBranch
2090
+ });
2091
+
2092
+ const currentContent = Buffer.from(fileData.content, 'base64').toString('utf8');
2093
+ const pluginsList = JSON.parse(currentContent);
2094
+
2095
+ // Проверяем, не добавлен ли уже плагин
2096
+ const existingPlugin = pluginsList.find(p => p.id === cleanRepoName);
2097
+ const isUpdate = !!existingPlugin;
2098
+ const oldVersion = existingPlugin?.latestTag;
2099
+
2100
+ // Название ветки зависит от типа операции
2101
+ const branchName = isUpdate ? `update-plugin-${cleanRepoName}` : `add-plugin-${cleanRepoName}`;
2102
+
2103
+ // Проверяем, существует ли уже ветка с таким именем
2104
+ let branchExists = false;
2105
+ try {
2106
+ await octokit.git.getRef({
2107
+ owner: listOwner,
2108
+ repo: listRepo,
2109
+ ref: `heads/${branchName}`
2110
+ });
2111
+ branchExists = true;
2112
+ } catch (error) {
2113
+ // Ветка не существует, это нормально
2114
+ }
2115
+
2116
+ // Если ветка существует, удаляем её
2117
+ if (branchExists) {
2118
+ try {
2119
+ await octokit.git.deleteRef({
2120
+ owner: listOwner,
2121
+ repo: listRepo,
2122
+ ref: `heads/${branchName}`
2123
+ });
2124
+ console.log(`[Plugin IDE] Deleted existing branch ${branchName}`);
2125
+ } catch (error) {
2126
+ console.warn('[Plugin IDE] Could not delete existing branch:', error.message);
2127
+ }
2128
+ }
2129
+
2130
+ // Создаем новую ветку
2131
+ await octokit.git.createRef({
2132
+ owner: listOwner,
2133
+ repo: listRepo,
2134
+ ref: `refs/heads/${branchName}`,
2135
+ sha: baseSha
2136
+ });
2137
+
2138
+ // Обновляем или добавляем плагин в список
2139
+ const existingIndex = pluginsList.findIndex(p => p.id === cleanRepoName);
2140
+ if (existingIndex !== -1) {
2141
+ // Обновляем существующий
2142
+ pluginsList[existingIndex] = pluginEntry;
2143
+ } else {
2144
+ // Добавляем новый
2145
+ pluginsList.push(pluginEntry);
2146
+ }
2147
+
2148
+ // Сортируем по id для консистентности
2149
+ pluginsList.sort((a, b) => a.id.localeCompare(b.id));
2150
+
2151
+ const newContent = JSON.stringify(pluginsList, null, 2) + '\n';
2152
+
2153
+ // Создаем коммит с обновленным index.json
2154
+ const { data: blob } = await octokit.git.createBlob({
2155
+ owner: listOwner,
2156
+ repo: listRepo,
2157
+ content: Buffer.from(newContent).toString('base64'),
2158
+ encoding: 'base64'
2159
+ });
2160
+
2161
+ const { data: baseCommit } = await octokit.git.getCommit({
2162
+ owner: listOwner,
2163
+ repo: listRepo,
2164
+ commit_sha: baseSha
2165
+ });
2166
+
2167
+ const { data: tree } = await octokit.git.createTree({
2168
+ owner: listOwner,
2169
+ repo: listRepo,
2170
+ tree: [
2171
+ {
2172
+ path: 'index.json',
2173
+ mode: '100644',
2174
+ type: 'blob',
2175
+ sha: blob.sha
2176
+ }
2177
+ ],
2178
+ base_tree: baseCommit.tree.sha
2179
+ });
2180
+
2181
+ // Commit message и PR body зависят от типа операции
2182
+ const commitMessage = isUpdate
2183
+ ? `Update ${pluginDisplayName} to ${latestTag}`
2184
+ : `Add ${pluginDisplayName} plugin`;
2185
+
2186
+ const { data: commit } = await octokit.git.createCommit({
2187
+ owner: listOwner,
2188
+ repo: listRepo,
2189
+ message: commitMessage,
2190
+ tree: tree.sha,
2191
+ parents: [baseSha]
2192
+ });
2193
+
2194
+ await octokit.git.updateRef({
2195
+ owner: listOwner,
2196
+ repo: listRepo,
2197
+ ref: `heads/${branchName}`,
2198
+ sha: commit.sha
2199
+ });
2200
+
2201
+ // Создаем Pull Request
2202
+ const prTitle = isUpdate
2203
+ ? `🔄 Update ${pluginDisplayName} to ${latestTag}`
2204
+ : `✨ Add ${pluginDisplayName} plugin`;
2205
+
2206
+ const prBody = isUpdate
2207
+ ? `## Обновление плагина: ${pluginDisplayName}
2208
+
2209
+ **ID**: \`${cleanRepoName}\`
2210
+ **Автор**: ${packageJson.author || owner}
2211
+ **Описание**: ${packageJson.description || 'Нет описания'}
2212
+ **Репозиторий**: ${repoUrl.replace('.git', '')}
2213
+ **Старая версия**: ${oldVersion}
2214
+ **Новая версия**: ${latestTag}
2215
+
2216
+ ---
2217
+
2218
+ Этот PR был автоматически создан из BlockMine IDE.
2219
+
2220
+ ### Что нужно проверить:
2221
+ - [ ] Новая версия работает корректно
2222
+ - [ ] Обновленные данные плагина корректны
2223
+ - [ ] Заполнены \`categories\`, \`supportedHosts\`, \`dependencies\` (если нужно)
2224
+ - [ ] Иконка отображается корректно
2225
+ `
2226
+ : `## Новый плагин: ${pluginDisplayName}
2227
+
2228
+ **ID**: \`${cleanRepoName}\`
2229
+ **Автор**: ${packageJson.author || owner}
2230
+ **Описание**: ${packageJson.description || 'Нет описания'}
2231
+ **Репозиторий**: ${repoUrl.replace('.git', '')}
2232
+ **Версия**: ${latestTag}
2233
+
2234
+ ---
2235
+
2236
+ Этот PR был автоматически создан из BlockMine IDE.
2237
+
2238
+ ### Что нужно проверить:
2239
+ - [ ] Плагин работает
2240
+ - [ ] Описание корректно
2241
+ - [ ] Заполнены \`categories\`, \`supportedHosts\`, \`dependencies\` (если нужно)
2242
+ - [ ] Иконка отображается корректно
2243
+ `;
2244
+
2245
+ let prUrl;
2246
+ let prNumber;
2247
+
2248
+ try {
2249
+ const { data: pr } = await octokit.pulls.create({
2250
+ owner: listOwner,
2251
+ repo: listRepo,
2252
+ title: prTitle,
2253
+ head: branchName,
2254
+ base: defaultBranch,
2255
+ body: prBody
2256
+ });
2257
+ prUrl = pr.html_url;
2258
+ prNumber = pr.number;
2259
+ } catch (error) {
2260
+ // Возможно PR уже существует
2261
+ if (error.status === 422) {
2262
+ // Ищем существующий PR
2263
+ const { data: pulls } = await octokit.pulls.list({
2264
+ owner: listOwner,
2265
+ repo: listRepo,
2266
+ head: `${listOwner}:${branchName}`,
2267
+ state: 'open'
2268
+ });
2269
+
2270
+ if (pulls.length > 0) {
2271
+ prUrl = pulls[0].html_url;
2272
+ prNumber = pulls[0].number;
2273
+ } else {
2274
+ throw error;
2275
+ }
2276
+ } else {
2277
+ throw error;
2278
+ }
2279
+ }
2280
+
2281
+ res.json({
2282
+ success: true,
2283
+ prUrl,
2284
+ prNumber,
2285
+ message: 'Pull Request created successfully'
2286
+ });
2287
+
2288
+ } catch (error) {
2289
+ console.error(`[Plugin IDE Error] /submit-to-official-list:`, error);
2290
+ res.status(500).json({
2291
+ error: error.message || 'Failed to create Pull Request.'
2292
+ });
2293
+ }
2294
+ });
2295
+
716
2296
  module.exports = router;