@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adversity/coding-tool-x",
3
- "version": "2.6.1",
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
+ };