@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adversity/coding-tool-x",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"bin/",
|
|
34
34
|
"src/commands/",
|
|
35
35
|
"src/config/",
|
|
36
|
+
"src/plugins/",
|
|
36
37
|
"src/server/",
|
|
37
38
|
"src/ui/",
|
|
38
39
|
"src/utils/",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
|
|
4
|
+
const PLUGINS_DIR = path.join(os.homedir(), '.claude', 'cc-tool', 'plugins');
|
|
5
|
+
const REGISTRY_FILE = path.join(PLUGINS_DIR, 'registry.json');
|
|
6
|
+
const CONFIG_DIR = path.join(PLUGINS_DIR, 'config');
|
|
7
|
+
const INSTALLED_DIR = path.join(PLUGINS_DIR, 'installed');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
PLUGINS_DIR,
|
|
11
|
+
REGISTRY_FILE,
|
|
12
|
+
CONFIG_DIR,
|
|
13
|
+
INSTALLED_DIR,
|
|
14
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const { EventEmitter } = require('events');
|
|
2
|
+
|
|
3
|
+
class PluginEventBus extends EventEmitter {
|
|
4
|
+
constructor() {
|
|
5
|
+
super();
|
|
6
|
+
this.setMaxListeners(50);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Emit event synchronously with error isolation
|
|
11
|
+
* All listener errors are caught and logged, not propagated
|
|
12
|
+
* @param {string} event - Event name
|
|
13
|
+
* @param {*} payload - Event payload
|
|
14
|
+
*/
|
|
15
|
+
emitSync(event, payload) {
|
|
16
|
+
const listeners = this.listeners(event);
|
|
17
|
+
|
|
18
|
+
listeners.forEach((listener) => {
|
|
19
|
+
try {
|
|
20
|
+
listener(payload);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error(
|
|
23
|
+
`[PluginEventBus] Error in listener for event "${event}":`,
|
|
24
|
+
error.message
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Emit event asynchronously with error isolation
|
|
32
|
+
* Listeners are awaited sequentially; errors are caught and logged
|
|
33
|
+
* @param {string} event - Event name
|
|
34
|
+
* @param {*} payload - Event payload
|
|
35
|
+
* @returns {Promise<void>}
|
|
36
|
+
*/
|
|
37
|
+
async emitAsync(event, payload) {
|
|
38
|
+
const listeners = this.listeners(event);
|
|
39
|
+
|
|
40
|
+
for (const listener of listeners) {
|
|
41
|
+
try {
|
|
42
|
+
await listener(payload);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(
|
|
45
|
+
`[PluginEventBus] Error in async listener for event "${event}":`,
|
|
46
|
+
error.message
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Export singleton instance
|
|
54
|
+
module.exports = new PluginEventBus();
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const Ajv = require('ajv');
|
|
2
|
+
const semver = require('semver');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validate a plugin manifest against the JSON schema
|
|
8
|
+
* @param {Object} manifest - The manifest object to validate
|
|
9
|
+
* @returns {{ valid: boolean, errors: Array<{field: string, message: string}> }}
|
|
10
|
+
*/
|
|
11
|
+
function validateManifest(manifest) {
|
|
12
|
+
try {
|
|
13
|
+
// Load the schema
|
|
14
|
+
const schemaPath = path.join(__dirname, 'schema', 'plugin-manifest.json');
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(schemaPath)) {
|
|
17
|
+
return {
|
|
18
|
+
valid: false,
|
|
19
|
+
errors: [{
|
|
20
|
+
field: 'schema',
|
|
21
|
+
message: `Schema file not found at ${schemaPath}`
|
|
22
|
+
}]
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
|
27
|
+
|
|
28
|
+
// Initialize AJV with strict mode and formats
|
|
29
|
+
const ajv = new Ajv({
|
|
30
|
+
allErrors: true,
|
|
31
|
+
strict: true,
|
|
32
|
+
validateFormats: false // Disable format validation to avoid unknown format errors
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const validate = ajv.compile(schema);
|
|
36
|
+
const valid = validate(manifest);
|
|
37
|
+
|
|
38
|
+
if (!valid) {
|
|
39
|
+
// Transform AJV errors into a more user-friendly format
|
|
40
|
+
const errors = validate.errors.map(err => ({
|
|
41
|
+
field: err.instancePath || err.params?.missingProperty || 'root',
|
|
42
|
+
message: err.message || 'Validation failed',
|
|
43
|
+
details: err
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
valid: false,
|
|
48
|
+
errors
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
valid: true,
|
|
54
|
+
errors: []
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return {
|
|
59
|
+
valid: false,
|
|
60
|
+
errors: [{
|
|
61
|
+
field: 'validator',
|
|
62
|
+
message: `Validation error: ${error.message}`
|
|
63
|
+
}]
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if the plugin's minimum version requirement is compatible with current CTX version
|
|
70
|
+
* @param {string} minVersion - Minimum version required by plugin (e.g., "2.5.0")
|
|
71
|
+
* @returns {{ compatible: boolean, reason: string }}
|
|
72
|
+
*/
|
|
73
|
+
function checkVersionCompatibility(minVersion) {
|
|
74
|
+
try {
|
|
75
|
+
// Load CTX version from package.json
|
|
76
|
+
const packagePath = path.join(__dirname, '..', '..', 'package.json');
|
|
77
|
+
|
|
78
|
+
if (!fs.existsSync(packagePath)) {
|
|
79
|
+
return {
|
|
80
|
+
compatible: false,
|
|
81
|
+
reason: 'Could not find CTX package.json to determine version'
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
86
|
+
const currentVersion = packageJson.version;
|
|
87
|
+
|
|
88
|
+
// Validate versions are valid semver
|
|
89
|
+
if (!semver.valid(minVersion)) {
|
|
90
|
+
return {
|
|
91
|
+
compatible: false,
|
|
92
|
+
reason: `Invalid minVersion format: "${minVersion}". Must be a valid semantic version (e.g., "2.5.0")`
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!semver.valid(currentVersion)) {
|
|
97
|
+
return {
|
|
98
|
+
compatible: false,
|
|
99
|
+
reason: `Invalid CTX version format: "${currentVersion}"`
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check if current version satisfies minimum requirement
|
|
104
|
+
const compatible = semver.gte(currentVersion, minVersion);
|
|
105
|
+
|
|
106
|
+
if (!compatible) {
|
|
107
|
+
return {
|
|
108
|
+
compatible: false,
|
|
109
|
+
reason: `Plugin requires CTX >= ${minVersion}, but current version is ${currentVersion}`
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
compatible: true,
|
|
115
|
+
reason: `CTX version ${currentVersion} meets minimum requirement ${minVersion}`
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return {
|
|
120
|
+
compatible: false,
|
|
121
|
+
reason: `Version compatibility check failed: ${error.message}`
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
validateManifest,
|
|
128
|
+
checkVersionCompatibility
|
|
129
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { CONFIG_DIR } = require('./constants');
|
|
4
|
+
const eventBus = require('./event-bus');
|
|
5
|
+
const { loadConfig } = require('../config/loader');
|
|
6
|
+
const packageJson = require('../../package.json');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create sandboxed plugin context with isolated API surface
|
|
10
|
+
* @param {string} pluginName - Name of the plugin
|
|
11
|
+
* @param {Object} pluginConfig - Plugin-specific configuration
|
|
12
|
+
* @param {string} pluginDir - Plugin installation directory
|
|
13
|
+
* @returns {Object} Plugin context object
|
|
14
|
+
*/
|
|
15
|
+
function createPluginContext(pluginName, pluginConfig, pluginDir) {
|
|
16
|
+
const commandRegistry = new Map();
|
|
17
|
+
|
|
18
|
+
// Storage API - persists plugin data to ~/.claude/cc-tool/plugins/config/<plugin-name>.json
|
|
19
|
+
const storage = createStorageAPI(pluginName);
|
|
20
|
+
|
|
21
|
+
// Logger API - prefixed logging
|
|
22
|
+
const logger = {
|
|
23
|
+
info: (msg) => console.log(`[${pluginName}] ${msg}`),
|
|
24
|
+
warn: (msg) => console.warn(`[${pluginName}] ${msg}`),
|
|
25
|
+
error: (msg) => console.error(`[${pluginName}] ${msg}`),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Plugin context object
|
|
29
|
+
const ctx = {
|
|
30
|
+
// Event bus for cross-plugin communication
|
|
31
|
+
events: eventBus,
|
|
32
|
+
|
|
33
|
+
// Frozen plugin configuration
|
|
34
|
+
config: Object.freeze({ ...pluginConfig }),
|
|
35
|
+
|
|
36
|
+
// Prefixed logger
|
|
37
|
+
logger,
|
|
38
|
+
|
|
39
|
+
// Command registration (collected by plugin-manager)
|
|
40
|
+
registerCommand: (name, handler) => {
|
|
41
|
+
if (!name || typeof name !== 'string') {
|
|
42
|
+
throw new Error('Command name must be a non-empty string');
|
|
43
|
+
}
|
|
44
|
+
if (typeof handler !== 'function') {
|
|
45
|
+
throw new Error('Command handler must be a function');
|
|
46
|
+
}
|
|
47
|
+
commandRegistry.set(name, handler);
|
|
48
|
+
logger.info(`Registered command: ${name}`);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Get frozen copy of app configuration
|
|
52
|
+
getAppConfig: () => {
|
|
53
|
+
const config = loadConfig();
|
|
54
|
+
return Object.freeze({ ...config });
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Get CTX version
|
|
58
|
+
getVersion: () => packageJson.version,
|
|
59
|
+
|
|
60
|
+
// Persistent key-value storage
|
|
61
|
+
storage,
|
|
62
|
+
|
|
63
|
+
// Internal: expose command registry for plugin-manager
|
|
64
|
+
_getCommands: () => commandRegistry,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return ctx;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create storage API for plugin data persistence
|
|
72
|
+
* @param {string} pluginName - Plugin name
|
|
73
|
+
* @returns {Object} Storage API
|
|
74
|
+
*/
|
|
75
|
+
function createStorageAPI(pluginName) {
|
|
76
|
+
const storageFile = path.join(CONFIG_DIR, `${pluginName}.json`);
|
|
77
|
+
|
|
78
|
+
// Ensure config directory exists
|
|
79
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
80
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Load storage data
|
|
84
|
+
function loadData() {
|
|
85
|
+
try {
|
|
86
|
+
if (fs.existsSync(storageFile)) {
|
|
87
|
+
const content = fs.readFileSync(storageFile, 'utf8');
|
|
88
|
+
return JSON.parse(content);
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error(`[${pluginName}] Failed to load storage:`, error.message);
|
|
92
|
+
}
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Save storage data
|
|
97
|
+
function saveData(data) {
|
|
98
|
+
try {
|
|
99
|
+
fs.writeFileSync(storageFile, JSON.stringify(data, null, 2));
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error(`[${pluginName}] Failed to save storage:`, error.message);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
get: (key) => {
|
|
108
|
+
const data = loadData();
|
|
109
|
+
return data[key];
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
set: (key, value) => {
|
|
113
|
+
const data = loadData();
|
|
114
|
+
data[key] = value;
|
|
115
|
+
saveData(data);
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
delete: (key) => {
|
|
119
|
+
const data = loadData();
|
|
120
|
+
delete data[key];
|
|
121
|
+
saveData(data);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
createPluginContext,
|
|
128
|
+
};
|