@adversity/coding-tool-x 2.6.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/plugins/constants.js +14 -0
- package/src/plugins/event-bus.js +54 -0
- package/src/plugins/manifest-validator.js +129 -0
- package/src/plugins/plugin-api.js +128 -0
- package/src/plugins/plugin-installer.js +601 -0
- package/src/plugins/plugin-loader.js +229 -0
- package/src/plugins/plugin-manager.js +170 -0
- package/src/plugins/registry.js +152 -0
- package/src/plugins/schema/plugin-manifest.json +115 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync, spawn } = require('child_process');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const inquirer = require('inquirer');
|
|
7
|
+
|
|
8
|
+
const { PLUGINS_DIR, CONFIG_DIR, INSTALLED_DIR } = require('./constants');
|
|
9
|
+
const { validateManifest, checkVersionCompatibility } = require('./manifest-validator');
|
|
10
|
+
const { addPlugin, removePlugin, getPlugin, listPlugins, updatePlugin: updatePluginRegistry } = require('./registry');
|
|
11
|
+
|
|
12
|
+
// Core commands that plugins cannot override
|
|
13
|
+
const CORE_COMMANDS = [
|
|
14
|
+
'start', 'stop', 'restart', 'status',
|
|
15
|
+
'ui', 'logs', 'stats', 'doctor', 'reset',
|
|
16
|
+
'claude', 'codex', 'gemini', 'proxy',
|
|
17
|
+
'security', 'help', 'version'
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate Git URL format
|
|
22
|
+
* @param {string} url - Git URL to validate
|
|
23
|
+
* @returns {{ valid: boolean, error?: string }}
|
|
24
|
+
*/
|
|
25
|
+
function validateGitUrl(url) {
|
|
26
|
+
if (!url || typeof url !== 'string') {
|
|
27
|
+
return { valid: false, error: 'Git URL is required' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Support HTTPS URLs
|
|
31
|
+
const httpsPattern = /^https:\/\/github\.com\/[\w-]+\/[\w.-]+(?:\.git)?$/;
|
|
32
|
+
// Support SSH URLs
|
|
33
|
+
const sshPattern = /^git@github\.com:[\w-]+\/[\w.-]+(?:\.git)?$/;
|
|
34
|
+
// Support generic git URLs
|
|
35
|
+
const genericPattern = /^(https?|git):\/\/.+\.git$/;
|
|
36
|
+
|
|
37
|
+
if (httpsPattern.test(url) || sshPattern.test(url) || genericPattern.test(url)) {
|
|
38
|
+
return { valid: true };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
valid: false,
|
|
43
|
+
error: `Invalid Git URL format: "${url}". Expected formats:\n` +
|
|
44
|
+
' - https://github.com/user/repo.git\n' +
|
|
45
|
+
' - git@github.com:user/repo.git'
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generate a random temporary directory path
|
|
51
|
+
* @returns {string} Temporary directory path
|
|
52
|
+
*/
|
|
53
|
+
function getTempDir() {
|
|
54
|
+
const random = crypto.randomBytes(8).toString('hex');
|
|
55
|
+
return path.join(os.tmpdir(), `ctx-plugin-${random}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Execute a shell command and return result
|
|
60
|
+
* @param {string} command - Command to execute
|
|
61
|
+
* @param {Object} options - Options for execSync
|
|
62
|
+
* @returns {{ success: boolean, output?: string, error?: string }}
|
|
63
|
+
*/
|
|
64
|
+
function execCommand(command, options = {}) {
|
|
65
|
+
try {
|
|
66
|
+
const output = execSync(command, {
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
69
|
+
...options
|
|
70
|
+
});
|
|
71
|
+
return { success: true, output: output.trim() };
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return {
|
|
74
|
+
success: false,
|
|
75
|
+
error: error.stderr || error.message || 'Command failed'
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Recursively delete a directory
|
|
82
|
+
* @param {string} dirPath - Directory path to delete
|
|
83
|
+
*/
|
|
84
|
+
function deleteDirectory(dirPath) {
|
|
85
|
+
if (fs.existsSync(dirPath)) {
|
|
86
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Ensure a directory exists
|
|
92
|
+
* @param {string} dirPath - Directory path to create
|
|
93
|
+
*/
|
|
94
|
+
function ensureDirectory(dirPath) {
|
|
95
|
+
if (!fs.existsSync(dirPath)) {
|
|
96
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Copy directory recursively
|
|
102
|
+
* @param {string} src - Source directory
|
|
103
|
+
* @param {string} dest - Destination directory
|
|
104
|
+
*/
|
|
105
|
+
function copyDirectory(src, dest) {
|
|
106
|
+
ensureDirectory(dest);
|
|
107
|
+
|
|
108
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
109
|
+
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
const srcPath = path.join(src, entry.name);
|
|
112
|
+
const destPath = path.join(dest, entry.name);
|
|
113
|
+
|
|
114
|
+
if (entry.isDirectory()) {
|
|
115
|
+
copyDirectory(srcPath, destPath);
|
|
116
|
+
} else {
|
|
117
|
+
fs.copyFileSync(srcPath, destPath);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check for name conflicts with existing plugins
|
|
124
|
+
* @param {string} pluginName - Plugin name to check
|
|
125
|
+
* @returns {{ conflict: boolean, reason?: string }}
|
|
126
|
+
*/
|
|
127
|
+
function checkNameConflict(pluginName) {
|
|
128
|
+
const existingPlugin = getPlugin(pluginName);
|
|
129
|
+
|
|
130
|
+
if (existingPlugin) {
|
|
131
|
+
return {
|
|
132
|
+
conflict: true,
|
|
133
|
+
reason: `A plugin named "${pluginName}" is already installed (version ${existingPlugin.version})`
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { conflict: false };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check for command name conflicts with core commands
|
|
142
|
+
* @param {Array} pluginCommands - Array of command names from plugin
|
|
143
|
+
* @returns {{ conflict: boolean, reason?: string, conflicts?: Array }}
|
|
144
|
+
*/
|
|
145
|
+
function checkCommandConflicts(pluginCommands) {
|
|
146
|
+
if (!pluginCommands || !Array.isArray(pluginCommands)) {
|
|
147
|
+
return { conflict: false };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const conflicts = [];
|
|
151
|
+
|
|
152
|
+
for (const cmd of pluginCommands) {
|
|
153
|
+
const cmdName = typeof cmd === 'string' ? cmd : cmd.name;
|
|
154
|
+
if (CORE_COMMANDS.includes(cmdName)) {
|
|
155
|
+
conflicts.push(cmdName);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (conflicts.length > 0) {
|
|
160
|
+
return {
|
|
161
|
+
conflict: true,
|
|
162
|
+
reason: `Plugin commands conflict with core commands: ${conflicts.join(', ')}`,
|
|
163
|
+
conflicts
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { conflict: false };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Install npm dependencies for a plugin
|
|
172
|
+
* @param {string} pluginDir - Plugin directory path
|
|
173
|
+
* @returns {{ success: boolean, error?: string }}
|
|
174
|
+
*/
|
|
175
|
+
function installDependencies(pluginDir) {
|
|
176
|
+
const packageJsonPath = path.join(pluginDir, 'package.json');
|
|
177
|
+
|
|
178
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
179
|
+
return { success: true }; // No dependencies to install
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const result = execCommand('npm install --production --ignore-scripts', {
|
|
183
|
+
cwd: pluginDir,
|
|
184
|
+
timeout: 120000 // 2 minute timeout
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (!result.success) {
|
|
188
|
+
return {
|
|
189
|
+
success: false,
|
|
190
|
+
error: `Failed to install dependencies: ${result.error}`
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { success: true };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Install a plugin from a Git URL
|
|
199
|
+
* @param {string} gitUrl - Git repository URL
|
|
200
|
+
* @returns {Promise<{ success: boolean, plugin?: Object, error?: string }>}
|
|
201
|
+
*/
|
|
202
|
+
async function installPlugin(gitUrl) {
|
|
203
|
+
let tempDir = null;
|
|
204
|
+
let installedDir = null;
|
|
205
|
+
let registryAdded = false;
|
|
206
|
+
let pluginName = null;
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
// Step 1: Validate Git URL format
|
|
210
|
+
const urlValidation = validateGitUrl(gitUrl);
|
|
211
|
+
if (!urlValidation.valid) {
|
|
212
|
+
return { success: false, error: urlValidation.error };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Step 2: Security warning and user confirmation
|
|
216
|
+
console.warn('\n⚠️ WARNING: Plugins have full system access!');
|
|
217
|
+
console.warn('Only install plugins from trusted sources.');
|
|
218
|
+
console.warn(`Source: ${gitUrl}\n`);
|
|
219
|
+
|
|
220
|
+
const { confirmed } = await inquirer.prompt([
|
|
221
|
+
{
|
|
222
|
+
type: 'confirm',
|
|
223
|
+
name: 'confirmed',
|
|
224
|
+
message: 'Do you want to continue with the installation?',
|
|
225
|
+
default: false
|
|
226
|
+
}
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
if (!confirmed) {
|
|
230
|
+
return {
|
|
231
|
+
success: false,
|
|
232
|
+
error: 'Installation cancelled by user'
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Step 3: Clone to temp directory
|
|
237
|
+
tempDir = getTempDir();
|
|
238
|
+
const cloneResult = execCommand(`git clone --depth 1 "${gitUrl}" "${tempDir}"`, {
|
|
239
|
+
timeout: 60000 // 1 minute timeout
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (!cloneResult.success) {
|
|
243
|
+
return {
|
|
244
|
+
success: false,
|
|
245
|
+
error: `Failed to clone repository: ${cloneResult.error}`
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Step 4: Read and validate plugin.json
|
|
250
|
+
const manifestPath = path.join(tempDir, 'plugin.json');
|
|
251
|
+
|
|
252
|
+
if (!fs.existsSync(manifestPath)) {
|
|
253
|
+
return {
|
|
254
|
+
success: false,
|
|
255
|
+
error: 'Invalid plugin: plugin.json not found in repository root'
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let manifest;
|
|
260
|
+
try {
|
|
261
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
262
|
+
} catch (parseError) {
|
|
263
|
+
return {
|
|
264
|
+
success: false,
|
|
265
|
+
error: `Failed to parse plugin.json: ${parseError.message}`
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const validation = validateManifest(manifest);
|
|
270
|
+
if (!validation.valid) {
|
|
271
|
+
const errorMessages = validation.errors
|
|
272
|
+
.map(e => ` - ${e.field}: ${e.message}`)
|
|
273
|
+
.join('\n');
|
|
274
|
+
return {
|
|
275
|
+
success: false,
|
|
276
|
+
error: `Invalid plugin.json:\n${errorMessages}`
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
pluginName = manifest.name;
|
|
281
|
+
|
|
282
|
+
// Step 5: Check version compatibility
|
|
283
|
+
if (manifest.minVersion) {
|
|
284
|
+
const versionCheck = checkVersionCompatibility(manifest.minVersion);
|
|
285
|
+
if (!versionCheck.compatible) {
|
|
286
|
+
return {
|
|
287
|
+
success: false,
|
|
288
|
+
error: versionCheck.reason
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Step 6: Check for name conflicts with existing plugins
|
|
294
|
+
const nameCheck = checkNameConflict(pluginName);
|
|
295
|
+
if (nameCheck.conflict) {
|
|
296
|
+
return {
|
|
297
|
+
success: false,
|
|
298
|
+
error: nameCheck.reason
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Step 7: Check for command name conflicts with core commands
|
|
303
|
+
const commandCheck = checkCommandConflicts(manifest.commands);
|
|
304
|
+
if (commandCheck.conflict) {
|
|
305
|
+
return {
|
|
306
|
+
success: false,
|
|
307
|
+
error: commandCheck.reason
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Step 8: Install npm dependencies if package.json exists
|
|
312
|
+
const depResult = installDependencies(tempDir);
|
|
313
|
+
if (!depResult.success) {
|
|
314
|
+
return {
|
|
315
|
+
success: false,
|
|
316
|
+
error: depResult.error
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Step 9: Move to final location
|
|
321
|
+
installedDir = path.join(INSTALLED_DIR, pluginName);
|
|
322
|
+
ensureDirectory(INSTALLED_DIR);
|
|
323
|
+
|
|
324
|
+
// Remove any existing directory (shouldn't exist due to conflict check, but be safe)
|
|
325
|
+
if (fs.existsSync(installedDir)) {
|
|
326
|
+
deleteDirectory(installedDir);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Move temp dir to installed dir
|
|
330
|
+
copyDirectory(tempDir, installedDir);
|
|
331
|
+
|
|
332
|
+
// Step 10: Update registry
|
|
333
|
+
addPlugin(pluginName, {
|
|
334
|
+
version: manifest.version,
|
|
335
|
+
enabled: true,
|
|
336
|
+
source: gitUrl,
|
|
337
|
+
loadOrder: manifest.loadOrder || 10
|
|
338
|
+
});
|
|
339
|
+
registryAdded = true;
|
|
340
|
+
|
|
341
|
+
// Clean up temp directory
|
|
342
|
+
deleteDirectory(tempDir);
|
|
343
|
+
tempDir = null;
|
|
344
|
+
|
|
345
|
+
// Return success with plugin info
|
|
346
|
+
return {
|
|
347
|
+
success: true,
|
|
348
|
+
plugin: {
|
|
349
|
+
name: pluginName,
|
|
350
|
+
version: manifest.version,
|
|
351
|
+
description: manifest.description,
|
|
352
|
+
author: manifest.author,
|
|
353
|
+
commands: manifest.commands ? manifest.commands.length : 0,
|
|
354
|
+
hooks: manifest.hooks ? manifest.hooks.length : 0,
|
|
355
|
+
source: gitUrl,
|
|
356
|
+
installedAt: new Date().toISOString()
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
} catch (error) {
|
|
361
|
+
// ROLLBACK on any failure
|
|
362
|
+
if (tempDir) {
|
|
363
|
+
deleteDirectory(tempDir);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (installedDir && fs.existsSync(installedDir)) {
|
|
367
|
+
deleteDirectory(installedDir);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (registryAdded && pluginName) {
|
|
371
|
+
removePlugin(pluginName);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
success: false,
|
|
376
|
+
error: `Installation failed: ${error.message}`
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Uninstall a plugin by name
|
|
383
|
+
* @param {string} name - Plugin name to uninstall
|
|
384
|
+
* @returns {{ success: boolean, message?: string, error?: string }}
|
|
385
|
+
*/
|
|
386
|
+
function uninstallPlugin(name) {
|
|
387
|
+
try {
|
|
388
|
+
// Check if plugin exists in registry
|
|
389
|
+
const plugin = getPlugin(name);
|
|
390
|
+
if (!plugin) {
|
|
391
|
+
return {
|
|
392
|
+
success: false,
|
|
393
|
+
error: `Plugin "${name}" is not installed`
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Remove from registry first
|
|
398
|
+
removePlugin(name);
|
|
399
|
+
|
|
400
|
+
// Delete installed directory
|
|
401
|
+
const pluginDir = path.join(INSTALLED_DIR, name);
|
|
402
|
+
if (fs.existsSync(pluginDir)) {
|
|
403
|
+
deleteDirectory(pluginDir);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Delete config file if exists
|
|
407
|
+
const configFile = path.join(CONFIG_DIR, `${name}.json`);
|
|
408
|
+
if (fs.existsSync(configFile)) {
|
|
409
|
+
fs.unlinkSync(configFile);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
success: true,
|
|
414
|
+
message: `Plugin "${name}" has been uninstalled successfully`
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
} catch (error) {
|
|
418
|
+
return {
|
|
419
|
+
success: false,
|
|
420
|
+
error: `Failed to uninstall plugin "${name}": ${error.message}`
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Update a plugin to the latest version
|
|
427
|
+
* @param {string} name - Plugin name to update
|
|
428
|
+
* @returns {Promise<{ success: boolean, plugin?: Object, message?: string, error?: string }>}
|
|
429
|
+
*/
|
|
430
|
+
async function updatePlugin(name) {
|
|
431
|
+
try {
|
|
432
|
+
// Check if plugin exists
|
|
433
|
+
const pluginInfo = getPlugin(name);
|
|
434
|
+
if (!pluginInfo) {
|
|
435
|
+
return {
|
|
436
|
+
success: false,
|
|
437
|
+
error: `Plugin "${name}" is not installed`
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const pluginDir = path.join(INSTALLED_DIR, name);
|
|
442
|
+
|
|
443
|
+
if (!fs.existsSync(pluginDir)) {
|
|
444
|
+
return {
|
|
445
|
+
success: false,
|
|
446
|
+
error: `Plugin directory not found: ${pluginDir}`
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Get old version for comparison
|
|
451
|
+
const manifestPath = path.join(pluginDir, 'plugin.json');
|
|
452
|
+
let oldVersion = pluginInfo.version;
|
|
453
|
+
|
|
454
|
+
// Git pull to get latest changes
|
|
455
|
+
const pullResult = execCommand('git pull --ff-only', {
|
|
456
|
+
cwd: pluginDir,
|
|
457
|
+
timeout: 60000
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
if (!pullResult.success) {
|
|
461
|
+
// Try a fetch and reset if pull fails
|
|
462
|
+
const fetchResult = execCommand('git fetch origin && git reset --hard origin/HEAD', {
|
|
463
|
+
cwd: pluginDir,
|
|
464
|
+
timeout: 60000
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
if (!fetchResult.success) {
|
|
468
|
+
return {
|
|
469
|
+
success: false,
|
|
470
|
+
error: `Failed to update plugin: ${fetchResult.error}`
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Reinstall dependencies
|
|
476
|
+
const depResult = installDependencies(pluginDir);
|
|
477
|
+
if (!depResult.success) {
|
|
478
|
+
return {
|
|
479
|
+
success: false,
|
|
480
|
+
error: depResult.error
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Read updated manifest
|
|
485
|
+
let manifest;
|
|
486
|
+
try {
|
|
487
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
488
|
+
} catch (parseError) {
|
|
489
|
+
return {
|
|
490
|
+
success: false,
|
|
491
|
+
error: `Failed to read updated plugin.json: ${parseError.message}`
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Validate updated manifest
|
|
496
|
+
const validation = validateManifest(manifest);
|
|
497
|
+
if (!validation.valid) {
|
|
498
|
+
return {
|
|
499
|
+
success: false,
|
|
500
|
+
error: `Updated plugin.json is invalid. Please contact the plugin author.`
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Check version compatibility
|
|
505
|
+
if (manifest.minVersion) {
|
|
506
|
+
const versionCheck = checkVersionCompatibility(manifest.minVersion);
|
|
507
|
+
if (!versionCheck.compatible) {
|
|
508
|
+
return {
|
|
509
|
+
success: false,
|
|
510
|
+
error: `Updated plugin requires newer CTX version: ${versionCheck.reason}`
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Update registry with new version
|
|
516
|
+
updatePluginRegistry(name, {
|
|
517
|
+
version: manifest.version,
|
|
518
|
+
updatedAt: new Date().toISOString()
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const wasUpdated = oldVersion !== manifest.version;
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
success: true,
|
|
525
|
+
plugin: {
|
|
526
|
+
name: name,
|
|
527
|
+
oldVersion: oldVersion,
|
|
528
|
+
newVersion: manifest.version,
|
|
529
|
+
updated: wasUpdated
|
|
530
|
+
},
|
|
531
|
+
message: wasUpdated
|
|
532
|
+
? `Plugin "${name}" updated from v${oldVersion} to v${manifest.version}`
|
|
533
|
+
: `Plugin "${name}" is already at the latest version (v${manifest.version})`
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
} catch (error) {
|
|
537
|
+
return {
|
|
538
|
+
success: false,
|
|
539
|
+
error: `Failed to update plugin "${name}": ${error.message}`
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Update all installed plugins
|
|
546
|
+
* @returns {Promise<{ success: boolean, results: Array, summary: { updated: number, failed: number, unchanged: number } }>}
|
|
547
|
+
*/
|
|
548
|
+
async function updateAllPlugins() {
|
|
549
|
+
const plugins = listPlugins();
|
|
550
|
+
const results = [];
|
|
551
|
+
const summary = {
|
|
552
|
+
updated: 0,
|
|
553
|
+
failed: 0,
|
|
554
|
+
unchanged: 0
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
if (plugins.length === 0) {
|
|
558
|
+
return {
|
|
559
|
+
success: true,
|
|
560
|
+
results: [],
|
|
561
|
+
summary
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
for (const plugin of plugins) {
|
|
566
|
+
const result = await updatePlugin(plugin.name);
|
|
567
|
+
|
|
568
|
+
results.push({
|
|
569
|
+
name: plugin.name,
|
|
570
|
+
...result
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
if (result.success) {
|
|
574
|
+
if (result.plugin && result.plugin.updated) {
|
|
575
|
+
summary.updated++;
|
|
576
|
+
} else {
|
|
577
|
+
summary.unchanged++;
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
summary.failed++;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
success: summary.failed === 0,
|
|
586
|
+
results,
|
|
587
|
+
summary
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
module.exports = {
|
|
592
|
+
installPlugin,
|
|
593
|
+
uninstallPlugin,
|
|
594
|
+
updatePlugin,
|
|
595
|
+
updateAllPlugins,
|
|
596
|
+
// Export utilities for testing
|
|
597
|
+
validateGitUrl,
|
|
598
|
+
checkNameConflict,
|
|
599
|
+
checkCommandConflicts,
|
|
600
|
+
CORE_COMMANDS
|
|
601
|
+
};
|