coding-tool-x 3.2.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/CHANGELOG.md +599 -0
- package/LICENSE +21 -0
- package/README.md +439 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
- package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
- package/dist/web/assets/Home-38JTUlYt.js +1 -0
- package/dist/web/assets/Home-CjupSEWE.css +1 -0
- package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
- package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
- package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
- package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
- package/dist/web/assets/icons-DRrXwWZi.js +1 -0
- package/dist/web/assets/index-CetESrXw.css +1 -0
- package/dist/web/assets/index-Cfvn-2Gb.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-DlpKk-8M.js +1 -0
- package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
- package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +20 -0
- package/dist/web/logo.png +0 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/model-redirection.md +251 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +80 -0
- package/src/commands/channels.js +551 -0
- package/src/commands/cli-type.js +101 -0
- package/src/commands/daemon.js +365 -0
- package/src/commands/doctor.js +333 -0
- package/src/commands/export-config.js +205 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +261 -0
- package/src/commands/plugin.js +585 -0
- package/src/commands/port-config.js +135 -0
- package/src/commands/proxy-control.js +264 -0
- package/src/commands/proxy.js +152 -0
- package/src/commands/resume.js +137 -0
- package/src/commands/search.js +190 -0
- package/src/commands/security.js +37 -0
- package/src/commands/stats.js +398 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +247 -0
- package/src/commands/ui.js +99 -0
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +69 -0
- package/src/config/loader.js +149 -0
- package/src/config/model-metadata.js +167 -0
- package/src/config/model-metadata.json +125 -0
- package/src/config/model-pricing.js +35 -0
- package/src/config/paths.js +190 -0
- package/src/index.js +680 -0
- package/src/plugins/constants.js +15 -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/src/reset-config.js +94 -0
- package/src/server/api/agents.js +826 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +368 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +417 -0
- package/src/server/api/codex-projects.js +104 -0
- package/src/server/api/codex-proxy.js +195 -0
- package/src/server/api/codex-sessions.js +483 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +482 -0
- package/src/server/api/config-export.js +212 -0
- package/src/server/api/config-registry.js +357 -0
- package/src/server/api/config-sync.js +155 -0
- package/src/server/api/config-templates.js +248 -0
- package/src/server/api/config.js +521 -0
- package/src/server/api/convert.js +260 -0
- package/src/server/api/dashboard.js +142 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +366 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +173 -0
- package/src/server/api/gemini-sessions.js +376 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +31 -0
- package/src/server/api/mcp.js +399 -0
- package/src/server/api/opencode-channels.js +419 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +207 -0
- package/src/server/api/opencode-sessions.js +327 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +463 -0
- package/src/server/api/pm2-autostart.js +269 -0
- package/src/server/api/projects.js +124 -0
- package/src/server/api/prompts.js +279 -0
- package/src/server/api/proxy.js +306 -0
- package/src/server/api/security.js +53 -0
- package/src/server/api/sessions.js +514 -0
- package/src/server/api/settings.js +142 -0
- package/src/server/api/skills.js +570 -0
- package/src/server/api/statistics.js +238 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +456 -0
- package/src/server/codex-proxy-server.js +681 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +610 -0
- package/src/server/index.js +422 -0
- package/src/server/opencode-proxy-server.js +4771 -0
- package/src/server/proxy-server.js +669 -0
- package/src/server/services/agents-service.js +1137 -0
- package/src/server/services/alias.js +71 -0
- package/src/server/services/channel-health.js +234 -0
- package/src/server/services/channel-scheduler.js +240 -0
- package/src/server/services/channels.js +447 -0
- package/src/server/services/codex-channels.js +705 -0
- package/src/server/services/codex-config.js +90 -0
- package/src/server/services/codex-parser.js +322 -0
- package/src/server/services/codex-sessions.js +936 -0
- package/src/server/services/codex-settings-manager.js +619 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +161 -0
- package/src/server/services/commands-service.js +574 -0
- package/src/server/services/config-export-service.js +1165 -0
- package/src/server/services/config-registry-service.js +828 -0
- package/src/server/services/config-sync-manager.js +941 -0
- package/src/server/services/config-sync-service.js +504 -0
- package/src/server/services/config-templates-service.js +913 -0
- package/src/server/services/enhanced-cache.js +196 -0
- package/src/server/services/env-checker.js +409 -0
- package/src/server/services/env-manager.js +436 -0
- package/src/server/services/favorites.js +165 -0
- package/src/server/services/format-converter.js +620 -0
- package/src/server/services/gemini-channels.js +459 -0
- package/src/server/services/gemini-config.js +73 -0
- package/src/server/services/gemini-sessions.js +689 -0
- package/src/server/services/gemini-settings-manager.js +263 -0
- package/src/server/services/gemini-statistics-service.js +157 -0
- package/src/server/services/health-check.js +85 -0
- package/src/server/services/mcp-client.js +790 -0
- package/src/server/services/mcp-service.js +1732 -0
- package/src/server/services/model-detector.js +1245 -0
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +366 -0
- package/src/server/services/opencode-gateway-adapters.js +1168 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +931 -0
- package/src/server/services/opencode-settings-manager.js +478 -0
- package/src/server/services/opencode-statistics-service.js +161 -0
- package/src/server/services/plugins-service.js +1268 -0
- package/src/server/services/prompts-service.js +534 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/repo-scanner-base.js +708 -0
- package/src/server/services/request-logger.js +130 -0
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/security-config.js +131 -0
- package/src/server/services/session-cache.js +127 -0
- package/src/server/services/session-converter.js +577 -0
- package/src/server/services/sessions.js +900 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +1482 -0
- package/src/server/services/speed-test.js +1146 -0
- package/src/server/services/statistics-service.js +1043 -0
- package/src/server/services/ui-config.js +132 -0
- package/src/server/services/workspace-service.js +830 -0
- package/src/server/utils/pricing.js +73 -0
- package/src/server/websocket-server.js +513 -0
- package/src/ui/menu.js +139 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +108 -0
- package/src/utils/session.js +240 -0
|
@@ -0,0 +1,1268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugins Service
|
|
3
|
+
*
|
|
4
|
+
* Wraps the plugin system for API access
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const { listPlugins, getPlugin, updatePlugin: updatePluginRegistry } = require('../../plugins/registry');
|
|
11
|
+
const { installPlugin: installPluginCore, uninstallPlugin: uninstallPluginCore } = require('../../plugins/plugin-installer');
|
|
12
|
+
const { initializePlugins, shutdownPlugins } = require('../../plugins/plugin-manager');
|
|
13
|
+
const { INSTALLED_DIR, CONFIG_DIR } = require('../../plugins/constants');
|
|
14
|
+
const { NATIVE_PATHS } = require('../../config/paths');
|
|
15
|
+
|
|
16
|
+
const CLAUDE_PLUGINS_DIR = path.join(os.homedir(), '.claude', 'plugins');
|
|
17
|
+
const CLAUDE_INSTALLED_FILE = path.join(CLAUDE_PLUGINS_DIR, 'installed_plugins.json');
|
|
18
|
+
const CLAUDE_MARKETPLACES_FILE = path.join(CLAUDE_PLUGINS_DIR, 'known_marketplaces.json');
|
|
19
|
+
const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
|
|
20
|
+
const DEFAULT_REPOS_BY_PLATFORM = {
|
|
21
|
+
claude: [],
|
|
22
|
+
opencode: [
|
|
23
|
+
{
|
|
24
|
+
owner: 'Tommertom',
|
|
25
|
+
name: 'opencode-plugin-marketplace',
|
|
26
|
+
url: 'https://github.com/Tommertom/opencode-plugin-marketplace',
|
|
27
|
+
branch: 'main',
|
|
28
|
+
enabled: true,
|
|
29
|
+
source: 'opencode-default'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
owner: 'avifenesh',
|
|
33
|
+
name: 'awesome-slash',
|
|
34
|
+
url: 'https://github.com/avifenesh/awesome-slash',
|
|
35
|
+
branch: 'main',
|
|
36
|
+
enabled: true,
|
|
37
|
+
source: 'opencode-default'
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
owner: 'NeoLabHQ',
|
|
41
|
+
name: 'context-engineering-kit',
|
|
42
|
+
url: 'https://github.com/NeoLabHQ/context-engineering-kit',
|
|
43
|
+
branch: 'master',
|
|
44
|
+
enabled: true,
|
|
45
|
+
source: 'opencode-default'
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function cloneRepos(repos = []) {
|
|
51
|
+
return repos.map(repo => ({ ...repo }));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stripJsonComments(input = '') {
|
|
55
|
+
let result = '';
|
|
56
|
+
let inString = false;
|
|
57
|
+
let stringChar = '';
|
|
58
|
+
let i = 0;
|
|
59
|
+
|
|
60
|
+
while (i < input.length) {
|
|
61
|
+
const ch = input[i];
|
|
62
|
+
const next = input[i + 1];
|
|
63
|
+
|
|
64
|
+
if (inString) {
|
|
65
|
+
result += ch;
|
|
66
|
+
if (ch === '\\') {
|
|
67
|
+
if (next) {
|
|
68
|
+
result += next;
|
|
69
|
+
i += 2;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
} else if (ch === stringChar) {
|
|
73
|
+
inString = false;
|
|
74
|
+
}
|
|
75
|
+
i += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (ch === '"' || ch === '\'') {
|
|
80
|
+
inString = true;
|
|
81
|
+
stringChar = ch;
|
|
82
|
+
result += ch;
|
|
83
|
+
i += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (ch === '/' && next === '/') {
|
|
88
|
+
i += 2;
|
|
89
|
+
while (i < input.length && input[i] !== '\n') i += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (ch === '/' && next === '*') {
|
|
94
|
+
i += 2;
|
|
95
|
+
while (i < input.length - 1 && !(input[i] === '*' && input[i + 1] === '/')) i += 1;
|
|
96
|
+
i += 2;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
result += ch;
|
|
101
|
+
i += 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
class PluginsService {
|
|
108
|
+
constructor(platform = 'claude') {
|
|
109
|
+
this.platform = ['claude', 'opencode'].includes(platform) ? platform : 'claude';
|
|
110
|
+
this.ccToolConfigDir = path.join(os.homedir(), '.cc-tool');
|
|
111
|
+
this.opencodePluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugins');
|
|
112
|
+
this.opencodeLegacyPluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugin');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_ensureDir(dirPath) {
|
|
116
|
+
if (!fs.existsSync(dirPath)) {
|
|
117
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_isOpenCode() {
|
|
122
|
+
return this.platform === 'opencode';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_getOpenCodePluginsDir() {
|
|
126
|
+
if (fs.existsSync(this.opencodeLegacyPluginsDir) && !fs.existsSync(this.opencodePluginsDir)) {
|
|
127
|
+
return this.opencodeLegacyPluginsDir;
|
|
128
|
+
}
|
|
129
|
+
return this.opencodePluginsDir;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_getOpenCodeConfigPath() {
|
|
133
|
+
const jsonc = path.join(OPENCODE_CONFIG_DIR, 'opencode.jsonc');
|
|
134
|
+
const json = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
|
|
135
|
+
const config = path.join(OPENCODE_CONFIG_DIR, 'config.json');
|
|
136
|
+
if (fs.existsSync(jsonc)) return jsonc;
|
|
137
|
+
if (fs.existsSync(json)) return json;
|
|
138
|
+
if (fs.existsSync(config)) return config;
|
|
139
|
+
return json;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_readOpenCodeConfig() {
|
|
143
|
+
const filePath = this._getOpenCodeConfigPath();
|
|
144
|
+
if (!fs.existsSync(filePath)) return { filePath, config: {} };
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
148
|
+
if (!raw.trim()) return { filePath, config: {} };
|
|
149
|
+
if (filePath.endsWith('.jsonc')) {
|
|
150
|
+
return { filePath, config: JSON.parse(stripJsonComments(raw)) };
|
|
151
|
+
}
|
|
152
|
+
return { filePath, config: JSON.parse(raw) };
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error('[PluginsService] Failed to read opencode config:', err.message);
|
|
155
|
+
return { filePath, config: {} };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
_writeOpenCodeConfig(filePath, config) {
|
|
160
|
+
this._ensureDir(path.dirname(filePath));
|
|
161
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf8');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_listOpenCodeConfiguredPlugins() {
|
|
165
|
+
const { config } = this._readOpenCodeConfig();
|
|
166
|
+
if (!Array.isArray(config.plugin)) return [];
|
|
167
|
+
return config.plugin.filter(Boolean);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_setOpenCodeConfiguredPlugins(plugins) {
|
|
171
|
+
const { filePath, config } = this._readOpenCodeConfig();
|
|
172
|
+
const nextConfig = (config && typeof config === 'object') ? { ...config } : {};
|
|
173
|
+
nextConfig.plugin = Array.from(new Set((plugins || []).filter(Boolean)));
|
|
174
|
+
this._writeOpenCodeConfig(filePath, nextConfig);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_listOpenCodeLocalPlugins() {
|
|
178
|
+
const pluginsDir = this._getOpenCodePluginsDir();
|
|
179
|
+
if (!fs.existsSync(pluginsDir)) return [];
|
|
180
|
+
|
|
181
|
+
const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
|
|
182
|
+
const plugins = [];
|
|
183
|
+
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (entry.name.startsWith('.')) continue;
|
|
186
|
+
const fullPath = path.join(pluginsDir, entry.name);
|
|
187
|
+
|
|
188
|
+
if (entry.isDirectory()) {
|
|
189
|
+
const pkgPath = path.join(fullPath, 'package.json');
|
|
190
|
+
let packageName = entry.name;
|
|
191
|
+
let description = '';
|
|
192
|
+
let version = '1.0.0';
|
|
193
|
+
if (fs.existsSync(pkgPath)) {
|
|
194
|
+
try {
|
|
195
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
196
|
+
packageName = pkg.name || packageName;
|
|
197
|
+
description = pkg.description || '';
|
|
198
|
+
version = pkg.version || version;
|
|
199
|
+
} catch (err) {
|
|
200
|
+
// ignore invalid package.json
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
plugins.push({
|
|
204
|
+
name: packageName,
|
|
205
|
+
directory: entry.name,
|
|
206
|
+
installPath: fullPath,
|
|
207
|
+
source: 'opencode-local',
|
|
208
|
+
version,
|
|
209
|
+
description,
|
|
210
|
+
installed: true,
|
|
211
|
+
enabled: true,
|
|
212
|
+
pluginType: 'local'
|
|
213
|
+
});
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
218
|
+
if (['.js', '.mjs', '.cjs', '.ts'].includes(ext)) {
|
|
219
|
+
plugins.push({
|
|
220
|
+
name: entry.name.replace(ext, ''),
|
|
221
|
+
directory: entry.name,
|
|
222
|
+
installPath: fullPath,
|
|
223
|
+
source: 'opencode-local',
|
|
224
|
+
version: '1.0.0',
|
|
225
|
+
description: '',
|
|
226
|
+
installed: true,
|
|
227
|
+
enabled: true,
|
|
228
|
+
pluginType: 'local'
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return plugins;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* List all installed plugins with their status
|
|
238
|
+
* Reads from Claude Code's native installed_plugins.json
|
|
239
|
+
* @returns {Object} { plugins: Array }
|
|
240
|
+
*/
|
|
241
|
+
listPlugins() {
|
|
242
|
+
if (this._isOpenCode()) {
|
|
243
|
+
const plugins = [];
|
|
244
|
+
const seen = new Set();
|
|
245
|
+
|
|
246
|
+
for (const pkg of this._listOpenCodeConfiguredPlugins()) {
|
|
247
|
+
if (seen.has(pkg)) continue;
|
|
248
|
+
seen.add(pkg);
|
|
249
|
+
plugins.push({
|
|
250
|
+
name: pkg,
|
|
251
|
+
directory: pkg,
|
|
252
|
+
source: 'opencode-config',
|
|
253
|
+
version: 'latest',
|
|
254
|
+
description: '',
|
|
255
|
+
installed: true,
|
|
256
|
+
enabled: true,
|
|
257
|
+
pluginType: 'npm'
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const plugin of this._listOpenCodeLocalPlugins()) {
|
|
262
|
+
if (!seen.has(plugin.name)) {
|
|
263
|
+
seen.add(plugin.name);
|
|
264
|
+
plugins.push(plugin);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { plugins };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const plugins = [];
|
|
272
|
+
|
|
273
|
+
// Read Claude Code's installed_plugins.json
|
|
274
|
+
if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
|
|
275
|
+
try {
|
|
276
|
+
const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
|
|
277
|
+
if (data.plugins) {
|
|
278
|
+
for (const [key, installations] of Object.entries(data.plugins)) {
|
|
279
|
+
if (installations && installations.length > 0) {
|
|
280
|
+
const install = installations[0]; // Get first installation
|
|
281
|
+
const [name, marketplace] = key.split('@');
|
|
282
|
+
|
|
283
|
+
// Read plugin.json from installPath for description
|
|
284
|
+
let description = '';
|
|
285
|
+
let source = install.source || '';
|
|
286
|
+
let repoUrl = '';
|
|
287
|
+
|
|
288
|
+
if (install.installPath && fs.existsSync(install.installPath)) {
|
|
289
|
+
const manifestPath = path.join(install.installPath, 'plugin.json');
|
|
290
|
+
if (fs.existsSync(manifestPath)) {
|
|
291
|
+
try {
|
|
292
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
293
|
+
description = manifest.description || '';
|
|
294
|
+
} catch (err) {
|
|
295
|
+
// Ignore parse errors
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Parse repoUrl from source if available
|
|
301
|
+
if (source) {
|
|
302
|
+
const match = source.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
|
303
|
+
if (match) {
|
|
304
|
+
repoUrl = `https://github.com/${match[1]}/${match[2]}`;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Read enabled state from CTX registry (defaults to true if not set)
|
|
309
|
+
const legacyInfo = getPlugin(name);
|
|
310
|
+
const enabledState = legacyInfo ? legacyInfo.enabled !== false : true;
|
|
311
|
+
|
|
312
|
+
plugins.push({
|
|
313
|
+
name,
|
|
314
|
+
marketplace,
|
|
315
|
+
version: install.version || '1.0.0',
|
|
316
|
+
installPath: install.installPath,
|
|
317
|
+
installedAt: install.installedAt,
|
|
318
|
+
scope: install.scope,
|
|
319
|
+
enabled: enabledState,
|
|
320
|
+
description,
|
|
321
|
+
source,
|
|
322
|
+
repoUrl
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
} catch (err) {
|
|
328
|
+
console.error('[PluginsService] Failed to read installed_plugins.json:', err.message);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Also check legacy registry
|
|
333
|
+
try {
|
|
334
|
+
const legacyPlugins = listPlugins();
|
|
335
|
+
for (const plugin of legacyPlugins) {
|
|
336
|
+
if (!plugins.find(p => p.name === plugin.name)) {
|
|
337
|
+
plugins.push(plugin);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
} catch (err) {
|
|
341
|
+
// Ignore legacy registry errors
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { plugins };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Get single plugin details
|
|
349
|
+
* @param {string} name - Plugin name
|
|
350
|
+
* @returns {Object|null} Plugin details or null
|
|
351
|
+
*/
|
|
352
|
+
getPlugin(name) {
|
|
353
|
+
if (this._isOpenCode()) {
|
|
354
|
+
const plugin = this.listPlugins().plugins.find(p => p.name === name || p.directory === name);
|
|
355
|
+
if (!plugin) return null;
|
|
356
|
+
return plugin;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const plugin = getPlugin(name);
|
|
360
|
+
if (!plugin) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const pluginDir = path.join(INSTALLED_DIR, name);
|
|
365
|
+
const manifestPath = path.join(pluginDir, 'plugin.json');
|
|
366
|
+
|
|
367
|
+
let manifest = null;
|
|
368
|
+
if (fs.existsSync(manifestPath)) {
|
|
369
|
+
try {
|
|
370
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
371
|
+
} catch (err) {
|
|
372
|
+
// Ignore parse errors
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
name,
|
|
378
|
+
...plugin,
|
|
379
|
+
description: manifest?.description || '',
|
|
380
|
+
author: manifest?.author || '',
|
|
381
|
+
commands: manifest?.commands || [],
|
|
382
|
+
hooks: manifest?.hooks || [],
|
|
383
|
+
manifest
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Install plugin from Git URL or repo directory
|
|
389
|
+
* @param {string} source - Git repository URL or tree URL
|
|
390
|
+
* @param {Object} repoInfo - Optional repo info { owner, name, branch, directory }
|
|
391
|
+
* @returns {Promise<Object>} Installation result
|
|
392
|
+
*/
|
|
393
|
+
async installPlugin(source, repoInfo = null) {
|
|
394
|
+
if (this._isOpenCode()) {
|
|
395
|
+
if (repoInfo && repoInfo.owner && repoInfo.name && repoInfo.directory) {
|
|
396
|
+
return this._installFromGitHubDirectory(repoInfo, { installRoot: this._getOpenCodePluginsDir() });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const treeMatch = source.match(/github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/);
|
|
400
|
+
if (treeMatch) {
|
|
401
|
+
const [, owner, name, branch, directory] = treeMatch;
|
|
402
|
+
return this._installFromGitHubDirectory({ owner, name, branch, directory }, { installRoot: this._getOpenCodePluginsDir() });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// OpenCode 原生支持 npm 包名,通过 opencode.json 的 plugin 数组管理
|
|
406
|
+
if (!/^https?:\/\//.test(source)) {
|
|
407
|
+
const plugins = this._listOpenCodeConfiguredPlugins();
|
|
408
|
+
if (!plugins.includes(source)) {
|
|
409
|
+
plugins.push(source);
|
|
410
|
+
this._setOpenCodeConfiguredPlugins(plugins);
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
success: true,
|
|
414
|
+
plugin: { name: source, version: 'latest', description: '' }
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
success: false,
|
|
420
|
+
error: 'OpenCode plugin install expects npm package name or GitHub tree URL'
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// If repoInfo is provided, download from GitHub directly
|
|
425
|
+
if (repoInfo && repoInfo.owner && repoInfo.name && repoInfo.directory) {
|
|
426
|
+
return await this._installFromGitHubDirectory(repoInfo);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Parse tree URL format: https://github.com/owner/repo/tree/branch/path
|
|
430
|
+
const treeMatch = source.match(/github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/);
|
|
431
|
+
if (treeMatch) {
|
|
432
|
+
const [, owner, name, branch, directory] = treeMatch;
|
|
433
|
+
return await this._installFromGitHubDirectory({ owner, name, branch, directory });
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Fallback to original git clone method
|
|
437
|
+
return await installPluginCore(source);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Install plugin from GitHub directory
|
|
442
|
+
* @private
|
|
443
|
+
*/
|
|
444
|
+
async _installFromGitHubDirectory(repoInfo, options = {}) {
|
|
445
|
+
const { owner, name, branch, directory } = repoInfo;
|
|
446
|
+
const https = require('https');
|
|
447
|
+
const pluginName = directory.split('/').pop();
|
|
448
|
+
const installRoot = options.installRoot || INSTALLED_DIR;
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
// Fetch plugin.json from the directory
|
|
452
|
+
const manifestUrl = `https://raw.githubusercontent.com/${owner}/${name}/${branch}/${directory}/plugin.json`;
|
|
453
|
+
let manifest;
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
manifest = await this._fetchJson(manifestUrl);
|
|
457
|
+
} catch (e) {
|
|
458
|
+
// No plugin.json, create a basic manifest
|
|
459
|
+
manifest = { name: pluginName, version: '1.0.0' };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Create plugin directory
|
|
463
|
+
const pluginDir = path.join(installRoot, manifest.name || pluginName);
|
|
464
|
+
if (!fs.existsSync(pluginDir)) {
|
|
465
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Download all files from the directory
|
|
469
|
+
const contentsUrl = `https://api.github.com/repos/${owner}/${name}/contents/${directory}?ref=${branch}`;
|
|
470
|
+
const contents = await this._fetchJson(contentsUrl);
|
|
471
|
+
|
|
472
|
+
for (const item of contents) {
|
|
473
|
+
if (item.type === 'file') {
|
|
474
|
+
const fileContent = await this._fetchRawFile(item.download_url);
|
|
475
|
+
fs.writeFileSync(path.join(pluginDir, item.name), fileContent);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Write plugin.json if not exists
|
|
480
|
+
const manifestPath = path.join(pluginDir, 'plugin.json');
|
|
481
|
+
if (!fs.existsSync(manifestPath)) {
|
|
482
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (!this._isOpenCode()) {
|
|
486
|
+
const installedPluginName = manifest.name || pluginName;
|
|
487
|
+
const installTimestamp = new Date().toISOString();
|
|
488
|
+
const sourceUrl = `https://github.com/${owner}/${name}/tree/${branch}/${directory}`;
|
|
489
|
+
|
|
490
|
+
// Register in CTX legacy registry (for listPlugins fallback)
|
|
491
|
+
const { addPlugin } = require('../../plugins/registry');
|
|
492
|
+
try {
|
|
493
|
+
addPlugin(installedPluginName, {
|
|
494
|
+
version: manifest.version || '1.0.0',
|
|
495
|
+
enabled: true,
|
|
496
|
+
installedAt: installTimestamp,
|
|
497
|
+
source: sourceUrl
|
|
498
|
+
});
|
|
499
|
+
} catch (e) {
|
|
500
|
+
console.warn('[PluginsService] Legacy registry addPlugin warning:', e.message);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Also register in Claude's native installed_plugins.json
|
|
504
|
+
try {
|
|
505
|
+
this._ensureDir(CLAUDE_PLUGINS_DIR);
|
|
506
|
+
let nativeData = { plugins: {} };
|
|
507
|
+
if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
|
|
508
|
+
try {
|
|
509
|
+
nativeData = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
|
|
510
|
+
if (!nativeData.plugins) nativeData.plugins = {};
|
|
511
|
+
} catch (e) { /* ignore parse error */ }
|
|
512
|
+
}
|
|
513
|
+
const nativeKey = `${installedPluginName}@ctx`;
|
|
514
|
+
nativeData.plugins[nativeKey] = [{
|
|
515
|
+
version: manifest.version || '1.0.0',
|
|
516
|
+
installPath: pluginDir,
|
|
517
|
+
installedAt: installTimestamp,
|
|
518
|
+
scope: 'user',
|
|
519
|
+
source: sourceUrl
|
|
520
|
+
}];
|
|
521
|
+
fs.writeFileSync(CLAUDE_INSTALLED_FILE, JSON.stringify(nativeData, null, 2), 'utf8');
|
|
522
|
+
} catch (e) {
|
|
523
|
+
console.error('[PluginsService] Failed to update native installed_plugins.json:', e.message);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
success: true,
|
|
529
|
+
plugin: {
|
|
530
|
+
name: manifest.name || pluginName,
|
|
531
|
+
version: manifest.version || '1.0.0',
|
|
532
|
+
description: manifest.description || ''
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
} catch (err) {
|
|
536
|
+
return {
|
|
537
|
+
success: false,
|
|
538
|
+
error: `Failed to install plugin: ${err.message}`
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Fetch raw file content
|
|
545
|
+
* @private
|
|
546
|
+
*/
|
|
547
|
+
async _fetchRawFile(url) {
|
|
548
|
+
const https = require('https');
|
|
549
|
+
return new Promise((resolve, reject) => {
|
|
550
|
+
https.get(url, {
|
|
551
|
+
headers: { 'User-Agent': 'coding-tool-x' }
|
|
552
|
+
}, (res) => {
|
|
553
|
+
let data = '';
|
|
554
|
+
res.on('data', chunk => data += chunk);
|
|
555
|
+
res.on('end', () => {
|
|
556
|
+
if (res.statusCode === 200) {
|
|
557
|
+
resolve(data);
|
|
558
|
+
} else {
|
|
559
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
}).on('error', reject);
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Uninstall plugin
|
|
568
|
+
* @param {string} name - Plugin name
|
|
569
|
+
* @returns {Object} Uninstallation result
|
|
570
|
+
*/
|
|
571
|
+
uninstallPlugin(name) {
|
|
572
|
+
if (this._isOpenCode()) {
|
|
573
|
+
const pluginsDir = this._getOpenCodePluginsDir();
|
|
574
|
+
let removed = false;
|
|
575
|
+
|
|
576
|
+
// Remove from opencode config.plugin (npm plugins)
|
|
577
|
+
const configured = this._listOpenCodeConfiguredPlugins();
|
|
578
|
+
const next = configured.filter(p => p !== name);
|
|
579
|
+
if (next.length !== configured.length) {
|
|
580
|
+
this._setOpenCodeConfiguredPlugins(next);
|
|
581
|
+
removed = true;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Remove local plugin directory/file
|
|
585
|
+
if (fs.existsSync(pluginsDir)) {
|
|
586
|
+
const directPath = path.join(pluginsDir, name);
|
|
587
|
+
if (fs.existsSync(directPath)) {
|
|
588
|
+
fs.rmSync(directPath, { recursive: true, force: true });
|
|
589
|
+
removed = true;
|
|
590
|
+
} else {
|
|
591
|
+
const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
|
|
592
|
+
for (const entry of entries) {
|
|
593
|
+
const baseName = entry.name.replace(path.extname(entry.name), '');
|
|
594
|
+
if (entry.name === name || baseName === name) {
|
|
595
|
+
fs.rmSync(path.join(pluginsDir, entry.name), { recursive: true, force: true });
|
|
596
|
+
removed = true;
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
success: true,
|
|
605
|
+
message: removed ? 'Plugin removed successfully' : 'Plugin not found'
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Claude: Remove from native installed_plugins.json and delete install directories
|
|
610
|
+
let removed = false;
|
|
611
|
+
if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
|
|
612
|
+
try {
|
|
613
|
+
const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
|
|
614
|
+
if (data.plugins) {
|
|
615
|
+
const keysToDelete = [];
|
|
616
|
+
const baseName = name.split('/').pop(); // handle "plugins/pr-review-toolkit" → "pr-review-toolkit"
|
|
617
|
+
for (const [key, installations] of Object.entries(data.plugins)) {
|
|
618
|
+
const [pluginName] = key.split('@');
|
|
619
|
+
if (pluginName === name || key === name || pluginName === baseName) {
|
|
620
|
+
keysToDelete.push(key);
|
|
621
|
+
// Delete install directories
|
|
622
|
+
if (Array.isArray(installations)) {
|
|
623
|
+
for (const install of installations) {
|
|
624
|
+
if (install.installPath && fs.existsSync(install.installPath)) {
|
|
625
|
+
try {
|
|
626
|
+
fs.rmSync(install.installPath, { recursive: true, force: true });
|
|
627
|
+
} catch (e) {
|
|
628
|
+
console.error('[PluginsService] Failed to delete install dir:', e.message);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (keysToDelete.length > 0) {
|
|
636
|
+
for (const key of keysToDelete) {
|
|
637
|
+
delete data.plugins[key];
|
|
638
|
+
}
|
|
639
|
+
fs.writeFileSync(CLAUDE_INSTALLED_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
640
|
+
removed = true;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
} catch (err) {
|
|
644
|
+
console.error('[PluginsService] Failed to update installed_plugins.json:', err.message);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Also try legacy registry removal
|
|
649
|
+
try {
|
|
650
|
+
const legacyResult = uninstallPluginCore(name);
|
|
651
|
+
if (legacyResult.success) removed = true;
|
|
652
|
+
} catch (err) {
|
|
653
|
+
// Ignore legacy registry errors
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (!removed) {
|
|
657
|
+
return {
|
|
658
|
+
success: false,
|
|
659
|
+
error: `Plugin "${name}" not found`
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
success: true,
|
|
665
|
+
message: 'Plugin uninstalled successfully'
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Toggle plugin enabled/disabled
|
|
671
|
+
* @param {string} name - Plugin name
|
|
672
|
+
* @param {boolean} enabled - Enable or disable
|
|
673
|
+
* @returns {Object} Updated plugin info
|
|
674
|
+
*/
|
|
675
|
+
togglePlugin(name, enabled) {
|
|
676
|
+
if (this._isOpenCode()) {
|
|
677
|
+
const configured = this._listOpenCodeConfiguredPlugins();
|
|
678
|
+
const exists = configured.includes(name);
|
|
679
|
+
if (enabled && !exists) {
|
|
680
|
+
configured.push(name);
|
|
681
|
+
this._setOpenCodeConfiguredPlugins(configured);
|
|
682
|
+
} else if (!enabled && exists) {
|
|
683
|
+
this._setOpenCodeConfiguredPlugins(configured.filter(p => p !== name));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return {
|
|
687
|
+
name,
|
|
688
|
+
enabled
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Claude: store enabled state in CTX registry
|
|
693
|
+
// First check if plugin exists in native installed_plugins.json
|
|
694
|
+
let pluginExists = false;
|
|
695
|
+
const baseName = name.split('/').pop();
|
|
696
|
+
if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
|
|
697
|
+
try {
|
|
698
|
+
const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
|
|
699
|
+
if (data.plugins) {
|
|
700
|
+
for (const key of Object.keys(data.plugins)) {
|
|
701
|
+
const [pluginName] = key.split('@');
|
|
702
|
+
if (pluginName === name || key === name || pluginName === baseName) {
|
|
703
|
+
pluginExists = true;
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
} catch (e) { /* ignore */ }
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Also check legacy registry
|
|
712
|
+
const legacyPlugin = getPlugin(name);
|
|
713
|
+
if (legacyPlugin) pluginExists = true;
|
|
714
|
+
|
|
715
|
+
if (!pluginExists) {
|
|
716
|
+
throw new Error(`Plugin "${name}" not found`);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Store enabled state in CTX registry (upsert)
|
|
720
|
+
try {
|
|
721
|
+
const { addPlugin } = require('../../plugins/registry');
|
|
722
|
+
if (legacyPlugin) {
|
|
723
|
+
updatePluginRegistry(name, { enabled });
|
|
724
|
+
} else {
|
|
725
|
+
addPlugin(name, { version: '1.0.0', enabled, source: 'claude-native' });
|
|
726
|
+
}
|
|
727
|
+
} catch (e) {
|
|
728
|
+
console.warn('[PluginsService] Failed to update plugin registry:', e.message);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return {
|
|
732
|
+
name,
|
|
733
|
+
enabled,
|
|
734
|
+
success: true
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Update plugin config
|
|
740
|
+
* @param {string} name - Plugin name
|
|
741
|
+
* @param {Object} config - Configuration object
|
|
742
|
+
* @returns {Object} Result
|
|
743
|
+
*/
|
|
744
|
+
updatePluginConfig(name, config) {
|
|
745
|
+
if (this._isOpenCode()) {
|
|
746
|
+
const configDir = path.join(OPENCODE_CONFIG_DIR, 'plugins-config');
|
|
747
|
+
this._ensureDir(configDir);
|
|
748
|
+
const configFile = path.join(configDir, `${name}.json`);
|
|
749
|
+
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), 'utf8');
|
|
750
|
+
return {
|
|
751
|
+
success: true,
|
|
752
|
+
message: `Configuration updated for plugin "${name}"`
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const plugin = getPlugin(name);
|
|
757
|
+
if (!plugin) {
|
|
758
|
+
throw new Error(`Plugin "${name}" not found`);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const configFile = path.join(CONFIG_DIR, `${name}.json`);
|
|
762
|
+
|
|
763
|
+
// Ensure config directory exists
|
|
764
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
765
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), 'utf8');
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
success: true,
|
|
772
|
+
message: `Configuration updated for plugin "${name}"`
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Get plugin repositories config file path
|
|
778
|
+
* @returns {string} Config file path
|
|
779
|
+
*/
|
|
780
|
+
getReposConfigPath() {
|
|
781
|
+
this._ensureDir(this.ccToolConfigDir);
|
|
782
|
+
if (this._isOpenCode()) {
|
|
783
|
+
return path.join(this.ccToolConfigDir, 'opencode-plugin-repos.json');
|
|
784
|
+
}
|
|
785
|
+
return path.join(this.ccToolConfigDir, 'plugin-repos.json');
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
_getDefaultRepos() {
|
|
789
|
+
return cloneRepos(DEFAULT_REPOS_BY_PLATFORM[this.platform] || DEFAULT_REPOS_BY_PLATFORM.claude);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Load repos from config file
|
|
794
|
+
* @returns {Object} Config object with repos array
|
|
795
|
+
*/
|
|
796
|
+
loadReposConfig() {
|
|
797
|
+
const configPath = this.getReposConfigPath();
|
|
798
|
+
const defaultRepos = this._getDefaultRepos();
|
|
799
|
+
if (!fs.existsSync(configPath)) {
|
|
800
|
+
return { repos: defaultRepos };
|
|
801
|
+
}
|
|
802
|
+
try {
|
|
803
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
804
|
+
if (parsed && Array.isArray(parsed.repos)) {
|
|
805
|
+
return parsed;
|
|
806
|
+
}
|
|
807
|
+
return { repos: defaultRepos };
|
|
808
|
+
} catch (err) {
|
|
809
|
+
console.error('Failed to load repos config:', err);
|
|
810
|
+
return { repos: defaultRepos };
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Save repos to config file
|
|
816
|
+
* @param {Object} config - Config object with repos array
|
|
817
|
+
*/
|
|
818
|
+
saveReposConfig(config) {
|
|
819
|
+
const configPath = this.getReposConfigPath();
|
|
820
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Get plugin repositories
|
|
825
|
+
* Reads from both our config and Claude Code's native marketplace config
|
|
826
|
+
* @returns {Array} Repos list
|
|
827
|
+
*/
|
|
828
|
+
getRepos() {
|
|
829
|
+
const repos = [];
|
|
830
|
+
const seenRepos = new Set();
|
|
831
|
+
const pushRepo = (repo) => {
|
|
832
|
+
if (!repo || !repo.owner || !repo.name) return;
|
|
833
|
+
const key = `${repo.owner}/${repo.name}`;
|
|
834
|
+
if (seenRepos.has(key)) return;
|
|
835
|
+
repos.push(repo);
|
|
836
|
+
seenRepos.add(key);
|
|
837
|
+
};
|
|
838
|
+
const parseRepoUrl = (url) => {
|
|
839
|
+
if (!url || typeof url !== 'string') return null;
|
|
840
|
+
const match = url.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
|
841
|
+
if (!match) return null;
|
|
842
|
+
return { owner: match[1], name: match[2], url };
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
// 1. Load our own config
|
|
846
|
+
const config = this.loadReposConfig();
|
|
847
|
+
for (const repo of config.repos || []) {
|
|
848
|
+
pushRepo(repo);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// 2. Load Claude Code's native marketplace config (Claude only)
|
|
852
|
+
if (!this._isOpenCode() && fs.existsSync(CLAUDE_MARKETPLACES_FILE)) {
|
|
853
|
+
try {
|
|
854
|
+
const marketplaces = JSON.parse(fs.readFileSync(CLAUDE_MARKETPLACES_FILE, 'utf8'));
|
|
855
|
+
const entries = [];
|
|
856
|
+
if (Array.isArray(marketplaces)) {
|
|
857
|
+
entries.push(...marketplaces.map(item => ({ key: '', data: item })));
|
|
858
|
+
} else if (marketplaces && typeof marketplaces === 'object') {
|
|
859
|
+
entries.push(...Object.entries(marketplaces).map(([key, data]) => ({ key, data })));
|
|
860
|
+
if (Array.isArray(marketplaces.marketplaces)) {
|
|
861
|
+
entries.push(...marketplaces.marketplaces.map(item => ({ key: item?.name || '', data: item })));
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
for (const { key, data } of entries) {
|
|
866
|
+
const sourceUrl = data?.source?.url || data?.url || data?.repoUrl || data?.repository;
|
|
867
|
+
const parsed = parseRepoUrl(sourceUrl);
|
|
868
|
+
if (!parsed) continue;
|
|
869
|
+
pushRepo({
|
|
870
|
+
owner: parsed.owner,
|
|
871
|
+
name: parsed.name,
|
|
872
|
+
url: parsed.url,
|
|
873
|
+
branch: data?.source?.branch || data?.branch || 'main',
|
|
874
|
+
enabled: data?.enabled !== false,
|
|
875
|
+
source: 'claude-native',
|
|
876
|
+
marketplace: key || data?.name || '',
|
|
877
|
+
lastUpdated: data?.lastUpdated
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
} catch (err) {
|
|
881
|
+
console.error('[PluginsService] Failed to read known_marketplaces.json:', err.message);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
return repos;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Add repository
|
|
890
|
+
* @param {Object} repo - Repository info { url, owner, name, branch, enabled }
|
|
891
|
+
* @returns {Array} Updated repos list
|
|
892
|
+
*/
|
|
893
|
+
addRepo(repo) {
|
|
894
|
+
const config = this.loadReposConfig();
|
|
895
|
+
|
|
896
|
+
// Parse URL if provided
|
|
897
|
+
let owner = repo.owner;
|
|
898
|
+
let name = repo.name;
|
|
899
|
+
let url = repo.url;
|
|
900
|
+
|
|
901
|
+
if (url && !owner && !name) {
|
|
902
|
+
// Extract owner/name from URL
|
|
903
|
+
const match = url.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
|
904
|
+
if (match) {
|
|
905
|
+
owner = match[1];
|
|
906
|
+
name = match[2];
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (!owner || !name) {
|
|
911
|
+
throw new Error('Repository owner and name are required');
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Construct URL if not provided
|
|
915
|
+
if (!url) {
|
|
916
|
+
url = `https://github.com/${owner}/${name}`;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Check if repo already exists
|
|
920
|
+
const exists = config.repos.some(r => r.owner === owner && r.name === name);
|
|
921
|
+
if (exists) {
|
|
922
|
+
throw new Error(`Repository ${owner}/${name} already exists`);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Add new repo
|
|
926
|
+
const newRepo = {
|
|
927
|
+
owner,
|
|
928
|
+
name,
|
|
929
|
+
url,
|
|
930
|
+
branch: repo.branch || 'main',
|
|
931
|
+
enabled: repo.enabled !== false,
|
|
932
|
+
addedAt: new Date().toISOString()
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
config.repos.push(newRepo);
|
|
936
|
+
this.saveReposConfig(config);
|
|
937
|
+
|
|
938
|
+
return config.repos;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Remove repository
|
|
943
|
+
* @param {string} owner - Repository owner
|
|
944
|
+
* @param {string} name - Repository name
|
|
945
|
+
* @returns {Array} Updated repos list
|
|
946
|
+
*/
|
|
947
|
+
removeRepo(owner, name) {
|
|
948
|
+
const config = this.loadReposConfig();
|
|
949
|
+
config.repos = config.repos.filter(r => !(r.owner === owner && r.name === name));
|
|
950
|
+
this.saveReposConfig(config);
|
|
951
|
+
return config.repos;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Toggle repository enabled status
|
|
956
|
+
* @param {string} owner - Repository owner
|
|
957
|
+
* @param {string} name - Repository name
|
|
958
|
+
* @param {boolean} enabled - Enable or disable
|
|
959
|
+
* @returns {Array} Updated repos list
|
|
960
|
+
*/
|
|
961
|
+
toggleRepo(owner, name, enabled) {
|
|
962
|
+
const config = this.loadReposConfig();
|
|
963
|
+
const repo = config.repos.find(r => r.owner === owner && r.name === name);
|
|
964
|
+
if (!repo) {
|
|
965
|
+
throw new Error(`Repository ${owner}/${name} not found`);
|
|
966
|
+
}
|
|
967
|
+
repo.enabled = enabled;
|
|
968
|
+
this.saveReposConfig(config);
|
|
969
|
+
return config.repos;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Sync repositories to Claude Code marketplace
|
|
974
|
+
* @returns {Promise<Object>} Sync results
|
|
975
|
+
*/
|
|
976
|
+
async syncRepos() {
|
|
977
|
+
if (this._isOpenCode()) {
|
|
978
|
+
return { success: true, results: [] };
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const repos = this.getRepos();
|
|
982
|
+
const results = [];
|
|
983
|
+
const { execSync } = require('child_process');
|
|
984
|
+
|
|
985
|
+
for (const repo of repos.filter(r => r.enabled)) {
|
|
986
|
+
try {
|
|
987
|
+
execSync(`claude plugin marketplace add ${repo.url}`, {
|
|
988
|
+
encoding: 'utf8',
|
|
989
|
+
timeout: 30000,
|
|
990
|
+
stdio: 'pipe'
|
|
991
|
+
});
|
|
992
|
+
results.push({ repo: repo.url, success: true });
|
|
993
|
+
} catch (err) {
|
|
994
|
+
results.push({ repo: repo.url, success: false, error: err.message });
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return { success: true, results };
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Sync plugins from Claude Code
|
|
1003
|
+
* @returns {Promise<Object>} Updated plugins list
|
|
1004
|
+
*/
|
|
1005
|
+
async syncPlugins() {
|
|
1006
|
+
return this.listPlugins();
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Fetch JSON from URL
|
|
1011
|
+
* @private
|
|
1012
|
+
*/
|
|
1013
|
+
async _fetchJson(url) {
|
|
1014
|
+
const https = require('https');
|
|
1015
|
+
return new Promise((resolve, reject) => {
|
|
1016
|
+
https.get(url, {
|
|
1017
|
+
headers: {
|
|
1018
|
+
'User-Agent': 'coding-tool-x',
|
|
1019
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
1020
|
+
}
|
|
1021
|
+
}, (res) => {
|
|
1022
|
+
let data = '';
|
|
1023
|
+
res.on('data', chunk => data += chunk);
|
|
1024
|
+
res.on('end', () => {
|
|
1025
|
+
if (res.statusCode === 200) {
|
|
1026
|
+
resolve(JSON.parse(data));
|
|
1027
|
+
} else {
|
|
1028
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
}).on('error', reject);
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Get plugin README content
|
|
1037
|
+
* @param {Object} plugin - Plugin object with name, repoUrl, source, or repoInfo
|
|
1038
|
+
* @returns {Promise<string>} README content or empty string
|
|
1039
|
+
*/
|
|
1040
|
+
async getPluginReadme(plugin) {
|
|
1041
|
+
try {
|
|
1042
|
+
let readmeUrl = null;
|
|
1043
|
+
|
|
1044
|
+
// Case 1: Market plugin with repoInfo
|
|
1045
|
+
if (plugin.repoOwner && plugin.repoName && plugin.directory) {
|
|
1046
|
+
const branch = plugin.repoBranch || 'main';
|
|
1047
|
+
readmeUrl = `https://raw.githubusercontent.com/${plugin.repoOwner}/${plugin.repoName}/${branch}/${plugin.directory}/README.md`;
|
|
1048
|
+
}
|
|
1049
|
+
// Case 2: Installed plugin with source URL
|
|
1050
|
+
else if (plugin.source) {
|
|
1051
|
+
const treeMatch = plugin.source.match(/github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/);
|
|
1052
|
+
if (treeMatch) {
|
|
1053
|
+
const [, owner, name, branch, directory] = treeMatch;
|
|
1054
|
+
readmeUrl = `https://raw.githubusercontent.com/${owner}/${name}/${branch}/${directory}/README.md`;
|
|
1055
|
+
} else {
|
|
1056
|
+
// Try to parse as regular repo URL
|
|
1057
|
+
const repoMatch = plugin.source.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
|
1058
|
+
if (repoMatch) {
|
|
1059
|
+
const [, owner, name] = repoMatch;
|
|
1060
|
+
readmeUrl = `https://raw.githubusercontent.com/${owner}/${name}/main/README.md`;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
// Case 3: Plugin with repoUrl
|
|
1065
|
+
else if (plugin.repoUrl) {
|
|
1066
|
+
const match = plugin.repoUrl.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
|
1067
|
+
if (match) {
|
|
1068
|
+
const [, owner, name] = match;
|
|
1069
|
+
readmeUrl = `https://raw.githubusercontent.com/${owner}/${name}/main/README.md`;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
if (!readmeUrl) {
|
|
1074
|
+
return '';
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Fetch README content
|
|
1078
|
+
const content = await this._fetchRawFile(readmeUrl);
|
|
1079
|
+
return content;
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
console.error('[PluginsService] Failed to fetch README:', err.message);
|
|
1082
|
+
return '';
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
_parseGitHubRepo(url = '') {
|
|
1087
|
+
const match = url.match(/github\.com\/([^\/]+)\/([^\/#?]+)/i);
|
|
1088
|
+
if (!match) return null;
|
|
1089
|
+
return {
|
|
1090
|
+
owner: match[1],
|
|
1091
|
+
name: match[2].replace(/\.git$/, '')
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async _fetchOpenCodeMarketplacePlugins(repo, branch) {
|
|
1096
|
+
if (!this._isOpenCode()) return [];
|
|
1097
|
+
|
|
1098
|
+
let entries;
|
|
1099
|
+
try {
|
|
1100
|
+
const indexUrl = `https://api.github.com/repos/${repo.owner}/${repo.name}/contents/plugins?ref=${branch}`;
|
|
1101
|
+
entries = await this._fetchJson(indexUrl);
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
return [];
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
if (!Array.isArray(entries)) return [];
|
|
1107
|
+
|
|
1108
|
+
const manifestFiles = entries.filter(
|
|
1109
|
+
item => item.type === 'file' && item.name.endsWith('.plugin.json')
|
|
1110
|
+
);
|
|
1111
|
+
if (manifestFiles.length === 0) return [];
|
|
1112
|
+
|
|
1113
|
+
const results = await Promise.allSettled(
|
|
1114
|
+
manifestFiles.map(async (file) => {
|
|
1115
|
+
const fileUrl = file.download_url ||
|
|
1116
|
+
`https://raw.githubusercontent.com/${repo.owner}/${repo.name}/${branch}/${file.path}`;
|
|
1117
|
+
const manifest = await this._fetchJson(fileUrl);
|
|
1118
|
+
|
|
1119
|
+
const author = Array.isArray(manifest.authors)
|
|
1120
|
+
? manifest.authors.map(item => item?.name).filter(Boolean).join(', ')
|
|
1121
|
+
: '';
|
|
1122
|
+
const firstCategory = Array.isArray(manifest.categories) ? manifest.categories[0] : '';
|
|
1123
|
+
const repoUrl = manifest.links?.repository || `https://github.com/${repo.owner}/${repo.name}`;
|
|
1124
|
+
// OpenCode supports npm package plugins via opencode.json "plugin" array.
|
|
1125
|
+
// Use package name as install source so UI install button is enabled.
|
|
1126
|
+
const installSource = String(manifest.name || '').trim();
|
|
1127
|
+
const githubRepo = this._parseGitHubRepo(repoUrl);
|
|
1128
|
+
|
|
1129
|
+
return {
|
|
1130
|
+
name: manifest.name || file.name.replace(/\.plugin\.json$/, ''),
|
|
1131
|
+
displayName: manifest.displayName || '',
|
|
1132
|
+
description: manifest.description || '',
|
|
1133
|
+
author: author || repo.owner,
|
|
1134
|
+
version: manifest.version || manifest.opencode?.minimumVersion || '1.0.0',
|
|
1135
|
+
category: firstCategory ? String(firstCategory).toLowerCase() : 'general',
|
|
1136
|
+
repoUrl,
|
|
1137
|
+
repoOwner: '',
|
|
1138
|
+
repoName: '',
|
|
1139
|
+
repoBranch: githubRepo ? 'main' : branch,
|
|
1140
|
+
directory: file.path,
|
|
1141
|
+
installSource,
|
|
1142
|
+
marketplaceFormat: 'opencode-plugin-json',
|
|
1143
|
+
isInstalled: false
|
|
1144
|
+
};
|
|
1145
|
+
})
|
|
1146
|
+
);
|
|
1147
|
+
|
|
1148
|
+
return results
|
|
1149
|
+
.filter(item => item.status === 'fulfilled' && item.value)
|
|
1150
|
+
.map(item => item.value);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Get market plugins from configured repositories
|
|
1155
|
+
* @returns {Promise<Array>} List of available market plugins
|
|
1156
|
+
*/
|
|
1157
|
+
async getMarketPlugins() {
|
|
1158
|
+
const repos = this.getRepos().filter(r => r.enabled);
|
|
1159
|
+
const marketPlugins = [];
|
|
1160
|
+
|
|
1161
|
+
for (const repo of repos) {
|
|
1162
|
+
try {
|
|
1163
|
+
const branch = repo.branch || 'main';
|
|
1164
|
+
|
|
1165
|
+
// Try to fetch marketplace.json first (official format)
|
|
1166
|
+
try {
|
|
1167
|
+
const marketplaceUrl = `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/${branch}/.claude-plugin/marketplace.json`;
|
|
1168
|
+
const marketplace = await this._fetchJson(marketplaceUrl);
|
|
1169
|
+
|
|
1170
|
+
if (marketplace && marketplace.plugins) {
|
|
1171
|
+
for (const plugin of marketplace.plugins) {
|
|
1172
|
+
marketPlugins.push({
|
|
1173
|
+
name: plugin.name,
|
|
1174
|
+
description: plugin.description || '',
|
|
1175
|
+
author: plugin.author?.name || marketplace.owner?.name || repo.owner,
|
|
1176
|
+
version: plugin.version || '1.0.0',
|
|
1177
|
+
category: plugin.category || 'general',
|
|
1178
|
+
repoUrl: `https://github.com/${repo.owner}/${repo.name}`,
|
|
1179
|
+
repoOwner: repo.owner,
|
|
1180
|
+
repoName: repo.name,
|
|
1181
|
+
repoBranch: branch,
|
|
1182
|
+
directory: plugin.source?.replace(/^\.\//, '') || plugin.name,
|
|
1183
|
+
lspServers: plugin.lspServers || null,
|
|
1184
|
+
isInstalled: false
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
continue; // Skip legacy format check
|
|
1188
|
+
}
|
|
1189
|
+
} catch (e) {
|
|
1190
|
+
// marketplace.json not found, try legacy format
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// OpenCode plugin marketplace format: plugins/*.plugin.json
|
|
1194
|
+
if (this._isOpenCode()) {
|
|
1195
|
+
const openCodeMarketplacePlugins = await this._fetchOpenCodeMarketplacePlugins(repo, branch);
|
|
1196
|
+
if (openCodeMarketplacePlugins.length > 0) {
|
|
1197
|
+
marketPlugins.push(...openCodeMarketplacePlugins);
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Legacy format: each directory is a plugin with plugin.json/package.json
|
|
1203
|
+
const apiUrl = `https://api.github.com/repos/${repo.owner}/${repo.name}/contents?ref=${branch}`;
|
|
1204
|
+
const contents = await this._fetchJson(apiUrl);
|
|
1205
|
+
const pluginDirs = contents.filter(item => item.type === 'dir' && !item.name.startsWith('.'));
|
|
1206
|
+
|
|
1207
|
+
for (const dir of pluginDirs) {
|
|
1208
|
+
try {
|
|
1209
|
+
const manifestUrl = `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/${branch}/${dir.name}/plugin.json`;
|
|
1210
|
+
const manifest = await this._fetchJson(manifestUrl);
|
|
1211
|
+
|
|
1212
|
+
marketPlugins.push({
|
|
1213
|
+
name: manifest.name || dir.name,
|
|
1214
|
+
description: manifest.description || '',
|
|
1215
|
+
author: manifest.author || repo.owner,
|
|
1216
|
+
version: manifest.version || '1.0.0',
|
|
1217
|
+
repoUrl: `https://github.com/${repo.owner}/${repo.name}`,
|
|
1218
|
+
repoOwner: repo.owner,
|
|
1219
|
+
repoName: repo.name,
|
|
1220
|
+
repoBranch: branch,
|
|
1221
|
+
directory: dir.name,
|
|
1222
|
+
commands: manifest.commands || [],
|
|
1223
|
+
hooks: manifest.hooks || [],
|
|
1224
|
+
isInstalled: false
|
|
1225
|
+
});
|
|
1226
|
+
} catch (e) {
|
|
1227
|
+
// OpenCode 仓库常见 package.json 格式
|
|
1228
|
+
if (this._isOpenCode()) {
|
|
1229
|
+
try {
|
|
1230
|
+
const pkgUrl = `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/${branch}/${dir.name}/package.json`;
|
|
1231
|
+
const pkg = await this._fetchJson(pkgUrl);
|
|
1232
|
+
const pluginName = pkg.name || dir.name;
|
|
1233
|
+
marketPlugins.push({
|
|
1234
|
+
name: pluginName,
|
|
1235
|
+
description: pkg.description || '',
|
|
1236
|
+
author: pkg.author || repo.owner,
|
|
1237
|
+
version: pkg.version || '1.0.0',
|
|
1238
|
+
repoUrl: `https://github.com/${repo.owner}/${repo.name}`,
|
|
1239
|
+
repoOwner: repo.owner,
|
|
1240
|
+
repoName: repo.name,
|
|
1241
|
+
repoBranch: branch,
|
|
1242
|
+
directory: dir.name,
|
|
1243
|
+
isInstalled: false
|
|
1244
|
+
});
|
|
1245
|
+
} catch (pkgErr) {
|
|
1246
|
+
// neither plugin.json nor package.json
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
} catch (err) {
|
|
1252
|
+
console.error(`[PluginsService] Failed to fetch plugins from ${repo.owner}/${repo.name}:`, err.message);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// Mark installed plugins
|
|
1257
|
+
const installedPlugins = this.listPlugins().plugins;
|
|
1258
|
+
const installedNames = new Set(installedPlugins.map(p => p.name));
|
|
1259
|
+
|
|
1260
|
+
marketPlugins.forEach(plugin => {
|
|
1261
|
+
plugin.isInstalled = installedNames.has(plugin.name);
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
return marketPlugins;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
module.exports = { PluginsService };
|