coding-tool-x 3.6.7 → 3.6.9
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/dist/web/assets/{Analytics-CKycBI0G.js → Analytics-BuBlsbcI.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-CACLQb8e.js → ConfigTemplates-DQczxuOX.js} +1 -1
- package/dist/web/assets/Home-BvAzzWnB.js +1 -0
- package/dist/web/assets/Home-CIua-b-o.css +1 -0
- package/dist/web/assets/{PluginManager-CQJl_uso.js → PluginManager-C8gAlaps.js} +1 -1
- package/dist/web/assets/{ProjectList-DSkvQFEP.js → ProjectList-nqgi6_cR.js} +1 -1
- package/dist/web/assets/{SessionList-Czjs1GN1.js → SessionList-D8AxMYy6.js} +1 -1
- package/dist/web/assets/{SkillManager-Cg1xZ5Cv.js → SkillManager-BDbNbNkK.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-COjYeKvN.js → WorkspaceManager-DGGaGW2c.js} +1 -1
- package/dist/web/assets/index-Dsh4Q5Cg.css +1 -0
- package/dist/web/assets/index-y-eEayxR.js +2 -0
- package/dist/web/assets/{naive-ui-DM897mIP.js → naive-ui-kcWDeJwl.js} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/src/commands/stats.js +1 -1
- package/src/config/channel-preset-websites.js +2 -1
- package/src/config/paths.js +8 -0
- package/src/server/api/agents.js +16 -54
- package/src/server/api/claude-hooks.js +1 -1
- package/src/server/api/claude-statistics.js +57 -0
- package/src/server/api/commands.js +16 -54
- package/src/server/api/dashboard.js +2 -2
- package/src/server/api/gemini-channels.js +2 -0
- package/src/server/api/hooks.js +4 -1
- package/src/server/api/plugins.js +46 -69
- package/src/server/api/skills.js +21 -77
- package/src/server/api/validation-errors.js +34 -0
- package/src/server/gemini-proxy-server.js +205 -21
- package/src/server/index.js +1 -0
- package/src/server/services/agents-service.js +166 -75
- package/src/server/services/base/base-channel-service.js +15 -0
- package/src/server/services/base/response-usage-parser.js +44 -0
- package/src/server/services/channel-balance.js +86 -1
- package/src/server/services/claude-statistics-service.js +139 -0
- package/src/server/services/commands-service.js +203 -121
- package/src/server/services/config-artifact-paths.js +116 -0
- package/src/server/services/config-export-service.js +7 -6
- package/src/server/services/config-registry-service.js +2 -2
- package/src/server/services/config-sync-manager.js +45 -11
- package/src/server/services/config-templates-service.js +21 -19
- package/src/server/services/format-converter.js +42 -4
- package/src/server/services/gemini-channels.js +30 -1
- package/src/server/services/gemini-sessions.js +347 -61
- package/src/server/services/mcp-client.js +236 -49
- package/src/server/services/mcp-service.js +51 -64
- package/src/server/services/notification-hooks.js +659 -88
- package/src/server/services/opencode-channels.js +15 -0
- package/src/server/services/plugins-service.js +1137 -86
- package/src/server/services/repo-scanner-base.js +6 -29
- package/src/server/services/skill-service.js +198 -65
- package/dist/web/assets/Home-BeGkC4bb.css +0 -1
- package/dist/web/assets/Home-Bk6sP5Ed.js +0 -1
- package/dist/web/assets/index-0isNzXNY.css +0 -1
- package/dist/web/assets/index-fWgvlZQx.js +0 -2
|
@@ -10,6 +10,8 @@ const http = require('http');
|
|
|
10
10
|
const https = require('https');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const { execFileSync } = require('child_process');
|
|
13
|
+
const toml = require('toml');
|
|
14
|
+
const tomlStringify = require('@iarna/toml').stringify;
|
|
13
15
|
const AdmZip = require('adm-zip');
|
|
14
16
|
const { listPlugins, getPlugin, updatePlugin: updatePluginRegistry } = require('../../plugins/registry');
|
|
15
17
|
const { installPlugin: installPluginCore, uninstallPlugin: uninstallPluginCore } = require('../../plugins/plugin-installer');
|
|
@@ -17,10 +19,20 @@ const { initializePlugins, shutdownPlugins } = require('../../plugins/plugin-man
|
|
|
17
19
|
const { INSTALLED_DIR, CONFIG_DIR } = require('../../plugins/constants');
|
|
18
20
|
const { NATIVE_PATHS, PATHS } = require('../../config/paths');
|
|
19
21
|
const { maskToken } = require('./oauth-utils');
|
|
22
|
+
const {
|
|
23
|
+
assertInsideAllowedRoots,
|
|
24
|
+
normalizeSafeFileStem,
|
|
25
|
+
normalizeSafeRelativePath,
|
|
26
|
+
resolveInsideRoot
|
|
27
|
+
} = require('./config-artifact-paths');
|
|
20
28
|
|
|
21
29
|
const CLAUDE_PLUGINS_DIR = path.join(path.dirname(NATIVE_PATHS.claude.settings), 'plugins');
|
|
22
30
|
const CLAUDE_INSTALLED_FILE = path.join(CLAUDE_PLUGINS_DIR, 'installed_plugins.json');
|
|
23
31
|
const CLAUDE_MARKETPLACES_FILE = path.join(CLAUDE_PLUGINS_DIR, 'known_marketplaces.json');
|
|
32
|
+
const CLAUDE_PLUGINS_CACHE_DIR = path.join(CLAUDE_PLUGINS_DIR, 'cache');
|
|
33
|
+
const CLAUDE_MARKETPLACES_DIR = path.join(CLAUDE_PLUGINS_DIR, 'marketplaces');
|
|
34
|
+
const CODEX_PLUGINS_DIR = path.join(path.dirname(NATIVE_PATHS.codex.config), 'plugins');
|
|
35
|
+
const CODEX_PLUGINS_CACHE_DIR = path.join(CODEX_PLUGINS_DIR, 'cache');
|
|
24
36
|
const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
|
|
25
37
|
const REPO_SOURCE_META_FILE = '.cc-tool-plugin-source.json';
|
|
26
38
|
const SUPPORTED_REPO_PROVIDERS = ['github', 'gitlab', 'local'];
|
|
@@ -28,8 +40,62 @@ const DEFAULT_GITHUB_HOST = 'https://github.com';
|
|
|
28
40
|
const DEFAULT_GITLAB_HOST = 'https://gitlab.com';
|
|
29
41
|
const DEFAULT_REPOS_BY_PLATFORM = {
|
|
30
42
|
claude: [],
|
|
43
|
+
codex: [],
|
|
44
|
+
gemini: [],
|
|
31
45
|
opencode: []
|
|
32
46
|
};
|
|
47
|
+
const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
|
|
48
|
+
const PLATFORM_CAPABILITIES = {
|
|
49
|
+
claude: {
|
|
50
|
+
platform: 'claude',
|
|
51
|
+
supportsPlugins: true,
|
|
52
|
+
repositories: true,
|
|
53
|
+
market: true,
|
|
54
|
+
install: true,
|
|
55
|
+
uninstall: true,
|
|
56
|
+
toggle: true,
|
|
57
|
+
config: true,
|
|
58
|
+
import: true,
|
|
59
|
+
syncRepos: true
|
|
60
|
+
},
|
|
61
|
+
codex: {
|
|
62
|
+
platform: 'codex',
|
|
63
|
+
supportsPlugins: true,
|
|
64
|
+
repositories: true,
|
|
65
|
+
market: true,
|
|
66
|
+
install: true,
|
|
67
|
+
uninstall: true,
|
|
68
|
+
toggle: true,
|
|
69
|
+
config: false,
|
|
70
|
+
import: false,
|
|
71
|
+
syncRepos: false
|
|
72
|
+
},
|
|
73
|
+
gemini: {
|
|
74
|
+
platform: 'gemini',
|
|
75
|
+
supportsPlugins: false,
|
|
76
|
+
repositories: false,
|
|
77
|
+
market: false,
|
|
78
|
+
install: false,
|
|
79
|
+
uninstall: false,
|
|
80
|
+
toggle: false,
|
|
81
|
+
config: false,
|
|
82
|
+
import: false,
|
|
83
|
+
syncRepos: false,
|
|
84
|
+
disabledReason: 'Gemini plugin management is not implemented yet'
|
|
85
|
+
},
|
|
86
|
+
opencode: {
|
|
87
|
+
platform: 'opencode',
|
|
88
|
+
supportsPlugins: true,
|
|
89
|
+
repositories: true,
|
|
90
|
+
market: true,
|
|
91
|
+
install: true,
|
|
92
|
+
uninstall: true,
|
|
93
|
+
toggle: true,
|
|
94
|
+
config: true,
|
|
95
|
+
import: false,
|
|
96
|
+
syncRepos: false
|
|
97
|
+
}
|
|
98
|
+
};
|
|
33
99
|
|
|
34
100
|
function cloneRepos(repos = []) {
|
|
35
101
|
return repos.map(repo => ({ ...repo }));
|
|
@@ -50,10 +116,82 @@ function normalizeRepoDirectory(directory = '') {
|
|
|
50
116
|
return normalizeRepoPath(directory);
|
|
51
117
|
}
|
|
52
118
|
|
|
119
|
+
function normalizePluginRepoDirectory(directory = '', label = 'plugin directory') {
|
|
120
|
+
return normalizeSafeRelativePath(directory || '', label, {
|
|
121
|
+
allowEmpty: true,
|
|
122
|
+
allowHiddenSegments: true
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizePluginPathName(name = '', label = 'plugin name') {
|
|
127
|
+
return normalizeSafeRelativePath(name, label, {
|
|
128
|
+
allowHiddenSegments: false
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizePluginCacheSegment(value = '', label = 'plugin segment', fallback = 'local') {
|
|
133
|
+
const rawValue = String(value || '').trim() || fallback;
|
|
134
|
+
return normalizeSafeFileStem(rawValue, label, {
|
|
135
|
+
allowDots: true,
|
|
136
|
+
pattern: /^[a-zA-Z0-9][a-zA-Z0-9._@+-]*$/
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function normalizeClaudeMarketplaceSegment(value = '', fallback = 'ctx') {
|
|
141
|
+
return normalizePluginCacheSegment(value, 'Claude marketplace', fallback);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolvePluginConfigFile(configDir, name) {
|
|
145
|
+
const safeName = normalizePluginPathName(name, 'plugin config name');
|
|
146
|
+
return resolveInsideRoot(configDir, `${safeName}.json`, 'Plugin config path');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function joinRepoPath(...parts) {
|
|
150
|
+
return normalizeRepoPath(parts.filter(Boolean).join('/'));
|
|
151
|
+
}
|
|
152
|
+
|
|
53
153
|
function stripGitSuffix(value = '') {
|
|
54
154
|
return String(value || '').replace(/\.git$/i, '');
|
|
55
155
|
}
|
|
56
156
|
|
|
157
|
+
function slugifyCodexKey(value = '', fallback = 'local') {
|
|
158
|
+
const slug = String(value || '')
|
|
159
|
+
.trim()
|
|
160
|
+
.replace(/\\/g, '/')
|
|
161
|
+
.split('/')
|
|
162
|
+
.filter(Boolean)
|
|
163
|
+
.pop()
|
|
164
|
+
?.replace(/\.git$/i, '')
|
|
165
|
+
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
166
|
+
.replace(/^-+|-+$/g, '');
|
|
167
|
+
return slug || fallback;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function splitPluginMarketplaceKey(key = '') {
|
|
171
|
+
const value = String(key || '');
|
|
172
|
+
const atIndex = value.lastIndexOf('@');
|
|
173
|
+
if (atIndex <= 0) return { name: value, marketplace: '' };
|
|
174
|
+
return {
|
|
175
|
+
name: value.slice(0, atIndex),
|
|
176
|
+
marketplace: value.slice(atIndex + 1)
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function hasRepoInstallInfo(repoInfo = null) {
|
|
181
|
+
return Boolean(
|
|
182
|
+
repoInfo &&
|
|
183
|
+
typeof repoInfo === 'object' &&
|
|
184
|
+
(
|
|
185
|
+
Object.prototype.hasOwnProperty.call(repoInfo, 'directory') ||
|
|
186
|
+
repoInfo.localPath ||
|
|
187
|
+
repoInfo.owner ||
|
|
188
|
+
repoInfo.projectPath ||
|
|
189
|
+
repoInfo.repoUrl ||
|
|
190
|
+
repoInfo.url
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
57
195
|
function isWindowsAbsolutePath(input = '') {
|
|
58
196
|
return /^[a-zA-Z]:[\\/]/.test(String(input || ''));
|
|
59
197
|
}
|
|
@@ -210,17 +348,24 @@ function stripJsonComments(input = '') {
|
|
|
210
348
|
|
|
211
349
|
class PluginsService {
|
|
212
350
|
constructor(platform = 'claude') {
|
|
213
|
-
this.platform =
|
|
351
|
+
this.platform = SUPPORTED_PLATFORMS.includes(platform) ? platform : 'claude';
|
|
214
352
|
this.configDir = PATHS.config || path.join((PATHS.base || process.env.HOME || os.homedir()), 'config');
|
|
215
353
|
this.ccToolConfigDir = path.dirname(PATHS.pluginRepos.claude);
|
|
216
354
|
this.opencodePluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugins');
|
|
217
355
|
this.opencodeLegacyPluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugin');
|
|
218
|
-
this.
|
|
219
|
-
|
|
220
|
-
: PATHS.pluginMarketCache.claude;
|
|
356
|
+
this.codexPluginsCacheDir = CODEX_PLUGINS_CACHE_DIR;
|
|
357
|
+
this.marketCachePath = PATHS.pluginMarketCache[this.platform] || PATHS.pluginMarketCache.claude;
|
|
221
358
|
this._marketCache = null;
|
|
222
359
|
}
|
|
223
360
|
|
|
361
|
+
getCapabilities() {
|
|
362
|
+
return { ...(PLATFORM_CAPABILITIES[this.platform] || PLATFORM_CAPABILITIES.claude) };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
_pluginsSupported() {
|
|
366
|
+
return this.getCapabilities().supportsPlugins !== false;
|
|
367
|
+
}
|
|
368
|
+
|
|
224
369
|
normalizeRepoConfig(repo = {}) {
|
|
225
370
|
const provider = SUPPORTED_REPO_PROVIDERS.includes(repo.provider)
|
|
226
371
|
? repo.provider
|
|
@@ -426,6 +571,14 @@ class PluginsService {
|
|
|
426
571
|
return this.platform === 'opencode';
|
|
427
572
|
}
|
|
428
573
|
|
|
574
|
+
_isCodex() {
|
|
575
|
+
return this.platform === 'codex';
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
_isGemini() {
|
|
579
|
+
return this.platform === 'gemini';
|
|
580
|
+
}
|
|
581
|
+
|
|
429
582
|
_getOpenCodePluginsDir() {
|
|
430
583
|
if (fs.existsSync(this.opencodeLegacyPluginsDir) && !fs.existsSync(this.opencodePluginsDir)) {
|
|
431
584
|
return this.opencodeLegacyPluginsDir;
|
|
@@ -539,12 +692,397 @@ class PluginsService {
|
|
|
539
692
|
return plugins;
|
|
540
693
|
}
|
|
541
694
|
|
|
695
|
+
_readFirstMarkdownParagraph(filePath) {
|
|
696
|
+
if (!fs.existsSync(filePath)) return '';
|
|
697
|
+
try {
|
|
698
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
699
|
+
const lines = content
|
|
700
|
+
.split(/\r?\n/)
|
|
701
|
+
.map(line => line.trim())
|
|
702
|
+
.filter(line => line && !line.startsWith('#') && !line.startsWith('<!--'));
|
|
703
|
+
return lines[0] || '';
|
|
704
|
+
} catch {
|
|
705
|
+
return '';
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
_listCodexCachedPlugins() {
|
|
710
|
+
const cacheDir = this.codexPluginsCacheDir;
|
|
711
|
+
if (!fs.existsSync(cacheDir)) return [];
|
|
712
|
+
|
|
713
|
+
const plugins = [];
|
|
714
|
+
try {
|
|
715
|
+
const marketplaces = fs.readdirSync(cacheDir, { withFileTypes: true });
|
|
716
|
+
for (const marketplaceEntry of marketplaces) {
|
|
717
|
+
if (!marketplaceEntry.isDirectory() || marketplaceEntry.name.startsWith('.')) continue;
|
|
718
|
+
const marketplaceDir = path.join(cacheDir, marketplaceEntry.name);
|
|
719
|
+
const pluginEntries = fs.readdirSync(marketplaceDir, { withFileTypes: true });
|
|
720
|
+
|
|
721
|
+
for (const pluginEntry of pluginEntries) {
|
|
722
|
+
if (!pluginEntry.isDirectory() || pluginEntry.name.startsWith('.')) continue;
|
|
723
|
+
const pluginRoot = path.join(marketplaceDir, pluginEntry.name);
|
|
724
|
+
const versionEntries = fs.readdirSync(pluginRoot, { withFileTypes: true })
|
|
725
|
+
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.'));
|
|
726
|
+
const selectedVersion = versionEntries
|
|
727
|
+
.map(entry => ({
|
|
728
|
+
name: entry.name,
|
|
729
|
+
fullPath: path.join(pluginRoot, entry.name),
|
|
730
|
+
mtimeMs: fs.statSync(path.join(pluginRoot, entry.name)).mtimeMs
|
|
731
|
+
}))
|
|
732
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)[0];
|
|
733
|
+
if (!selectedVersion) continue;
|
|
734
|
+
|
|
735
|
+
const description = this._readFirstMarkdownParagraph(path.join(selectedVersion.fullPath, 'README.md'));
|
|
736
|
+
const manifest = this._readLocalManifest(selectedVersion.fullPath, [
|
|
737
|
+
'.codex-plugin/plugin.json',
|
|
738
|
+
'plugin.json',
|
|
739
|
+
'package.json'
|
|
740
|
+
]) || {};
|
|
741
|
+
plugins.push({
|
|
742
|
+
name: manifest.name || pluginEntry.name,
|
|
743
|
+
marketplace: marketplaceEntry.name,
|
|
744
|
+
version: manifest.version || selectedVersion.name,
|
|
745
|
+
installPath: selectedVersion.fullPath,
|
|
746
|
+
directory: path.relative(CODEX_PLUGINS_DIR, selectedVersion.fullPath),
|
|
747
|
+
source: 'codex-cache',
|
|
748
|
+
installed: true,
|
|
749
|
+
enabled: this._isCodexPluginEnabled(manifest.name || pluginEntry.name, marketplaceEntry.name),
|
|
750
|
+
pluginType: 'cache',
|
|
751
|
+
description: manifest.description || description
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
} catch (err) {
|
|
756
|
+
console.warn('[PluginsService] Failed to scan Codex plugin cache:', err.message);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
plugins.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
|
760
|
+
return plugins;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
_readLocalManifest(rootDir, candidates = []) {
|
|
764
|
+
for (const candidate of candidates) {
|
|
765
|
+
const manifestPath = path.join(rootDir, candidate);
|
|
766
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
767
|
+
try {
|
|
768
|
+
return JSON.parse(stripJsonComments(fs.readFileSync(manifestPath, 'utf8')));
|
|
769
|
+
} catch {
|
|
770
|
+
// try next candidate
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
_readCodexConfig() {
|
|
777
|
+
const filePath = NATIVE_PATHS.codex.config;
|
|
778
|
+
if (!fs.existsSync(filePath)) return { filePath, config: {} };
|
|
779
|
+
try {
|
|
780
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
781
|
+
if (!raw.trim()) return { filePath, config: {} };
|
|
782
|
+
return { filePath, config: toml.parse(raw) };
|
|
783
|
+
} catch (err) {
|
|
784
|
+
console.error('[PluginsService] Failed to read Codex config:', err.message);
|
|
785
|
+
return { filePath, config: {} };
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
_writeCodexConfig(config) {
|
|
790
|
+
const filePath = NATIVE_PATHS.codex.config;
|
|
791
|
+
this._ensureDir(path.dirname(filePath));
|
|
792
|
+
const safeConfig = JSON.parse(JSON.stringify(config || {}));
|
|
793
|
+
fs.writeFileSync(filePath, tomlStringify(safeConfig), 'utf8');
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
_readClaudeSettings() {
|
|
797
|
+
const filePath = NATIVE_PATHS.claude.settings;
|
|
798
|
+
if (!fs.existsSync(filePath)) return { filePath, settings: {} };
|
|
799
|
+
try {
|
|
800
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
801
|
+
return { filePath, settings: raw.trim() ? JSON.parse(raw) : {} };
|
|
802
|
+
} catch (err) {
|
|
803
|
+
console.error('[PluginsService] Failed to read Claude settings:', err.message);
|
|
804
|
+
return { filePath, settings: {} };
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
_writeClaudeSettings(settings) {
|
|
809
|
+
const filePath = NATIVE_PATHS.claude.settings;
|
|
810
|
+
this._ensureDir(path.dirname(filePath));
|
|
811
|
+
fs.writeFileSync(filePath, JSON.stringify(settings || {}, null, 2), 'utf8');
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
_setClaudePluginEnabled(name, marketplace, enabled) {
|
|
815
|
+
const { settings } = this._readClaudeSettings();
|
|
816
|
+
const nextSettings = (settings && typeof settings === 'object') ? { ...settings } : {};
|
|
817
|
+
const pluginKey = marketplace ? `${name}@${marketplace}` : name;
|
|
818
|
+
nextSettings.enabledPlugins = { ...(nextSettings.enabledPlugins || {}) };
|
|
819
|
+
nextSettings.enabledPlugins[pluginKey] = !!enabled;
|
|
820
|
+
this._writeClaudeSettings(nextSettings);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
_removeClaudePluginEnabled(name, marketplace = '') {
|
|
824
|
+
const { settings } = this._readClaudeSettings();
|
|
825
|
+
const nextSettings = (settings && typeof settings === 'object') ? { ...settings } : {};
|
|
826
|
+
const enabledPlugins = { ...(nextSettings.enabledPlugins || {}) };
|
|
827
|
+
for (const key of Object.keys(enabledPlugins)) {
|
|
828
|
+
const parsed = splitPluginMarketplaceKey(key);
|
|
829
|
+
if (parsed.name === name && (!marketplace || parsed.marketplace === marketplace)) {
|
|
830
|
+
delete enabledPlugins[key];
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
nextSettings.enabledPlugins = enabledPlugins;
|
|
834
|
+
this._writeClaudeSettings(nextSettings);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
_isClaudePluginEnabled(name, marketplace = '') {
|
|
838
|
+
const { settings } = this._readClaudeSettings();
|
|
839
|
+
const enabledPlugins = settings.enabledPlugins || {};
|
|
840
|
+
const pluginKey = marketplace ? `${name}@${marketplace}` : name;
|
|
841
|
+
if (Object.prototype.hasOwnProperty.call(enabledPlugins, pluginKey)) {
|
|
842
|
+
return enabledPlugins[pluginKey] !== false;
|
|
843
|
+
}
|
|
844
|
+
for (const [key, value] of Object.entries(enabledPlugins)) {
|
|
845
|
+
const parsed = splitPluginMarketplaceKey(key);
|
|
846
|
+
if (parsed.name === name) {
|
|
847
|
+
return value !== false;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return true;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
_resolveClaudeMarketplaceName(normalizedRepo, explicit = '') {
|
|
854
|
+
if (explicit) return normalizeClaudeMarketplaceSegment(explicit);
|
|
855
|
+
if (normalizedRepo.marketplace) return normalizeClaudeMarketplaceSegment(normalizedRepo.marketplace);
|
|
856
|
+
if (normalizedRepo.provider === 'local') return normalizeClaudeMarketplaceSegment(normalizedRepo.name || normalizedRepo.localPath, 'local');
|
|
857
|
+
if (normalizedRepo.provider === 'gitlab') return normalizeClaudeMarketplaceSegment(normalizedRepo.name || normalizedRepo.projectPath, 'gitlab');
|
|
858
|
+
return normalizeClaudeMarketplaceSegment(normalizedRepo.name || normalizedRepo.owner || 'ctx');
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
_resolveClaudePluginCachePath(marketplace, pluginName, version) {
|
|
862
|
+
return resolveInsideRoot(
|
|
863
|
+
CLAUDE_PLUGINS_CACHE_DIR,
|
|
864
|
+
path.posix.join(
|
|
865
|
+
normalizeClaudeMarketplaceSegment(marketplace),
|
|
866
|
+
normalizePluginCacheSegment(pluginName, 'Claude plugin name', 'plugin'),
|
|
867
|
+
normalizePluginCacheSegment(String(version || 'local'), 'Claude plugin version', 'local')
|
|
868
|
+
),
|
|
869
|
+
'Claude plugin cache path'
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
_resolveClaudeMarketplacePath(marketplace) {
|
|
874
|
+
return resolveInsideRoot(
|
|
875
|
+
CLAUDE_MARKETPLACES_DIR,
|
|
876
|
+
normalizeClaudeMarketplaceSegment(marketplace),
|
|
877
|
+
'Claude marketplace path'
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
_registerClaudeMarketplace(marketplace, sourceDir, marketplaceInfo = {}) {
|
|
882
|
+
const safeMarketplace = normalizeClaudeMarketplaceSegment(marketplace);
|
|
883
|
+
const marketplaceDir = this._resolveClaudeMarketplacePath(safeMarketplace);
|
|
884
|
+
this._ensureDir(path.join(marketplaceDir, '.claude-plugin'));
|
|
885
|
+
|
|
886
|
+
const plugins = Array.isArray(marketplaceInfo.plugins) ? marketplaceInfo.plugins : [];
|
|
887
|
+
const manifestPath = path.join(marketplaceDir, '.claude-plugin', 'marketplace.json');
|
|
888
|
+
const marketplaceManifest = {
|
|
889
|
+
name: safeMarketplace,
|
|
890
|
+
owner: { name: 'CTX' },
|
|
891
|
+
plugins,
|
|
892
|
+
...(marketplaceInfo.description ? { description: marketplaceInfo.description } : {})
|
|
893
|
+
};
|
|
894
|
+
fs.writeFileSync(manifestPath, JSON.stringify(marketplaceManifest, null, 2), 'utf8');
|
|
895
|
+
|
|
896
|
+
this._ensureDir(CLAUDE_PLUGINS_DIR);
|
|
897
|
+
let known = {};
|
|
898
|
+
if (fs.existsSync(CLAUDE_MARKETPLACES_FILE)) {
|
|
899
|
+
try {
|
|
900
|
+
known = JSON.parse(fs.readFileSync(CLAUDE_MARKETPLACES_FILE, 'utf8'));
|
|
901
|
+
} catch {
|
|
902
|
+
known = {};
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
known[safeMarketplace] = {
|
|
906
|
+
source: {
|
|
907
|
+
source: 'directory',
|
|
908
|
+
path: marketplaceDir
|
|
909
|
+
},
|
|
910
|
+
installLocation: marketplaceDir,
|
|
911
|
+
lastUpdated: new Date().toISOString()
|
|
912
|
+
};
|
|
913
|
+
fs.writeFileSync(CLAUDE_MARKETPLACES_FILE, JSON.stringify(known, null, 2), 'utf8');
|
|
914
|
+
|
|
915
|
+
const { settings } = this._readClaudeSettings();
|
|
916
|
+
const nextSettings = (settings && typeof settings === 'object') ? { ...settings } : {};
|
|
917
|
+
nextSettings.extraKnownMarketplaces = { ...(nextSettings.extraKnownMarketplaces || {}) };
|
|
918
|
+
nextSettings.extraKnownMarketplaces[safeMarketplace] = {
|
|
919
|
+
source: {
|
|
920
|
+
source: 'directory',
|
|
921
|
+
path: marketplaceDir
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
this._writeClaudeSettings(nextSettings);
|
|
925
|
+
return marketplaceDir;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
_upsertClaudeMarketplacePlugin(marketplace, pluginDir, plugin) {
|
|
929
|
+
const marketplaceDir = this._resolveClaudeMarketplacePath(marketplace);
|
|
930
|
+
const marketplaceManifestPath = path.join(marketplaceDir, '.claude-plugin', 'marketplace.json');
|
|
931
|
+
let marketplaceManifest = {
|
|
932
|
+
name: normalizeClaudeMarketplaceSegment(marketplace),
|
|
933
|
+
owner: { name: 'CTX' },
|
|
934
|
+
plugins: []
|
|
935
|
+
};
|
|
936
|
+
if (fs.existsSync(marketplaceManifestPath)) {
|
|
937
|
+
try {
|
|
938
|
+
marketplaceManifest = JSON.parse(fs.readFileSync(marketplaceManifestPath, 'utf8'));
|
|
939
|
+
} catch {
|
|
940
|
+
// recreate malformed marketplace manifest below
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (!Array.isArray(marketplaceManifest.plugins)) {
|
|
944
|
+
marketplaceManifest.plugins = [];
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const pluginName = String(plugin.name || '').trim();
|
|
948
|
+
const existingIndex = marketplaceManifest.plugins.findIndex(item => item?.name === pluginName);
|
|
949
|
+
const nextEntry = {
|
|
950
|
+
name: pluginName,
|
|
951
|
+
source: `./plugins/${pluginName}`,
|
|
952
|
+
description: plugin.description || ''
|
|
953
|
+
};
|
|
954
|
+
if (plugin.category) {
|
|
955
|
+
nextEntry.category = plugin.category;
|
|
956
|
+
}
|
|
957
|
+
if (existingIndex >= 0) {
|
|
958
|
+
marketplaceManifest.plugins[existingIndex] = {
|
|
959
|
+
...marketplaceManifest.plugins[existingIndex],
|
|
960
|
+
...nextEntry
|
|
961
|
+
};
|
|
962
|
+
} else {
|
|
963
|
+
marketplaceManifest.plugins.push(nextEntry);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const marketplacePluginDir = resolveInsideRoot(
|
|
967
|
+
marketplaceDir,
|
|
968
|
+
path.posix.join('plugins', normalizePluginCacheSegment(pluginName, 'Claude plugin name', 'plugin')),
|
|
969
|
+
'Claude marketplace plugin path'
|
|
970
|
+
);
|
|
971
|
+
if (fs.existsSync(marketplacePluginDir)) {
|
|
972
|
+
fs.rmSync(marketplacePluginDir, { recursive: true, force: true });
|
|
973
|
+
}
|
|
974
|
+
this.copyDirRecursive(pluginDir, marketplacePluginDir, {
|
|
975
|
+
excludeNames: [REPO_SOURCE_META_FILE]
|
|
976
|
+
});
|
|
977
|
+
this._registerClaudeMarketplace(marketplace, marketplaceDir, marketplaceManifest);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
_registerClaudeInstalledPlugin(name, marketplace, installPath, installData = {}) {
|
|
981
|
+
this._ensureDir(CLAUDE_PLUGINS_DIR);
|
|
982
|
+
let nativeData = { version: 2, plugins: {} };
|
|
983
|
+
if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
|
|
984
|
+
try {
|
|
985
|
+
nativeData = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
|
|
986
|
+
} catch {
|
|
987
|
+
nativeData = { version: 2, plugins: {} };
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
nativeData.version = nativeData.version || 2;
|
|
991
|
+
if (!nativeData.plugins || typeof nativeData.plugins !== 'object') {
|
|
992
|
+
nativeData.plugins = {};
|
|
993
|
+
}
|
|
994
|
+
const nativeKey = marketplace ? `${name}@${marketplace}` : name;
|
|
995
|
+
const installTimestamp = installData.installedAt || new Date().toISOString();
|
|
996
|
+
nativeData.plugins[nativeKey] = [{
|
|
997
|
+
scope: installData.scope || 'user',
|
|
998
|
+
installPath,
|
|
999
|
+
version: installData.version || '1.0.0',
|
|
1000
|
+
installedAt: installTimestamp,
|
|
1001
|
+
lastUpdated: installData.lastUpdated || installTimestamp,
|
|
1002
|
+
...(installData.source ? { source: installData.source } : {}),
|
|
1003
|
+
...(installData.repoSourceMeta || {})
|
|
1004
|
+
}];
|
|
1005
|
+
fs.writeFileSync(CLAUDE_INSTALLED_FILE, JSON.stringify(nativeData, null, 2), 'utf8');
|
|
1006
|
+
this._setClaudePluginEnabled(name, marketplace, true);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
_isCodexPluginEnabled(name, marketplace = '') {
|
|
1010
|
+
const { config } = this._readCodexConfig();
|
|
1011
|
+
const plugins = config.plugins || {};
|
|
1012
|
+
if (marketplace) {
|
|
1013
|
+
const exact = plugins[`${name}@${marketplace}`];
|
|
1014
|
+
if (exact && typeof exact === 'object' && exact.enabled === false) return false;
|
|
1015
|
+
}
|
|
1016
|
+
for (const [key, value] of Object.entries(plugins)) {
|
|
1017
|
+
const parsed = splitPluginMarketplaceKey(key);
|
|
1018
|
+
if (parsed.name === name && value && typeof value === 'object' && value.enabled === false) {
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return true;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
_setCodexPluginEnabled(name, marketplace, enabled) {
|
|
1026
|
+
const { config } = this._readCodexConfig();
|
|
1027
|
+
const nextConfig = (config && typeof config === 'object') ? { ...config } : {};
|
|
1028
|
+
nextConfig.plugins = { ...(nextConfig.plugins || {}) };
|
|
1029
|
+
const pluginKey = marketplace ? `${name}@${marketplace}` : name;
|
|
1030
|
+
nextConfig.plugins[pluginKey] = {
|
|
1031
|
+
...(nextConfig.plugins[pluginKey] || {}),
|
|
1032
|
+
enabled: !!enabled
|
|
1033
|
+
};
|
|
1034
|
+
this._writeCodexConfig(nextConfig);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
_registerCodexMarketplace(marketplace, source) {
|
|
1038
|
+
const { config } = this._readCodexConfig();
|
|
1039
|
+
const nextConfig = (config && typeof config === 'object') ? { ...config } : {};
|
|
1040
|
+
nextConfig.marketplaces = { ...(nextConfig.marketplaces || {}) };
|
|
1041
|
+
nextConfig.marketplaces[marketplace] = {
|
|
1042
|
+
...(nextConfig.marketplaces[marketplace] || {}),
|
|
1043
|
+
source_type: isLikelyLocalPath(source) ? 'local' : 'git',
|
|
1044
|
+
source,
|
|
1045
|
+
last_updated: new Date().toISOString()
|
|
1046
|
+
};
|
|
1047
|
+
this._writeCodexConfig(nextConfig);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
_unregisterCodexPlugin(name, marketplace = '') {
|
|
1051
|
+
const { config } = this._readCodexConfig();
|
|
1052
|
+
const nextConfig = (config && typeof config === 'object') ? { ...config } : {};
|
|
1053
|
+
const plugins = { ...(nextConfig.plugins || {}) };
|
|
1054
|
+
for (const key of Object.keys(plugins)) {
|
|
1055
|
+
const parsed = splitPluginMarketplaceKey(key);
|
|
1056
|
+
if (parsed.name === name && (!marketplace || parsed.marketplace === marketplace)) {
|
|
1057
|
+
delete plugins[key];
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
nextConfig.plugins = plugins;
|
|
1061
|
+
this._writeCodexConfig(nextConfig);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
_resolveCodexMarketplaceName(repo, explicit = '') {
|
|
1065
|
+
if (explicit) return slugifyCodexKey(explicit);
|
|
1066
|
+
if (repo.marketplace) return slugifyCodexKey(repo.marketplace);
|
|
1067
|
+
if (repo.provider === 'local') return slugifyCodexKey(repo.localPath, 'local');
|
|
1068
|
+
if (repo.provider === 'gitlab') return slugifyCodexKey(repo.projectPath, 'gitlab');
|
|
1069
|
+
return slugifyCodexKey(repo.name || repo.owner || 'github');
|
|
1070
|
+
}
|
|
1071
|
+
|
|
542
1072
|
/**
|
|
543
1073
|
* List all installed plugins with their status
|
|
544
1074
|
* Reads from Claude Code's native installed_plugins.json
|
|
545
1075
|
* @returns {Object} { plugins: Array }
|
|
546
1076
|
*/
|
|
547
1077
|
listPlugins() {
|
|
1078
|
+
if (!this._pluginsSupported()) {
|
|
1079
|
+
return { plugins: [] };
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (this._isCodex()) {
|
|
1083
|
+
return { plugins: this._listCodexCachedPlugins() };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
548
1086
|
if (this._isOpenCode()) {
|
|
549
1087
|
const plugins = [];
|
|
550
1088
|
const seen = new Set();
|
|
@@ -584,7 +1122,7 @@ class PluginsService {
|
|
|
584
1122
|
for (const [key, installations] of Object.entries(data.plugins)) {
|
|
585
1123
|
if (installations && installations.length > 0) {
|
|
586
1124
|
const install = installations[0]; // Get first installation
|
|
587
|
-
const
|
|
1125
|
+
const { name, marketplace } = splitPluginMarketplaceKey(key);
|
|
588
1126
|
|
|
589
1127
|
// Read plugin.json from installPath for description
|
|
590
1128
|
let description = '';
|
|
@@ -637,7 +1175,8 @@ class PluginsService {
|
|
|
637
1175
|
|
|
638
1176
|
// Read enabled state from CTX registry (defaults to true if not set)
|
|
639
1177
|
const legacyInfo = getPlugin(name);
|
|
640
|
-
const enabledState =
|
|
1178
|
+
const enabledState = this._isClaudePluginEnabled(name, marketplace) &&
|
|
1179
|
+
(legacyInfo ? legacyInfo.enabled !== false : true);
|
|
641
1180
|
|
|
642
1181
|
plugins.push({
|
|
643
1182
|
name,
|
|
@@ -690,6 +1229,22 @@ class PluginsService {
|
|
|
690
1229
|
* @returns {Object|null} Plugin details or null
|
|
691
1230
|
*/
|
|
692
1231
|
getPlugin(name) {
|
|
1232
|
+
if (this._isCodex()) {
|
|
1233
|
+
const plugin = this.listPlugins().plugins.find(p => p.name === name || `${p.name}@${p.marketplace}` === name);
|
|
1234
|
+
if (!plugin) return null;
|
|
1235
|
+
const manifest = plugin.installPath
|
|
1236
|
+
? this._readLocalManifest(plugin.installPath, ['.codex-plugin/plugin.json', 'plugin.json', 'package.json'])
|
|
1237
|
+
: null;
|
|
1238
|
+
return {
|
|
1239
|
+
name: plugin.name,
|
|
1240
|
+
...plugin,
|
|
1241
|
+
author: manifest?.author || '',
|
|
1242
|
+
commands: manifest?.commands || [],
|
|
1243
|
+
hooks: manifest?.hooks || [],
|
|
1244
|
+
manifest
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
|
|
693
1248
|
if (this._isOpenCode()) {
|
|
694
1249
|
const plugin = this.listPlugins().plugins.find(p => p.name === name || p.directory === name);
|
|
695
1250
|
if (!plugin) return null;
|
|
@@ -731,8 +1286,36 @@ class PluginsService {
|
|
|
731
1286
|
* @returns {Promise<Object>} Installation result
|
|
732
1287
|
*/
|
|
733
1288
|
async installPlugin(source, repoInfo = null) {
|
|
1289
|
+
if (this._isCodex()) {
|
|
1290
|
+
if (hasRepoInstallInfo(repoInfo)) {
|
|
1291
|
+
return this._installFromRepoDirectory(repoInfo);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const parsedSource = this.parseRepoTreeSource(source);
|
|
1295
|
+
if (parsedSource) {
|
|
1296
|
+
return this._installFromRepoDirectory(parsedSource);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const parsedRepo = this._repoFromGitUrl(source, 'main');
|
|
1300
|
+
if (parsedRepo) {
|
|
1301
|
+
return this._installFromRepoDirectory({ ...parsedRepo, directory: '' });
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
return {
|
|
1305
|
+
success: false,
|
|
1306
|
+
error: 'Codex plugin install expects repository metadata, a GitHub/GitLab tree URL, or a Git repository URL'
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
if (!this._pluginsSupported()) {
|
|
1311
|
+
return {
|
|
1312
|
+
success: false,
|
|
1313
|
+
error: `${this.platform} plugin management is not supported`
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
|
|
734
1317
|
if (this._isOpenCode()) {
|
|
735
|
-
if (repoInfo
|
|
1318
|
+
if (hasRepoInstallInfo(repoInfo)) {
|
|
736
1319
|
return this._installFromRepoDirectory(repoInfo, { installRoot: this._getOpenCodePluginsDir() });
|
|
737
1320
|
}
|
|
738
1321
|
|
|
@@ -741,6 +1324,11 @@ class PluginsService {
|
|
|
741
1324
|
return this._installFromRepoDirectory(parsedSource, { installRoot: this._getOpenCodePluginsDir() });
|
|
742
1325
|
}
|
|
743
1326
|
|
|
1327
|
+
const parsedRepo = this._repoFromGitUrl(source, 'main');
|
|
1328
|
+
if (parsedRepo) {
|
|
1329
|
+
return this._installFromRepoDirectory({ ...parsedRepo, directory: '' }, { installRoot: this._getOpenCodePluginsDir() });
|
|
1330
|
+
}
|
|
1331
|
+
|
|
744
1332
|
// OpenCode 原生支持 npm 包名,通过 opencode.json 的 plugin 数组管理
|
|
745
1333
|
if (!/^https?:\/\//.test(source)) {
|
|
746
1334
|
const plugins = this._listOpenCodeConfiguredPlugins();
|
|
@@ -760,7 +1348,7 @@ class PluginsService {
|
|
|
760
1348
|
};
|
|
761
1349
|
}
|
|
762
1350
|
|
|
763
|
-
if (repoInfo
|
|
1351
|
+
if (hasRepoInstallInfo(repoInfo)) {
|
|
764
1352
|
return await this._installFromRepoDirectory(repoInfo);
|
|
765
1353
|
}
|
|
766
1354
|
|
|
@@ -769,6 +1357,11 @@ class PluginsService {
|
|
|
769
1357
|
return await this._installFromRepoDirectory(parsedSource);
|
|
770
1358
|
}
|
|
771
1359
|
|
|
1360
|
+
const parsedRepo = this._repoFromGitUrl(source, 'main');
|
|
1361
|
+
if (parsedRepo) {
|
|
1362
|
+
return await this._installFromRepoDirectory({ ...parsedRepo, directory: '' });
|
|
1363
|
+
}
|
|
1364
|
+
|
|
772
1365
|
// Fallback to original git clone method
|
|
773
1366
|
return await installPluginCore(source);
|
|
774
1367
|
}
|
|
@@ -779,30 +1372,48 @@ class PluginsService {
|
|
|
779
1372
|
*/
|
|
780
1373
|
async _installFromRepoDirectory(repoInfo, options = {}) {
|
|
781
1374
|
const normalizedRepo = this.normalizeRepoConfig(repoInfo);
|
|
782
|
-
const directory =
|
|
783
|
-
const pluginName = directory.split('/').pop();
|
|
784
|
-
const
|
|
1375
|
+
const directory = normalizePluginRepoDirectory(repoInfo.directory || normalizedRepo.directory || '');
|
|
1376
|
+
const pluginName = directory.split('/').filter(Boolean).pop() || slugifyCodexKey(normalizedRepo.name, 'plugin');
|
|
1377
|
+
const manifestCandidates = this._getManifestCandidates(directory);
|
|
785
1378
|
|
|
786
1379
|
try {
|
|
787
1380
|
let manifest;
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
} catch {
|
|
1381
|
+
let manifestPath = '';
|
|
1382
|
+
for (const candidate of manifestCandidates) {
|
|
791
1383
|
try {
|
|
792
|
-
|
|
1384
|
+
manifestPath = joinRepoPath(directory, candidate);
|
|
1385
|
+
manifest = await this.fetchRepoJson(normalizedRepo, manifestPath);
|
|
1386
|
+
break;
|
|
793
1387
|
} catch {
|
|
794
|
-
|
|
1388
|
+
manifestPath = '';
|
|
795
1389
|
}
|
|
796
1390
|
}
|
|
1391
|
+
if (!manifest) {
|
|
1392
|
+
manifest = { name: pluginName, version: '1.0.0' };
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const installedPluginName = String(manifest.name || pluginName || 'plugin').trim() || 'plugin';
|
|
1396
|
+
const installRoot = this._resolvePluginInstallRoot(normalizedRepo, repoInfo, manifest, options);
|
|
797
1397
|
|
|
798
1398
|
// Create plugin directory
|
|
799
|
-
const pluginDir =
|
|
1399
|
+
const pluginDir = this._isCodex()
|
|
1400
|
+
? installRoot
|
|
1401
|
+
: this.platform === 'claude'
|
|
1402
|
+
? installRoot
|
|
1403
|
+
: resolveInsideRoot(
|
|
1404
|
+
installRoot,
|
|
1405
|
+
normalizePluginPathName(installedPluginName, 'plugin name'),
|
|
1406
|
+
'Plugin install path',
|
|
1407
|
+
{ allowHiddenSegments: false }
|
|
1408
|
+
);
|
|
800
1409
|
if (!fs.existsSync(pluginDir)) {
|
|
801
1410
|
fs.mkdirSync(pluginDir, { recursive: true });
|
|
802
1411
|
}
|
|
803
1412
|
|
|
804
1413
|
if (normalizedRepo.provider === 'local') {
|
|
805
|
-
const sourceDir =
|
|
1414
|
+
const sourceDir = directory
|
|
1415
|
+
? resolveInsideRoot(normalizedRepo.localPath, directory, 'Plugin directory', { allowHiddenSegments: true })
|
|
1416
|
+
: normalizedRepo.localPath;
|
|
806
1417
|
if (!fs.existsSync(sourceDir)) {
|
|
807
1418
|
throw new Error(`Plugin directory not found: ${directory}`);
|
|
808
1419
|
}
|
|
@@ -843,7 +1454,10 @@ class PluginsService {
|
|
|
843
1454
|
throw new Error('Empty archive');
|
|
844
1455
|
}
|
|
845
1456
|
|
|
846
|
-
const
|
|
1457
|
+
const extractedRoot = path.join(tempDir, extractedDir);
|
|
1458
|
+
const sourceDir = directory
|
|
1459
|
+
? resolveInsideRoot(extractedRoot, directory, 'Plugin directory', { allowHiddenSegments: true })
|
|
1460
|
+
: extractedRoot;
|
|
847
1461
|
if (!fs.existsSync(sourceDir)) {
|
|
848
1462
|
throw new Error(`Plugin directory not found: ${directory}`);
|
|
849
1463
|
}
|
|
@@ -854,15 +1468,43 @@ class PluginsService {
|
|
|
854
1468
|
}
|
|
855
1469
|
|
|
856
1470
|
// Write plugin.json if not exists
|
|
857
|
-
const
|
|
858
|
-
if (!fs.existsSync(
|
|
859
|
-
|
|
1471
|
+
const fallbackManifestPath = path.join(pluginDir, this._isCodex() ? '.codex-plugin/plugin.json' : 'plugin.json');
|
|
1472
|
+
if (!fs.existsSync(fallbackManifestPath)) {
|
|
1473
|
+
this._ensureDir(path.dirname(fallbackManifestPath));
|
|
1474
|
+
fs.writeFileSync(fallbackManifestPath, JSON.stringify(manifest, null, 2));
|
|
1475
|
+
}
|
|
1476
|
+
if (this.platform === 'claude') {
|
|
1477
|
+
const claudeManifestPath = path.join(pluginDir, '.claude-plugin', 'plugin.json');
|
|
1478
|
+
if (!fs.existsSync(claudeManifestPath)) {
|
|
1479
|
+
this._ensureDir(path.dirname(claudeManifestPath));
|
|
1480
|
+
fs.writeFileSync(claudeManifestPath, JSON.stringify(manifest, null, 2));
|
|
1481
|
+
}
|
|
860
1482
|
}
|
|
861
1483
|
|
|
862
|
-
if (
|
|
863
|
-
const
|
|
1484
|
+
if (this._isCodex()) {
|
|
1485
|
+
const marketplace = this._resolveCodexMarketplaceName(normalizedRepo, repoInfo.marketplace);
|
|
1486
|
+
const sourceUrl = this.buildRepoBrowserUrl(normalizedRepo, directory) || buildRepoUrl(normalizedRepo);
|
|
1487
|
+
this.writeRepoSourceMeta(pluginDir, {
|
|
1488
|
+
repoId: normalizedRepo.id,
|
|
1489
|
+
repoProvider: normalizedRepo.provider,
|
|
1490
|
+
repoOwner: normalizedRepo.owner || '',
|
|
1491
|
+
repoName: normalizedRepo.name || '',
|
|
1492
|
+
repoBranch: normalizedRepo.branch,
|
|
1493
|
+
repoDirectory: directory,
|
|
1494
|
+
repoHost: normalizedRepo.host || '',
|
|
1495
|
+
repoProjectPath: normalizedRepo.projectPath || '',
|
|
1496
|
+
repoLocalPath: normalizedRepo.localPath || '',
|
|
1497
|
+
repoUrl: normalizedRepo.repoUrl || buildRepoUrl(normalizedRepo),
|
|
1498
|
+
marketplace,
|
|
1499
|
+
manifestPath,
|
|
1500
|
+
source: sourceUrl
|
|
1501
|
+
});
|
|
1502
|
+
this._registerCodexMarketplace(marketplace, normalizedRepo.repoUrl || buildRepoUrl(normalizedRepo));
|
|
1503
|
+
this._setCodexPluginEnabled(installedPluginName, marketplace, true);
|
|
1504
|
+
} else if (this.platform === 'claude') {
|
|
864
1505
|
const installTimestamp = new Date().toISOString();
|
|
865
1506
|
const sourceUrl = this.buildRepoBrowserUrl(normalizedRepo, directory) || buildRepoUrl(normalizedRepo);
|
|
1507
|
+
const marketplace = this._resolveClaudeMarketplaceName(normalizedRepo, repoInfo.marketplace);
|
|
866
1508
|
const repoSourceMeta = {
|
|
867
1509
|
repoId: normalizedRepo.id,
|
|
868
1510
|
repoProvider: normalizedRepo.provider,
|
|
@@ -874,6 +1516,7 @@ class PluginsService {
|
|
|
874
1516
|
repoProjectPath: normalizedRepo.projectPath || '',
|
|
875
1517
|
repoLocalPath: normalizedRepo.localPath || '',
|
|
876
1518
|
repoUrl: normalizedRepo.repoUrl || buildRepoUrl(normalizedRepo),
|
|
1519
|
+
repoMarketplace: marketplace,
|
|
877
1520
|
source: sourceUrl
|
|
878
1521
|
};
|
|
879
1522
|
|
|
@@ -891,30 +1534,54 @@ class PluginsService {
|
|
|
891
1534
|
}
|
|
892
1535
|
|
|
893
1536
|
this.writeRepoSourceMeta(pluginDir, repoSourceMeta);
|
|
1537
|
+
this._upsertClaudeMarketplacePlugin(marketplace, pluginDir, {
|
|
1538
|
+
name: installedPluginName,
|
|
1539
|
+
description: manifest.description || '',
|
|
1540
|
+
category: manifest.category || ''
|
|
1541
|
+
});
|
|
894
1542
|
|
|
895
1543
|
// Also register in Claude's native installed_plugins.json
|
|
896
1544
|
try {
|
|
897
|
-
this.
|
|
898
|
-
let nativeData = { plugins: {} };
|
|
899
|
-
if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
|
|
900
|
-
try {
|
|
901
|
-
nativeData = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
|
|
902
|
-
if (!nativeData.plugins) nativeData.plugins = {};
|
|
903
|
-
} catch (e) { /* ignore parse error */ }
|
|
904
|
-
}
|
|
905
|
-
const nativeKey = `${installedPluginName}@ctx`;
|
|
906
|
-
nativeData.plugins[nativeKey] = [{
|
|
1545
|
+
this._registerClaudeInstalledPlugin(installedPluginName, marketplace, pluginDir, {
|
|
907
1546
|
version: manifest.version || '1.0.0',
|
|
908
|
-
installPath: pluginDir,
|
|
909
1547
|
installedAt: installTimestamp,
|
|
910
1548
|
scope: 'user',
|
|
911
1549
|
source: sourceUrl,
|
|
912
|
-
|
|
913
|
-
}
|
|
914
|
-
fs.writeFileSync(CLAUDE_INSTALLED_FILE, JSON.stringify(nativeData, null, 2), 'utf8');
|
|
1550
|
+
repoSourceMeta
|
|
1551
|
+
});
|
|
915
1552
|
} catch (e) {
|
|
916
1553
|
console.error('[PluginsService] Failed to update native installed_plugins.json:', e.message);
|
|
917
1554
|
}
|
|
1555
|
+
} else if (!this._isOpenCode()) {
|
|
1556
|
+
const installTimestamp = new Date().toISOString();
|
|
1557
|
+
const sourceUrl = this.buildRepoBrowserUrl(normalizedRepo, directory) || buildRepoUrl(normalizedRepo);
|
|
1558
|
+
const repoSourceMeta = {
|
|
1559
|
+
repoId: normalizedRepo.id,
|
|
1560
|
+
repoProvider: normalizedRepo.provider,
|
|
1561
|
+
repoOwner: normalizedRepo.owner || '',
|
|
1562
|
+
repoName: normalizedRepo.name || '',
|
|
1563
|
+
repoBranch: normalizedRepo.branch,
|
|
1564
|
+
repoDirectory: directory,
|
|
1565
|
+
repoHost: normalizedRepo.host || '',
|
|
1566
|
+
repoProjectPath: normalizedRepo.projectPath || '',
|
|
1567
|
+
repoLocalPath: normalizedRepo.localPath || '',
|
|
1568
|
+
repoUrl: normalizedRepo.repoUrl || buildRepoUrl(normalizedRepo),
|
|
1569
|
+
source: sourceUrl
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1572
|
+
const { addPlugin } = require('../../plugins/registry');
|
|
1573
|
+
try {
|
|
1574
|
+
addPlugin(installedPluginName, {
|
|
1575
|
+
version: manifest.version || '1.0.0',
|
|
1576
|
+
enabled: true,
|
|
1577
|
+
installedAt: installTimestamp,
|
|
1578
|
+
source: sourceUrl
|
|
1579
|
+
});
|
|
1580
|
+
} catch (e) {
|
|
1581
|
+
console.warn('[PluginsService] Legacy registry addPlugin warning:', e.message);
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
this.writeRepoSourceMeta(pluginDir, repoSourceMeta);
|
|
918
1585
|
}
|
|
919
1586
|
|
|
920
1587
|
return {
|
|
@@ -933,6 +1600,49 @@ class PluginsService {
|
|
|
933
1600
|
}
|
|
934
1601
|
}
|
|
935
1602
|
|
|
1603
|
+
_getManifestCandidates() {
|
|
1604
|
+
if (this._isCodex()) {
|
|
1605
|
+
return ['.codex-plugin/plugin.json', 'plugin.json', 'package.json'];
|
|
1606
|
+
}
|
|
1607
|
+
if (this._isOpenCode()) {
|
|
1608
|
+
return ['package.json', 'plugin.json'];
|
|
1609
|
+
}
|
|
1610
|
+
return ['.claude-plugin/plugin.json', 'plugin.json', 'package.json'];
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
_resolvePluginInstallRoot(normalizedRepo, repoInfo, manifest, options = {}) {
|
|
1614
|
+
if (!this._isCodex()) {
|
|
1615
|
+
if (this.platform === 'claude') {
|
|
1616
|
+
const marketplace = this._resolveClaudeMarketplaceName(normalizedRepo, repoInfo.marketplace);
|
|
1617
|
+
const pluginName = manifest.name || normalizeRepoPath(repoInfo.directory || '').split('/').pop() || 'plugin';
|
|
1618
|
+
const version = manifest.version || repoInfo.version || 'local';
|
|
1619
|
+
return this._resolveClaudePluginCachePath(marketplace, pluginName, version);
|
|
1620
|
+
}
|
|
1621
|
+
return options.installRoot || INSTALLED_DIR;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
const marketplace = normalizePluginCacheSegment(
|
|
1625
|
+
this._resolveCodexMarketplaceName(normalizedRepo, repoInfo.marketplace),
|
|
1626
|
+
'Codex marketplace',
|
|
1627
|
+
'local'
|
|
1628
|
+
);
|
|
1629
|
+
const pluginName = normalizePluginCacheSegment(
|
|
1630
|
+
manifest.name || normalizeRepoPath(repoInfo.directory || '').split('/').pop() || 'plugin',
|
|
1631
|
+
'Codex plugin name',
|
|
1632
|
+
'plugin'
|
|
1633
|
+
);
|
|
1634
|
+
const version = normalizePluginCacheSegment(
|
|
1635
|
+
String(manifest.version || repoInfo.version || 'local').trim() || 'local',
|
|
1636
|
+
'Codex plugin version',
|
|
1637
|
+
'local'
|
|
1638
|
+
);
|
|
1639
|
+
return resolveInsideRoot(
|
|
1640
|
+
this.codexPluginsCacheDir,
|
|
1641
|
+
path.posix.join(marketplace, pluginName, version),
|
|
1642
|
+
'Codex plugin cache path'
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
936
1646
|
/**
|
|
937
1647
|
* Parse GitHub/GitLab tree URL or local path
|
|
938
1648
|
* @private
|
|
@@ -986,13 +1696,56 @@ class PluginsService {
|
|
|
986
1696
|
* @returns {Object} Uninstallation result
|
|
987
1697
|
*/
|
|
988
1698
|
uninstallPlugin(name) {
|
|
1699
|
+
if (this._isCodex()) {
|
|
1700
|
+
const plugins = this.listPlugins().plugins;
|
|
1701
|
+
const target = plugins.find(plugin => plugin.name === name || `${plugin.name}@${plugin.marketplace}` === name);
|
|
1702
|
+
if (!target) {
|
|
1703
|
+
return {
|
|
1704
|
+
success: false,
|
|
1705
|
+
error: `Plugin "${name}" not found`
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
const pluginRoot = target.installPath
|
|
1710
|
+
? assertInsideAllowedRoots(
|
|
1711
|
+
path.dirname(assertInsideAllowedRoots(target.installPath, [this.codexPluginsCacheDir], 'Codex plugin install path')),
|
|
1712
|
+
[this.codexPluginsCacheDir],
|
|
1713
|
+
'Codex plugin root'
|
|
1714
|
+
)
|
|
1715
|
+
: resolveInsideRoot(
|
|
1716
|
+
this.codexPluginsCacheDir,
|
|
1717
|
+
path.posix.join(
|
|
1718
|
+
normalizePluginCacheSegment(target.marketplace || '', 'Codex marketplace', 'local'),
|
|
1719
|
+
normalizePluginCacheSegment(target.name || name, 'Codex plugin name', 'plugin')
|
|
1720
|
+
),
|
|
1721
|
+
'Codex plugin root'
|
|
1722
|
+
);
|
|
1723
|
+
if (pluginRoot && fs.existsSync(pluginRoot)) {
|
|
1724
|
+
fs.rmSync(pluginRoot, { recursive: true, force: true });
|
|
1725
|
+
}
|
|
1726
|
+
this._unregisterCodexPlugin(target.name, target.marketplace || '');
|
|
1727
|
+
|
|
1728
|
+
return {
|
|
1729
|
+
success: true,
|
|
1730
|
+
message: 'Plugin uninstalled successfully'
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
if (!this._pluginsSupported()) {
|
|
1735
|
+
return {
|
|
1736
|
+
success: false,
|
|
1737
|
+
error: `${this.platform} plugin management is not supported`
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
|
|
989
1741
|
if (this._isOpenCode()) {
|
|
1742
|
+
const safeName = normalizePluginPathName(name, 'plugin name');
|
|
990
1743
|
const pluginsDir = this._getOpenCodePluginsDir();
|
|
991
1744
|
let removed = false;
|
|
992
1745
|
|
|
993
1746
|
// Remove from opencode config.plugin (npm plugins)
|
|
994
1747
|
const configured = this._listOpenCodeConfiguredPlugins();
|
|
995
|
-
const next = configured.filter(p => p !==
|
|
1748
|
+
const next = configured.filter(p => p !== safeName);
|
|
996
1749
|
if (next.length !== configured.length) {
|
|
997
1750
|
this._setOpenCodeConfiguredPlugins(next);
|
|
998
1751
|
removed = true;
|
|
@@ -1000,7 +1753,7 @@ class PluginsService {
|
|
|
1000
1753
|
|
|
1001
1754
|
// Remove local plugin directory/file
|
|
1002
1755
|
if (fs.existsSync(pluginsDir)) {
|
|
1003
|
-
const directPath =
|
|
1756
|
+
const directPath = resolveInsideRoot(pluginsDir, safeName, 'OpenCode plugin path');
|
|
1004
1757
|
if (fs.existsSync(directPath)) {
|
|
1005
1758
|
fs.rmSync(directPath, { recursive: true, force: true });
|
|
1006
1759
|
removed = true;
|
|
@@ -1008,8 +1761,9 @@ class PluginsService {
|
|
|
1008
1761
|
const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
|
|
1009
1762
|
for (const entry of entries) {
|
|
1010
1763
|
const baseName = entry.name.replace(path.extname(entry.name), '');
|
|
1011
|
-
if (entry.name ===
|
|
1012
|
-
|
|
1764
|
+
if (entry.name === safeName || baseName === safeName) {
|
|
1765
|
+
const entryPath = resolveInsideRoot(pluginsDir, entry.name, 'OpenCode plugin path');
|
|
1766
|
+
fs.rmSync(entryPath, { recursive: true, force: true });
|
|
1013
1767
|
removed = true;
|
|
1014
1768
|
break;
|
|
1015
1769
|
}
|
|
@@ -1025,22 +1779,28 @@ class PluginsService {
|
|
|
1025
1779
|
|
|
1026
1780
|
// Claude: Remove from native installed_plugins.json and delete install directories
|
|
1027
1781
|
let removed = false;
|
|
1782
|
+
const safeName = normalizePluginPathName(name, 'plugin name');
|
|
1028
1783
|
if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
|
|
1029
1784
|
try {
|
|
1030
1785
|
const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
|
|
1031
1786
|
if (data.plugins) {
|
|
1032
1787
|
const keysToDelete = [];
|
|
1033
|
-
const baseName =
|
|
1788
|
+
const baseName = safeName.split('/').pop(); // handle "plugins/pr-review-toolkit" → "pr-review-toolkit"
|
|
1034
1789
|
for (const [key, installations] of Object.entries(data.plugins)) {
|
|
1035
|
-
const
|
|
1036
|
-
if (pluginName ===
|
|
1790
|
+
const { name: pluginName } = splitPluginMarketplaceKey(key);
|
|
1791
|
+
if (pluginName === safeName || key === safeName || pluginName === baseName) {
|
|
1037
1792
|
keysToDelete.push(key);
|
|
1038
1793
|
// Delete install directories
|
|
1039
1794
|
if (Array.isArray(installations)) {
|
|
1040
1795
|
for (const install of installations) {
|
|
1041
1796
|
if (install.installPath && fs.existsSync(install.installPath)) {
|
|
1042
1797
|
try {
|
|
1043
|
-
|
|
1798
|
+
const installPath = assertInsideAllowedRoots(
|
|
1799
|
+
install.installPath,
|
|
1800
|
+
[INSTALLED_DIR, CLAUDE_PLUGINS_DIR],
|
|
1801
|
+
'Claude plugin install path'
|
|
1802
|
+
);
|
|
1803
|
+
fs.rmSync(installPath, { recursive: true, force: true });
|
|
1044
1804
|
} catch (e) {
|
|
1045
1805
|
console.error('[PluginsService] Failed to delete install dir:', e.message);
|
|
1046
1806
|
}
|
|
@@ -1051,6 +1811,8 @@ class PluginsService {
|
|
|
1051
1811
|
}
|
|
1052
1812
|
if (keysToDelete.length > 0) {
|
|
1053
1813
|
for (const key of keysToDelete) {
|
|
1814
|
+
const parsed = splitPluginMarketplaceKey(key);
|
|
1815
|
+
this._removeClaudePluginEnabled(parsed.name || safeName, parsed.marketplace || '');
|
|
1054
1816
|
delete data.plugins[key];
|
|
1055
1817
|
}
|
|
1056
1818
|
fs.writeFileSync(CLAUDE_INSTALLED_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
@@ -1090,6 +1852,24 @@ class PluginsService {
|
|
|
1090
1852
|
* @returns {Object} Updated plugin info
|
|
1091
1853
|
*/
|
|
1092
1854
|
togglePlugin(name, enabled) {
|
|
1855
|
+
if (this._isCodex()) {
|
|
1856
|
+
const plugins = this.listPlugins().plugins;
|
|
1857
|
+
const target = plugins.find(plugin => plugin.name === name || `${plugin.name}@${plugin.marketplace}` === name);
|
|
1858
|
+
if (!target) {
|
|
1859
|
+
throw new Error(`Plugin "${name}" not found`);
|
|
1860
|
+
}
|
|
1861
|
+
this._setCodexPluginEnabled(target.name, target.marketplace || '', enabled);
|
|
1862
|
+
return {
|
|
1863
|
+
name: target.name,
|
|
1864
|
+
enabled,
|
|
1865
|
+
success: true
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
if (!this._pluginsSupported()) {
|
|
1870
|
+
throw new Error(`${this.platform} plugin management is not supported`);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1093
1873
|
if (this._isOpenCode()) {
|
|
1094
1874
|
const configured = this._listOpenCodeConfiguredPlugins();
|
|
1095
1875
|
const exists = configured.includes(name);
|
|
@@ -1115,7 +1895,7 @@ class PluginsService {
|
|
|
1115
1895
|
const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
|
|
1116
1896
|
if (data.plugins) {
|
|
1117
1897
|
for (const key of Object.keys(data.plugins)) {
|
|
1118
|
-
const
|
|
1898
|
+
const { name: pluginName } = splitPluginMarketplaceKey(key);
|
|
1119
1899
|
if (pluginName === name || key === name || pluginName === baseName) {
|
|
1120
1900
|
pluginExists = true;
|
|
1121
1901
|
break;
|
|
@@ -1134,6 +1914,7 @@ class PluginsService {
|
|
|
1134
1914
|
}
|
|
1135
1915
|
|
|
1136
1916
|
// Store enabled state in CTX registry (upsert)
|
|
1917
|
+
let matchedMarketplace = '';
|
|
1137
1918
|
try {
|
|
1138
1919
|
const { addPlugin } = require('../../plugins/registry');
|
|
1139
1920
|
if (legacyPlugin) {
|
|
@@ -1144,6 +1925,21 @@ class PluginsService {
|
|
|
1144
1925
|
} catch (e) {
|
|
1145
1926
|
console.warn('[PluginsService] Failed to update plugin registry:', e.message);
|
|
1146
1927
|
}
|
|
1928
|
+
if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
|
|
1929
|
+
try {
|
|
1930
|
+
const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
|
|
1931
|
+
for (const key of Object.keys(data.plugins || {})) {
|
|
1932
|
+
const parsed = splitPluginMarketplaceKey(key);
|
|
1933
|
+
if (parsed.name === name || key === name || parsed.name === baseName) {
|
|
1934
|
+
matchedMarketplace = parsed.marketplace || '';
|
|
1935
|
+
break;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
} catch {
|
|
1939
|
+
// ignore
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
this._setClaudePluginEnabled(baseName || name, matchedMarketplace, enabled);
|
|
1147
1943
|
|
|
1148
1944
|
return {
|
|
1149
1945
|
name,
|
|
@@ -1159,10 +1955,18 @@ class PluginsService {
|
|
|
1159
1955
|
* @returns {Object} Result
|
|
1160
1956
|
*/
|
|
1161
1957
|
updatePluginConfig(name, config) {
|
|
1958
|
+
if (this._isCodex()) {
|
|
1959
|
+
throw new Error('Codex cached plugins do not support config updates');
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
if (!this._pluginsSupported()) {
|
|
1963
|
+
throw new Error(`${this.platform} plugin management is not supported`);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1162
1966
|
if (this._isOpenCode()) {
|
|
1163
1967
|
const configDir = path.join(OPENCODE_CONFIG_DIR, 'plugins-config');
|
|
1164
|
-
|
|
1165
|
-
|
|
1968
|
+
const configFile = resolvePluginConfigFile(configDir, name);
|
|
1969
|
+
this._ensureDir(path.dirname(configFile));
|
|
1166
1970
|
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), 'utf8');
|
|
1167
1971
|
return {
|
|
1168
1972
|
success: true,
|
|
@@ -1175,11 +1979,11 @@ class PluginsService {
|
|
|
1175
1979
|
throw new Error(`Plugin "${name}" not found`);
|
|
1176
1980
|
}
|
|
1177
1981
|
|
|
1178
|
-
const configFile =
|
|
1982
|
+
const configFile = resolvePluginConfigFile(CONFIG_DIR, name);
|
|
1179
1983
|
|
|
1180
1984
|
// Ensure config directory exists
|
|
1181
|
-
if (!fs.existsSync(
|
|
1182
|
-
fs.mkdirSync(
|
|
1985
|
+
if (!fs.existsSync(path.dirname(configFile))) {
|
|
1986
|
+
fs.mkdirSync(path.dirname(configFile), { recursive: true });
|
|
1183
1987
|
}
|
|
1184
1988
|
|
|
1185
1989
|
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), 'utf8');
|
|
@@ -1195,7 +1999,7 @@ class PluginsService {
|
|
|
1195
1999
|
* @returns {string} Config file path
|
|
1196
2000
|
*/
|
|
1197
2001
|
getReposConfigPath() {
|
|
1198
|
-
const filePath =
|
|
2002
|
+
const filePath = PATHS.pluginRepos[this.platform] || PATHS.pluginRepos.claude;
|
|
1199
2003
|
this._ensureDir(path.dirname(filePath));
|
|
1200
2004
|
return filePath;
|
|
1201
2005
|
}
|
|
@@ -1242,6 +2046,10 @@ class PluginsService {
|
|
|
1242
2046
|
* @returns {Array} Repos list
|
|
1243
2047
|
*/
|
|
1244
2048
|
getRepos() {
|
|
2049
|
+
if (!this.getCapabilities().repositories) {
|
|
2050
|
+
return [];
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1245
2053
|
const repos = [];
|
|
1246
2054
|
const seenRepos = new Set();
|
|
1247
2055
|
const pushRepo = (repo) => {
|
|
@@ -1271,7 +2079,7 @@ class PluginsService {
|
|
|
1271
2079
|
}
|
|
1272
2080
|
|
|
1273
2081
|
// 2. Load Claude Code's native marketplace config (Claude only)
|
|
1274
|
-
if (
|
|
2082
|
+
if (this.platform === 'claude' && fs.existsSync(CLAUDE_MARKETPLACES_FILE)) {
|
|
1275
2083
|
try {
|
|
1276
2084
|
const marketplaces = JSON.parse(fs.readFileSync(CLAUDE_MARKETPLACES_FILE, 'utf8'));
|
|
1277
2085
|
const entries = [];
|
|
@@ -1644,11 +2452,10 @@ class PluginsService {
|
|
|
1644
2452
|
scanLocalRepoTree(currentDir, repoRoot, tree) {
|
|
1645
2453
|
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
1646
2454
|
for (const entry of entries) {
|
|
1647
|
-
if (entry.name.startsWith('.')) continue;
|
|
1648
2455
|
const fullPath = path.join(currentDir, entry.name);
|
|
1649
2456
|
const relativePath = normalizeRepoPath(path.relative(repoRoot, fullPath));
|
|
1650
2457
|
if (entry.isDirectory()) {
|
|
1651
|
-
if (entry.name === 'node_modules') continue;
|
|
2458
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
1652
2459
|
tree.push({ path: relativePath, type: 'tree', name: entry.name });
|
|
1653
2460
|
this.scanLocalRepoTree(fullPath, repoRoot, tree);
|
|
1654
2461
|
} else {
|
|
@@ -1704,7 +2511,12 @@ class PluginsService {
|
|
|
1704
2511
|
|
|
1705
2512
|
async fetchRepoFileContent(repo, filePath, file = null) {
|
|
1706
2513
|
if (repo.provider === 'local') {
|
|
1707
|
-
|
|
2514
|
+
const safeFilePath = normalizePluginRepoDirectory(filePath, 'plugin repository file path');
|
|
2515
|
+
if (!safeFilePath) {
|
|
2516
|
+
throw new Error('Invalid plugin repository file path');
|
|
2517
|
+
}
|
|
2518
|
+
const localFilePath = resolveInsideRoot(repo.localPath, safeFilePath, 'Plugin repository file path', { allowHiddenSegments: true });
|
|
2519
|
+
return fs.readFileSync(localFilePath, 'utf-8');
|
|
1708
2520
|
}
|
|
1709
2521
|
if (repo.provider === 'gitlab') {
|
|
1710
2522
|
return this.fetchGitLabFileContent(repo, filePath);
|
|
@@ -1752,16 +2564,17 @@ class PluginsService {
|
|
|
1752
2564
|
}
|
|
1753
2565
|
}
|
|
1754
2566
|
|
|
1755
|
-
copyDirRecursive(sourceDir, destDir) {
|
|
2567
|
+
copyDirRecursive(sourceDir, destDir, options = {}) {
|
|
1756
2568
|
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
1757
2569
|
for (const entry of entries) {
|
|
2570
|
+
if (options.excludeNames?.includes(entry.name)) continue;
|
|
1758
2571
|
const sourcePath = path.join(sourceDir, entry.name);
|
|
1759
2572
|
const destPath = path.join(destDir, entry.name);
|
|
1760
2573
|
if (entry.isDirectory()) {
|
|
1761
2574
|
if (!fs.existsSync(destPath)) {
|
|
1762
2575
|
fs.mkdirSync(destPath, { recursive: true });
|
|
1763
2576
|
}
|
|
1764
|
-
this.copyDirRecursive(sourcePath, destPath);
|
|
2577
|
+
this.copyDirRecursive(sourcePath, destPath, options);
|
|
1765
2578
|
} else {
|
|
1766
2579
|
fs.copyFileSync(sourcePath, destPath);
|
|
1767
2580
|
}
|
|
@@ -1820,7 +2633,7 @@ class PluginsService {
|
|
|
1820
2633
|
* @returns {Promise<Object>} Sync results
|
|
1821
2634
|
*/
|
|
1822
2635
|
async syncRepos() {
|
|
1823
|
-
if (this.
|
|
2636
|
+
if (!this.getCapabilities().syncRepos) {
|
|
1824
2637
|
return { success: true, results: [] };
|
|
1825
2638
|
}
|
|
1826
2639
|
|
|
@@ -1957,26 +2770,31 @@ class PluginsService {
|
|
|
1957
2770
|
}
|
|
1958
2771
|
|
|
1959
2772
|
buildMarketPluginItem(repo, data = {}) {
|
|
2773
|
+
const itemRepo = data.repo || repo;
|
|
2774
|
+
const directory = Object.prototype.hasOwnProperty.call(data, 'directory')
|
|
2775
|
+
? data.directory
|
|
2776
|
+
: data.name;
|
|
1960
2777
|
return {
|
|
1961
2778
|
name: data.name,
|
|
1962
2779
|
displayName: data.displayName || '',
|
|
1963
2780
|
description: data.description || '',
|
|
1964
|
-
author: data.author ||
|
|
2781
|
+
author: data.author || itemRepo.owner || itemRepo.projectPath || 'unknown',
|
|
1965
2782
|
version: data.version || '1.0.0',
|
|
1966
2783
|
category: data.category || 'general',
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2784
|
+
marketplace: data.marketplace || repo.marketplace || '',
|
|
2785
|
+
repoUrl: data.repoUrl || itemRepo.repoUrl || buildRepoUrl(itemRepo),
|
|
2786
|
+
repoProvider: itemRepo.provider,
|
|
2787
|
+
repoOwner: itemRepo.owner || '',
|
|
2788
|
+
repoName: itemRepo.name || '',
|
|
2789
|
+
repoBranch: itemRepo.branch || 'main',
|
|
2790
|
+
repoHost: itemRepo.host || '',
|
|
2791
|
+
repoProjectPath: itemRepo.projectPath || '',
|
|
2792
|
+
repoLocalPath: itemRepo.localPath || '',
|
|
2793
|
+
repoId: itemRepo.id,
|
|
2794
|
+
directory: normalizeRepoPath(directory || ''),
|
|
1977
2795
|
installSource: data.installSource || '',
|
|
1978
2796
|
marketplaceFormat: data.marketplaceFormat || '',
|
|
1979
|
-
readmeUrl: this.buildRepoBrowserUrl(
|
|
2797
|
+
readmeUrl: this.buildRepoBrowserUrl(itemRepo, directory || ''),
|
|
1980
2798
|
lspServers: data.lspServers || null,
|
|
1981
2799
|
commands: data.commands || [],
|
|
1982
2800
|
hooks: data.hooks || [],
|
|
@@ -2017,7 +2835,7 @@ class PluginsService {
|
|
|
2017
2835
|
version: manifest.version || manifest.opencode?.minimumVersion || '1.0.0',
|
|
2018
2836
|
category: firstCategory ? String(firstCategory).toLowerCase() : 'general',
|
|
2019
2837
|
directory: file.path,
|
|
2020
|
-
installSource
|
|
2838
|
+
installSource,
|
|
2021
2839
|
marketplaceFormat: 'opencode-plugin-json',
|
|
2022
2840
|
repoUrl
|
|
2023
2841
|
});
|
|
@@ -2029,11 +2847,209 @@ class PluginsService {
|
|
|
2029
2847
|
.map(item => item.value);
|
|
2030
2848
|
}
|
|
2031
2849
|
|
|
2850
|
+
_repoFromMarketplaceSource(parentRepo, source) {
|
|
2851
|
+
if (!source) return { repo: parentRepo, directory: '', installSource: '' };
|
|
2852
|
+
|
|
2853
|
+
if (typeof source === 'string') {
|
|
2854
|
+
const raw = source.trim();
|
|
2855
|
+
if (!raw) return { repo: parentRepo, directory: '', installSource: '' };
|
|
2856
|
+
if (isLikelyLocalPath(raw) || raw.startsWith('.')) {
|
|
2857
|
+
return {
|
|
2858
|
+
repo: parentRepo,
|
|
2859
|
+
directory: normalizeRepoPath(raw.replace(/^\.\//, '')),
|
|
2860
|
+
installSource: ''
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
const parsedTree = this.parseRepoTreeSource(raw);
|
|
2864
|
+
if (parsedTree) {
|
|
2865
|
+
return { repo: this.normalizeRepoConfig(parsedTree), directory: parsedTree.directory, installSource: raw };
|
|
2866
|
+
}
|
|
2867
|
+
const parsedRepo = this._repoFromGitUrl(raw, parentRepo.branch || 'main');
|
|
2868
|
+
if (parsedRepo) {
|
|
2869
|
+
return { repo: parsedRepo, directory: '', installSource: raw };
|
|
2870
|
+
}
|
|
2871
|
+
return { repo: parentRepo, directory: normalizeRepoPath(raw.replace(/^\.\//, '')), installSource: raw };
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
if (typeof source !== 'object') {
|
|
2875
|
+
return { repo: parentRepo, directory: '', installSource: '' };
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
const sourceType = source.source || source.type || '';
|
|
2879
|
+
const rawPath = source.path || source.directory || '';
|
|
2880
|
+
if (sourceType === 'local' || (!source.url && rawPath)) {
|
|
2881
|
+
return {
|
|
2882
|
+
repo: parentRepo,
|
|
2883
|
+
directory: normalizeRepoPath(String(rawPath).replace(/^\.\//, '')),
|
|
2884
|
+
installSource: ''
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
if (source.url) {
|
|
2889
|
+
const branch = source.ref || source.branch || source.sha || parentRepo.branch || 'main';
|
|
2890
|
+
const parsedRepo = this._repoFromGitUrl(source.url, branch);
|
|
2891
|
+
if (parsedRepo) {
|
|
2892
|
+
return {
|
|
2893
|
+
repo: parsedRepo,
|
|
2894
|
+
directory: normalizeRepoPath(rawPath),
|
|
2895
|
+
installSource: source.url
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
return {
|
|
2901
|
+
repo: parentRepo,
|
|
2902
|
+
directory: normalizeRepoPath(rawPath),
|
|
2903
|
+
installSource: source.url || ''
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
_repoFromGitUrl(url = '', branch = 'main') {
|
|
2908
|
+
const value = String(url || '').trim();
|
|
2909
|
+
if (!value) return null;
|
|
2910
|
+
|
|
2911
|
+
const sshMatch = value.match(/^git@([^:]+):(.+?)(?:\.git)?$/i);
|
|
2912
|
+
if (sshMatch) {
|
|
2913
|
+
const host = `https://${sshMatch[1]}`;
|
|
2914
|
+
const repoPath = sshMatch[2].replace(/\.git$/i, '').replace(/^\/+|\/+$/g, '');
|
|
2915
|
+
const parts = repoPath.split('/').filter(Boolean);
|
|
2916
|
+
if (sshMatch[1].includes('github') && parts.length >= 2) {
|
|
2917
|
+
return this.normalizeRepoConfig({
|
|
2918
|
+
provider: 'github',
|
|
2919
|
+
host,
|
|
2920
|
+
owner: parts[0],
|
|
2921
|
+
name: parts[1],
|
|
2922
|
+
branch,
|
|
2923
|
+
repoUrl: value
|
|
2924
|
+
});
|
|
2925
|
+
}
|
|
2926
|
+
if (parts.length > 0) {
|
|
2927
|
+
return this.normalizeRepoConfig({
|
|
2928
|
+
provider: 'gitlab',
|
|
2929
|
+
host,
|
|
2930
|
+
projectPath: repoPath,
|
|
2931
|
+
branch,
|
|
2932
|
+
repoUrl: value
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
let parsed;
|
|
2938
|
+
try {
|
|
2939
|
+
parsed = new URL(value);
|
|
2940
|
+
} catch {
|
|
2941
|
+
return null;
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
const parts = parsed.pathname.replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '').split('/').filter(Boolean);
|
|
2945
|
+
if (parsed.hostname.includes('github') && parts.length >= 2) {
|
|
2946
|
+
return this.normalizeRepoConfig({
|
|
2947
|
+
provider: 'github',
|
|
2948
|
+
host: `${parsed.protocol}//${parsed.host}`,
|
|
2949
|
+
owner: parts[0],
|
|
2950
|
+
name: parts[1],
|
|
2951
|
+
branch,
|
|
2952
|
+
repoUrl: value
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
if (parsed.hostname.includes('gitlab') && parts.length >= 1) {
|
|
2957
|
+
return this.normalizeRepoConfig({
|
|
2958
|
+
provider: 'gitlab',
|
|
2959
|
+
host: `${parsed.protocol}//${parsed.host}`,
|
|
2960
|
+
projectPath: parts.join('/'),
|
|
2961
|
+
branch,
|
|
2962
|
+
repoUrl: value
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
return null;
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
async _readManifestFromRepo(repo, directory, candidates) {
|
|
2970
|
+
for (const candidate of candidates) {
|
|
2971
|
+
try {
|
|
2972
|
+
return await this.fetchRepoJson(repo, joinRepoPath(directory, candidate));
|
|
2973
|
+
} catch {
|
|
2974
|
+
// try next candidate
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
return null;
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
async _fetchClaudeMarketplacePlugins(repo, marketplace, readJson) {
|
|
2981
|
+
if (!marketplace || !Array.isArray(marketplace.plugins)) return [];
|
|
2982
|
+
const results = [];
|
|
2983
|
+
for (const plugin of marketplace.plugins) {
|
|
2984
|
+
const sourceInfo = this._repoFromMarketplaceSource(repo, plugin.source);
|
|
2985
|
+
const canReadManifest = sourceInfo.repo.provider === 'local' || sourceInfo.repo.id === repo.id;
|
|
2986
|
+
const manifest = canReadManifest
|
|
2987
|
+
? await this._readManifestFromRepo(sourceInfo.repo, sourceInfo.directory, [
|
|
2988
|
+
'.claude-plugin/plugin.json',
|
|
2989
|
+
'plugin.json',
|
|
2990
|
+
'package.json'
|
|
2991
|
+
])
|
|
2992
|
+
: null;
|
|
2993
|
+
results.push(this.buildMarketPluginItem(repo, {
|
|
2994
|
+
repo: sourceInfo.repo,
|
|
2995
|
+
name: manifest?.name || plugin.name,
|
|
2996
|
+
description: manifest?.description || plugin.description || '',
|
|
2997
|
+
author: manifest?.author || plugin.author?.name || marketplace.owner?.name || repo.owner,
|
|
2998
|
+
version: manifest?.version || plugin.version || '1.0.0',
|
|
2999
|
+
category: plugin.category || 'general',
|
|
3000
|
+
directory: sourceInfo.directory || plugin.name,
|
|
3001
|
+
marketplace: marketplace.name || repo.marketplace || '',
|
|
3002
|
+
installSource: sourceInfo.directory ? '' : sourceInfo.installSource,
|
|
3003
|
+
marketplaceFormat: 'claude-marketplace',
|
|
3004
|
+
lspServers: plugin.lspServers || null,
|
|
3005
|
+
commands: manifest?.commands || [],
|
|
3006
|
+
hooks: manifest?.hooks || []
|
|
3007
|
+
}));
|
|
3008
|
+
}
|
|
3009
|
+
return results;
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
async _fetchCodexMarketplacePlugins(repo, marketplace) {
|
|
3013
|
+
if (!marketplace || !Array.isArray(marketplace.plugins)) return [];
|
|
3014
|
+
const marketplaceName = this._resolveCodexMarketplaceName(repo, marketplace.name || marketplace.interface?.displayName || repo.marketplace);
|
|
3015
|
+
const results = [];
|
|
3016
|
+
for (const plugin of marketplace.plugins) {
|
|
3017
|
+
const sourceInfo = this._repoFromMarketplaceSource(repo, plugin.source);
|
|
3018
|
+
const canReadManifest = sourceInfo.repo.provider === 'local' || sourceInfo.repo.id === repo.id;
|
|
3019
|
+
const manifest = canReadManifest
|
|
3020
|
+
? await this._readManifestFromRepo(sourceInfo.repo, sourceInfo.directory, [
|
|
3021
|
+
'.codex-plugin/plugin.json',
|
|
3022
|
+
'plugin.json',
|
|
3023
|
+
'package.json'
|
|
3024
|
+
])
|
|
3025
|
+
: null;
|
|
3026
|
+
results.push(this.buildMarketPluginItem(repo, {
|
|
3027
|
+
repo: sourceInfo.repo,
|
|
3028
|
+
name: manifest?.name || plugin.name,
|
|
3029
|
+
description: manifest?.description || plugin.description || '',
|
|
3030
|
+
author: manifest?.author || plugin.author?.name || marketplace.owner?.name || repo.owner,
|
|
3031
|
+
version: manifest?.version || plugin.version || '1.0.0',
|
|
3032
|
+
category: plugin.category || 'general',
|
|
3033
|
+
directory: sourceInfo.directory || plugin.name,
|
|
3034
|
+
marketplace: marketplaceName,
|
|
3035
|
+
installSource: sourceInfo.directory ? '' : sourceInfo.installSource,
|
|
3036
|
+
marketplaceFormat: 'codex-marketplace',
|
|
3037
|
+
commands: manifest?.commands || [],
|
|
3038
|
+
hooks: manifest?.hooks || []
|
|
3039
|
+
}));
|
|
3040
|
+
}
|
|
3041
|
+
return results;
|
|
3042
|
+
}
|
|
3043
|
+
|
|
2032
3044
|
/**
|
|
2033
3045
|
* Get market plugins from configured repositories
|
|
2034
3046
|
* @returns {Promise<Array>} List of available market plugins
|
|
2035
3047
|
*/
|
|
2036
3048
|
async getMarketPlugins(forceRefresh = false) {
|
|
3049
|
+
if (!this.getCapabilities().market) {
|
|
3050
|
+
return [];
|
|
3051
|
+
}
|
|
3052
|
+
|
|
2037
3053
|
if (forceRefresh) {
|
|
2038
3054
|
this.clearMarketCache({ removeFile: false });
|
|
2039
3055
|
}
|
|
@@ -2074,25 +3090,24 @@ class PluginsService {
|
|
|
2074
3090
|
return this.fetchRepoJson(repo, normalizedPath, file);
|
|
2075
3091
|
};
|
|
2076
3092
|
|
|
2077
|
-
//
|
|
2078
|
-
if (fileMap.has('.claude-plugin/marketplace.json')) {
|
|
3093
|
+
// Claude official marketplace format
|
|
3094
|
+
if (this.platform === 'claude' && fileMap.has('.claude-plugin/marketplace.json')) {
|
|
2079
3095
|
const marketplace = await readJson('.claude-plugin/marketplace.json');
|
|
2080
3096
|
if (marketplace && marketplace.plugins) {
|
|
2081
|
-
|
|
2082
|
-
marketPlugins.push(this.buildMarketPluginItem(repo, {
|
|
2083
|
-
name: plugin.name,
|
|
2084
|
-
description: plugin.description || '',
|
|
2085
|
-
author: plugin.author?.name || marketplace.owner?.name || repo.owner,
|
|
2086
|
-
version: plugin.version || '1.0.0',
|
|
2087
|
-
category: plugin.category || 'general',
|
|
2088
|
-
directory: plugin.source?.replace(/^\.\//, '') || plugin.name,
|
|
2089
|
-
lspServers: plugin.lspServers || null
|
|
2090
|
-
}));
|
|
2091
|
-
}
|
|
3097
|
+
marketPlugins.push(...await this._fetchClaudeMarketplacePlugins(repo, marketplace, readJson));
|
|
2092
3098
|
continue; // Skip legacy format check
|
|
2093
3099
|
}
|
|
2094
3100
|
}
|
|
2095
3101
|
|
|
3102
|
+
// Codex marketplace format generated by plugin-creator
|
|
3103
|
+
if (this._isCodex() && fileMap.has('.agents/plugins/marketplace.json')) {
|
|
3104
|
+
const marketplace = await readJson('.agents/plugins/marketplace.json');
|
|
3105
|
+
if (marketplace && marketplace.plugins) {
|
|
3106
|
+
marketPlugins.push(...await this._fetchCodexMarketplacePlugins(repo, marketplace));
|
|
3107
|
+
continue;
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
|
|
2096
3111
|
// OpenCode plugin marketplace format: plugins/*.plugin.json
|
|
2097
3112
|
if (this._isOpenCode()) {
|
|
2098
3113
|
const openCodeMarketplacePlugins = await this._fetchOpenCodeMarketplacePlugins(repo, repo.branch || 'main');
|
|
@@ -2102,6 +3117,42 @@ class PluginsService {
|
|
|
2102
3117
|
}
|
|
2103
3118
|
}
|
|
2104
3119
|
|
|
3120
|
+
const nestedManifestFiles = files.filter(file => {
|
|
3121
|
+
if (this._isCodex()) {
|
|
3122
|
+
return file.path === '.codex-plugin/plugin.json' || file.path.endsWith('/.codex-plugin/plugin.json');
|
|
3123
|
+
}
|
|
3124
|
+
if (this.platform === 'claude') {
|
|
3125
|
+
return file.path === '.claude-plugin/plugin.json' || file.path.endsWith('/.claude-plugin/plugin.json');
|
|
3126
|
+
}
|
|
3127
|
+
return false;
|
|
3128
|
+
});
|
|
3129
|
+
const nestedDirs = new Set();
|
|
3130
|
+
for (const file of nestedManifestFiles) {
|
|
3131
|
+
const marker = this._isCodex() ? '/.codex-plugin/plugin.json' : '/.claude-plugin/plugin.json';
|
|
3132
|
+
const rootMarker = marker.slice(1);
|
|
3133
|
+
const dir = file.path === rootMarker
|
|
3134
|
+
? ''
|
|
3135
|
+
: normalizeRepoPath(file.path.slice(0, -marker.length));
|
|
3136
|
+
const dirKey = dir || '.';
|
|
3137
|
+
if (nestedDirs.has(dirKey)) continue;
|
|
3138
|
+
nestedDirs.add(dirKey);
|
|
3139
|
+
const manifest = await readJson(file.path);
|
|
3140
|
+
marketPlugins.push(this.buildMarketPluginItem(repo, {
|
|
3141
|
+
name: manifest.name || dir.split('/').pop(),
|
|
3142
|
+
description: manifest.description || '',
|
|
3143
|
+
author: manifest.author || repo.owner,
|
|
3144
|
+
version: manifest.version || '1.0.0',
|
|
3145
|
+
directory: dir,
|
|
3146
|
+
marketplace: this._isCodex() ? this._resolveCodexMarketplaceName(repo) : '',
|
|
3147
|
+
marketplaceFormat: this._isCodex() ? 'codex-manifest' : 'claude-manifest',
|
|
3148
|
+
commands: manifest.commands || [],
|
|
3149
|
+
hooks: manifest.hooks || []
|
|
3150
|
+
}));
|
|
3151
|
+
}
|
|
3152
|
+
if (nestedDirs.size > 0) {
|
|
3153
|
+
continue;
|
|
3154
|
+
}
|
|
3155
|
+
|
|
2105
3156
|
// Legacy format: each directory is a plugin with plugin.json/package.json
|
|
2106
3157
|
const pluginDirs = Array.from(new Set(
|
|
2107
3158
|
files
|