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.
- package/.claude/agents/code-architect.md +34 -0
- package/.claude/agents/code-explorer.md +51 -0
- package/.claude/agents/code-reviewer.md +46 -0
- package/.claude/commands/feature-dev.md +125 -0
- package/.claude/settings.json +5 -1
- package/.claude/settings.local.json +12 -1
- package/.claude/skills/frontend-design/SKILL.md +42 -0
- package/CHANGELOG.md +32 -1
- package/README.md +302 -152
- package/backend/package-lock.json +681 -9
- package/backend/package.json +8 -0
- package/backend/prisma/migrations/20251116111851_add_execution_trace/migration.sql +22 -0
- package/backend/prisma/migrations/20251120154914_add_panel_api_keys/migration.sql +21 -0
- package/backend/prisma/migrations/20251121110241_add_proxy_table/migration.sql +45 -0
- package/backend/prisma/schema.prisma +70 -1
- package/backend/src/__tests__/services/BotLifecycleService.test.js +9 -4
- package/backend/src/ai/plugin-assistant-system-prompt.md +788 -0
- package/backend/src/api/middleware/auth.js +27 -0
- package/backend/src/api/middleware/botAccess.js +7 -3
- package/backend/src/api/middleware/panelApiAuth.js +135 -0
- package/backend/src/api/routes/aiAssistant.js +995 -0
- package/backend/src/api/routes/auth.js +90 -54
- package/backend/src/api/routes/botCommands.js +107 -0
- package/backend/src/api/routes/botGroups.js +165 -0
- package/backend/src/api/routes/botHistory.js +108 -0
- package/backend/src/api/routes/botPermissions.js +99 -0
- package/backend/src/api/routes/botStatus.js +36 -0
- package/backend/src/api/routes/botUsers.js +162 -0
- package/backend/src/api/routes/bots.js +108 -59
- package/backend/src/api/routes/eventGraphs.js +4 -1
- package/backend/src/api/routes/logs.js +13 -3
- package/backend/src/api/routes/panel.js +3 -3
- package/backend/src/api/routes/panelApiKeys.js +179 -0
- package/backend/src/api/routes/pluginIde.js +1715 -135
- package/backend/src/api/routes/plugins.js +170 -13
- package/backend/src/api/routes/proxies.js +130 -0
- package/backend/src/api/routes/search.js +4 -0
- package/backend/src/api/routes/servers.js +20 -3
- package/backend/src/api/routes/settings.js +5 -0
- package/backend/src/api/routes/system.js +3 -3
- package/backend/src/api/routes/traces.js +131 -0
- package/backend/src/config/debug.config.js +36 -0
- package/backend/src/core/BotHistoryStore.js +180 -0
- package/backend/src/core/BotManager.js +14 -4
- package/backend/src/core/BotProcess.js +1517 -1092
- package/backend/src/core/EventGraphManager.js +194 -280
- package/backend/src/core/GraphExecutionEngine.js +1004 -321
- package/backend/src/core/MessageQueue.js +12 -6
- package/backend/src/core/PluginLoader.js +99 -5
- package/backend/src/core/PluginManager.js +74 -13
- package/backend/src/core/TaskScheduler.js +1 -1
- package/backend/src/core/commands/whois.js +1 -1
- package/backend/src/core/node-registries/actions.js +72 -2
- package/backend/src/core/node-registries/arrays.js +18 -0
- package/backend/src/core/node-registries/data.js +1 -1
- package/backend/src/core/node-registries/events.js +14 -0
- package/backend/src/core/node-registries/logic.js +17 -0
- package/backend/src/core/node-registries/strings.js +34 -0
- package/backend/src/core/node-registries/type.js +25 -0
- package/backend/src/core/nodes/actions/bot_look_at.js +1 -1
- package/backend/src/core/nodes/actions/create_command.js +189 -0
- package/backend/src/core/nodes/actions/delete_command.js +92 -0
- package/backend/src/core/nodes/actions/http_request.js +23 -4
- package/backend/src/core/nodes/actions/send_message.js +2 -12
- package/backend/src/core/nodes/actions/update_command.js +133 -0
- package/backend/src/core/nodes/arrays/join.js +28 -0
- package/backend/src/core/nodes/data/cast.js +2 -1
- package/backend/src/core/nodes/data/string_literal.js +2 -13
- package/backend/src/core/nodes/logic/not.js +22 -0
- package/backend/src/core/nodes/strings/starts_with.js +1 -1
- package/backend/src/core/nodes/strings/to_lower.js +22 -0
- package/backend/src/core/nodes/strings/to_upper.js +22 -0
- package/backend/src/core/nodes/type/to_string.js +32 -0
- package/backend/src/core/services/BotLifecycleService.js +835 -596
- package/backend/src/core/services/CommandExecutionService.js +430 -351
- package/backend/src/core/services/DebugSessionManager.js +347 -0
- package/backend/src/core/services/GraphCollaborationManager.js +501 -0
- package/backend/src/core/services/MinecraftBotManager.js +259 -0
- package/backend/src/core/services/MinecraftViewerService.js +216 -0
- package/backend/src/core/services/TraceCollectorService.js +545 -0
- package/backend/src/core/system/RuntimeCommandRegistry.js +116 -0
- package/backend/src/core/system/Transport.js +0 -4
- package/backend/src/core/validation/nodeSchemas.js +6 -6
- package/backend/src/real-time/botApi/handlers/graphHandlers.js +2 -2
- package/backend/src/real-time/botApi/handlers/graphWebSocketHandlers.js +1 -1
- package/backend/src/real-time/botApi/utils.js +11 -0
- package/backend/src/real-time/panelNamespace.js +387 -0
- package/backend/src/real-time/presence.js +7 -2
- package/backend/src/real-time/socketHandler.js +395 -4
- package/backend/src/server.js +18 -0
- package/frontend/dist/assets/index-DqzDkFsP.js +11210 -0
- package/frontend/dist/assets/index-t6K1u4OV.css +32 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/package-lock.json +9437 -0
- package/frontend/package.json +8 -0
- package/package.json +2 -2
- package/screen/console.png +0 -0
- package/screen/dashboard.png +0 -0
- package/screen/graph_collabe.png +0 -0
- package/screen/graph_live_debug.png +0 -0
- package/screen/management_command.png +0 -0
- package/screen/node_debug_trace.png +0 -0
- package/screen/plugin_/320/276/320/261/320/267/320/276/321/200.png +0 -0
- package/screen/websocket.png +0 -0
- 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
- 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
- package/frontend/dist/assets/index-CfTo92bP.css +0 -1
- package/frontend/dist/assets/index-CiFD5X9Z.js +0 -8344
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
|
-
const { authenticate
|
|
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
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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: '
|
|
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
|
-
|
|
550
|
-
const
|
|
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:
|
|
571
|
-
manifest: JSON.stringify(packageJson.botpanel || {}),
|
|
572
|
-
isEnabled: false
|
|
674
|
+
sourceUri: currentPlugin.path, // Используем путь как sourceUri для локальных
|
|
573
675
|
}
|
|
574
676
|
});
|
|
575
677
|
|
|
576
|
-
|
|
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 = '
|
|
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 не установлен на этой системе.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
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;
|