coding-tool-x 3.6.8 → 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-C57ZRDq5.js → Analytics-BuBlsbcI.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-B3qqqL05.js → ConfigTemplates-DQczxuOX.js} +1 -1
- package/dist/web/assets/{Home-CG5XrTNc.js → Home-BvAzzWnB.js} +1 -1
- package/dist/web/assets/{PluginManager-BhX0LUzr.js → PluginManager-C8gAlaps.js} +1 -1
- package/dist/web/assets/{ProjectList-BQ01OZn9.js → ProjectList-nqgi6_cR.js} +1 -1
- package/dist/web/assets/{SessionList-DPv2IOXG.js → SessionList-D8AxMYy6.js} +1 -1
- package/dist/web/assets/{SkillManager-D5CfQm2O.js → SkillManager-BDbNbNkK.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Ce8ggteK.js → WorkspaceManager-DGGaGW2c.js} +1 -1
- package/dist/web/assets/{index-DJ9VFidd.css → index-Dsh4Q5Cg.css} +1 -1
- package/dist/web/assets/{index-CpnFNWqC.js → index-y-eEayxR.js} +2 -2
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/server/api/plugins.js +6 -4
- package/src/server/services/plugins-service.js +374 -31
- package/src/server/services/skill-service.js +12 -3
package/dist/web/index.html
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
<link rel="icon" href="/favicon.ico">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>coding-tool-x · AI 编程工作台</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-y-eEayxR.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/markdown-BNJfEgCA.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vue-vendor-B5RqjEjQ.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/naive-ui-kcWDeJwl.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/icons-QFXzEgqM.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/vendors-DQA4Hw9v.js">
|
|
14
14
|
<link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Dsh4Q5Cg.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="app"></div>
|
package/package.json
CHANGED
|
@@ -42,7 +42,8 @@ function extractRepoPayload(source = {}) {
|
|
|
42
42
|
projectPath: repo.projectPath || source.projectPath || '',
|
|
43
43
|
localPath: repo.localPath || source.localPath || '',
|
|
44
44
|
repoUrl: repo.repoUrl || repo.url || source.repoUrl || source.url || '',
|
|
45
|
-
token: repo.token || source.token || ''
|
|
45
|
+
token: repo.token || source.token || '',
|
|
46
|
+
marketplace: repo.marketplace || source.marketplace || ''
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -140,12 +141,13 @@ router.post('/install', async (req, res) => {
|
|
|
140
141
|
try {
|
|
141
142
|
const { platform, service } = getPluginsService(req);
|
|
142
143
|
const { directory, repo, gitUrl, source } = req.body;
|
|
144
|
+
const hasDirectoryField = Object.prototype.hasOwnProperty.call(req.body, 'directory');
|
|
143
145
|
|
|
144
146
|
// Support both new format (directory + repo) and legacy format (gitUrl)
|
|
145
147
|
let installUrl;
|
|
146
148
|
if (source) {
|
|
147
149
|
installUrl = source;
|
|
148
|
-
} else if (
|
|
150
|
+
} else if (repo && hasDirectoryField) {
|
|
149
151
|
installUrl = '';
|
|
150
152
|
} else if (gitUrl) {
|
|
151
153
|
installUrl = gitUrl;
|
|
@@ -158,10 +160,10 @@ router.post('/install', async (req, res) => {
|
|
|
158
160
|
|
|
159
161
|
const result = await service.installPlugin(
|
|
160
162
|
installUrl,
|
|
161
|
-
|
|
163
|
+
repo && hasDirectoryField
|
|
162
164
|
? {
|
|
163
165
|
...extractRepoPayload({ repo }),
|
|
164
|
-
directory
|
|
166
|
+
directory: directory || ''
|
|
165
167
|
}
|
|
166
168
|
: null
|
|
167
169
|
);
|
|
@@ -29,6 +29,8 @@ const {
|
|
|
29
29
|
const CLAUDE_PLUGINS_DIR = path.join(path.dirname(NATIVE_PATHS.claude.settings), 'plugins');
|
|
30
30
|
const CLAUDE_INSTALLED_FILE = path.join(CLAUDE_PLUGINS_DIR, 'installed_plugins.json');
|
|
31
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');
|
|
32
34
|
const CODEX_PLUGINS_DIR = path.join(path.dirname(NATIVE_PATHS.codex.config), 'plugins');
|
|
33
35
|
const CODEX_PLUGINS_CACHE_DIR = path.join(CODEX_PLUGINS_DIR, 'cache');
|
|
34
36
|
const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
|
|
@@ -135,6 +137,10 @@ function normalizePluginCacheSegment(value = '', label = 'plugin segment', fallb
|
|
|
135
137
|
});
|
|
136
138
|
}
|
|
137
139
|
|
|
140
|
+
function normalizeClaudeMarketplaceSegment(value = '', fallback = 'ctx') {
|
|
141
|
+
return normalizePluginCacheSegment(value, 'Claude marketplace', fallback);
|
|
142
|
+
}
|
|
143
|
+
|
|
138
144
|
function resolvePluginConfigFile(configDir, name) {
|
|
139
145
|
const safeName = normalizePluginPathName(name, 'plugin config name');
|
|
140
146
|
return resolveInsideRoot(configDir, `${safeName}.json`, 'Plugin config path');
|
|
@@ -171,6 +177,21 @@ function splitPluginMarketplaceKey(key = '') {
|
|
|
171
177
|
};
|
|
172
178
|
}
|
|
173
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
|
+
|
|
174
195
|
function isWindowsAbsolutePath(input = '') {
|
|
175
196
|
return /^[a-zA-Z]:[\\/]/.test(String(input || ''));
|
|
176
197
|
}
|
|
@@ -772,6 +793,219 @@ class PluginsService {
|
|
|
772
793
|
fs.writeFileSync(filePath, tomlStringify(safeConfig), 'utf8');
|
|
773
794
|
}
|
|
774
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
|
+
|
|
775
1009
|
_isCodexPluginEnabled(name, marketplace = '') {
|
|
776
1010
|
const { config } = this._readCodexConfig();
|
|
777
1011
|
const plugins = config.plugins || {};
|
|
@@ -888,7 +1122,7 @@ class PluginsService {
|
|
|
888
1122
|
for (const [key, installations] of Object.entries(data.plugins)) {
|
|
889
1123
|
if (installations && installations.length > 0) {
|
|
890
1124
|
const install = installations[0]; // Get first installation
|
|
891
|
-
const
|
|
1125
|
+
const { name, marketplace } = splitPluginMarketplaceKey(key);
|
|
892
1126
|
|
|
893
1127
|
// Read plugin.json from installPath for description
|
|
894
1128
|
let description = '';
|
|
@@ -941,7 +1175,8 @@ class PluginsService {
|
|
|
941
1175
|
|
|
942
1176
|
// Read enabled state from CTX registry (defaults to true if not set)
|
|
943
1177
|
const legacyInfo = getPlugin(name);
|
|
944
|
-
const enabledState =
|
|
1178
|
+
const enabledState = this._isClaudePluginEnabled(name, marketplace) &&
|
|
1179
|
+
(legacyInfo ? legacyInfo.enabled !== false : true);
|
|
945
1180
|
|
|
946
1181
|
plugins.push({
|
|
947
1182
|
name,
|
|
@@ -1052,7 +1287,7 @@ class PluginsService {
|
|
|
1052
1287
|
*/
|
|
1053
1288
|
async installPlugin(source, repoInfo = null) {
|
|
1054
1289
|
if (this._isCodex()) {
|
|
1055
|
-
if (repoInfo
|
|
1290
|
+
if (hasRepoInstallInfo(repoInfo)) {
|
|
1056
1291
|
return this._installFromRepoDirectory(repoInfo);
|
|
1057
1292
|
}
|
|
1058
1293
|
|
|
@@ -1080,7 +1315,7 @@ class PluginsService {
|
|
|
1080
1315
|
}
|
|
1081
1316
|
|
|
1082
1317
|
if (this._isOpenCode()) {
|
|
1083
|
-
if (repoInfo
|
|
1318
|
+
if (hasRepoInstallInfo(repoInfo)) {
|
|
1084
1319
|
return this._installFromRepoDirectory(repoInfo, { installRoot: this._getOpenCodePluginsDir() });
|
|
1085
1320
|
}
|
|
1086
1321
|
|
|
@@ -1089,6 +1324,11 @@ class PluginsService {
|
|
|
1089
1324
|
return this._installFromRepoDirectory(parsedSource, { installRoot: this._getOpenCodePluginsDir() });
|
|
1090
1325
|
}
|
|
1091
1326
|
|
|
1327
|
+
const parsedRepo = this._repoFromGitUrl(source, 'main');
|
|
1328
|
+
if (parsedRepo) {
|
|
1329
|
+
return this._installFromRepoDirectory({ ...parsedRepo, directory: '' }, { installRoot: this._getOpenCodePluginsDir() });
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1092
1332
|
// OpenCode 原生支持 npm 包名,通过 opencode.json 的 plugin 数组管理
|
|
1093
1333
|
if (!/^https?:\/\//.test(source)) {
|
|
1094
1334
|
const plugins = this._listOpenCodeConfiguredPlugins();
|
|
@@ -1108,7 +1348,7 @@ class PluginsService {
|
|
|
1108
1348
|
};
|
|
1109
1349
|
}
|
|
1110
1350
|
|
|
1111
|
-
if (repoInfo
|
|
1351
|
+
if (hasRepoInstallInfo(repoInfo)) {
|
|
1112
1352
|
return await this._installFromRepoDirectory(repoInfo);
|
|
1113
1353
|
}
|
|
1114
1354
|
|
|
@@ -1117,6 +1357,11 @@ class PluginsService {
|
|
|
1117
1357
|
return await this._installFromRepoDirectory(parsedSource);
|
|
1118
1358
|
}
|
|
1119
1359
|
|
|
1360
|
+
const parsedRepo = this._repoFromGitUrl(source, 'main');
|
|
1361
|
+
if (parsedRepo) {
|
|
1362
|
+
return await this._installFromRepoDirectory({ ...parsedRepo, directory: '' });
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1120
1365
|
// Fallback to original git clone method
|
|
1121
1366
|
return await installPluginCore(source);
|
|
1122
1367
|
}
|
|
@@ -1153,6 +1398,8 @@ class PluginsService {
|
|
|
1153
1398
|
// Create plugin directory
|
|
1154
1399
|
const pluginDir = this._isCodex()
|
|
1155
1400
|
? installRoot
|
|
1401
|
+
: this.platform === 'claude'
|
|
1402
|
+
? installRoot
|
|
1156
1403
|
: resolveInsideRoot(
|
|
1157
1404
|
installRoot,
|
|
1158
1405
|
normalizePluginPathName(installedPluginName, 'plugin name'),
|
|
@@ -1226,6 +1473,13 @@ class PluginsService {
|
|
|
1226
1473
|
this._ensureDir(path.dirname(fallbackManifestPath));
|
|
1227
1474
|
fs.writeFileSync(fallbackManifestPath, JSON.stringify(manifest, null, 2));
|
|
1228
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
|
+
}
|
|
1482
|
+
}
|
|
1229
1483
|
|
|
1230
1484
|
if (this._isCodex()) {
|
|
1231
1485
|
const marketplace = this._resolveCodexMarketplaceName(normalizedRepo, repoInfo.marketplace);
|
|
@@ -1247,9 +1501,10 @@ class PluginsService {
|
|
|
1247
1501
|
});
|
|
1248
1502
|
this._registerCodexMarketplace(marketplace, normalizedRepo.repoUrl || buildRepoUrl(normalizedRepo));
|
|
1249
1503
|
this._setCodexPluginEnabled(installedPluginName, marketplace, true);
|
|
1250
|
-
} else if (
|
|
1504
|
+
} else if (this.platform === 'claude') {
|
|
1251
1505
|
const installTimestamp = new Date().toISOString();
|
|
1252
1506
|
const sourceUrl = this.buildRepoBrowserUrl(normalizedRepo, directory) || buildRepoUrl(normalizedRepo);
|
|
1507
|
+
const marketplace = this._resolveClaudeMarketplaceName(normalizedRepo, repoInfo.marketplace);
|
|
1253
1508
|
const repoSourceMeta = {
|
|
1254
1509
|
repoId: normalizedRepo.id,
|
|
1255
1510
|
repoProvider: normalizedRepo.provider,
|
|
@@ -1261,6 +1516,7 @@ class PluginsService {
|
|
|
1261
1516
|
repoProjectPath: normalizedRepo.projectPath || '',
|
|
1262
1517
|
repoLocalPath: normalizedRepo.localPath || '',
|
|
1263
1518
|
repoUrl: normalizedRepo.repoUrl || buildRepoUrl(normalizedRepo),
|
|
1519
|
+
repoMarketplace: marketplace,
|
|
1264
1520
|
source: sourceUrl
|
|
1265
1521
|
};
|
|
1266
1522
|
|
|
@@ -1278,30 +1534,54 @@ class PluginsService {
|
|
|
1278
1534
|
}
|
|
1279
1535
|
|
|
1280
1536
|
this.writeRepoSourceMeta(pluginDir, repoSourceMeta);
|
|
1537
|
+
this._upsertClaudeMarketplacePlugin(marketplace, pluginDir, {
|
|
1538
|
+
name: installedPluginName,
|
|
1539
|
+
description: manifest.description || '',
|
|
1540
|
+
category: manifest.category || ''
|
|
1541
|
+
});
|
|
1281
1542
|
|
|
1282
1543
|
// Also register in Claude's native installed_plugins.json
|
|
1283
1544
|
try {
|
|
1284
|
-
this.
|
|
1285
|
-
let nativeData = { plugins: {} };
|
|
1286
|
-
if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
|
|
1287
|
-
try {
|
|
1288
|
-
nativeData = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
|
|
1289
|
-
if (!nativeData.plugins) nativeData.plugins = {};
|
|
1290
|
-
} catch (e) { /* ignore parse error */ }
|
|
1291
|
-
}
|
|
1292
|
-
const nativeKey = `${installedPluginName}@ctx`;
|
|
1293
|
-
nativeData.plugins[nativeKey] = [{
|
|
1545
|
+
this._registerClaudeInstalledPlugin(installedPluginName, marketplace, pluginDir, {
|
|
1294
1546
|
version: manifest.version || '1.0.0',
|
|
1295
|
-
installPath: pluginDir,
|
|
1296
1547
|
installedAt: installTimestamp,
|
|
1297
1548
|
scope: 'user',
|
|
1298
1549
|
source: sourceUrl,
|
|
1299
|
-
|
|
1300
|
-
}
|
|
1301
|
-
fs.writeFileSync(CLAUDE_INSTALLED_FILE, JSON.stringify(nativeData, null, 2), 'utf8');
|
|
1550
|
+
repoSourceMeta
|
|
1551
|
+
});
|
|
1302
1552
|
} catch (e) {
|
|
1303
1553
|
console.error('[PluginsService] Failed to update native installed_plugins.json:', e.message);
|
|
1304
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);
|
|
1305
1585
|
}
|
|
1306
1586
|
|
|
1307
1587
|
return {
|
|
@@ -1332,6 +1612,12 @@ class PluginsService {
|
|
|
1332
1612
|
|
|
1333
1613
|
_resolvePluginInstallRoot(normalizedRepo, repoInfo, manifest, options = {}) {
|
|
1334
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
|
+
}
|
|
1335
1621
|
return options.installRoot || INSTALLED_DIR;
|
|
1336
1622
|
}
|
|
1337
1623
|
|
|
@@ -1501,7 +1787,7 @@ class PluginsService {
|
|
|
1501
1787
|
const keysToDelete = [];
|
|
1502
1788
|
const baseName = safeName.split('/').pop(); // handle "plugins/pr-review-toolkit" → "pr-review-toolkit"
|
|
1503
1789
|
for (const [key, installations] of Object.entries(data.plugins)) {
|
|
1504
|
-
const
|
|
1790
|
+
const { name: pluginName } = splitPluginMarketplaceKey(key);
|
|
1505
1791
|
if (pluginName === safeName || key === safeName || pluginName === baseName) {
|
|
1506
1792
|
keysToDelete.push(key);
|
|
1507
1793
|
// Delete install directories
|
|
@@ -1525,6 +1811,8 @@ class PluginsService {
|
|
|
1525
1811
|
}
|
|
1526
1812
|
if (keysToDelete.length > 0) {
|
|
1527
1813
|
for (const key of keysToDelete) {
|
|
1814
|
+
const parsed = splitPluginMarketplaceKey(key);
|
|
1815
|
+
this._removeClaudePluginEnabled(parsed.name || safeName, parsed.marketplace || '');
|
|
1528
1816
|
delete data.plugins[key];
|
|
1529
1817
|
}
|
|
1530
1818
|
fs.writeFileSync(CLAUDE_INSTALLED_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
@@ -1607,7 +1895,7 @@ class PluginsService {
|
|
|
1607
1895
|
const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
|
|
1608
1896
|
if (data.plugins) {
|
|
1609
1897
|
for (const key of Object.keys(data.plugins)) {
|
|
1610
|
-
const
|
|
1898
|
+
const { name: pluginName } = splitPluginMarketplaceKey(key);
|
|
1611
1899
|
if (pluginName === name || key === name || pluginName === baseName) {
|
|
1612
1900
|
pluginExists = true;
|
|
1613
1901
|
break;
|
|
@@ -1626,6 +1914,7 @@ class PluginsService {
|
|
|
1626
1914
|
}
|
|
1627
1915
|
|
|
1628
1916
|
// Store enabled state in CTX registry (upsert)
|
|
1917
|
+
let matchedMarketplace = '';
|
|
1629
1918
|
try {
|
|
1630
1919
|
const { addPlugin } = require('../../plugins/registry');
|
|
1631
1920
|
if (legacyPlugin) {
|
|
@@ -1636,6 +1925,21 @@ class PluginsService {
|
|
|
1636
1925
|
} catch (e) {
|
|
1637
1926
|
console.warn('[PluginsService] Failed to update plugin registry:', e.message);
|
|
1638
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);
|
|
1639
1943
|
|
|
1640
1944
|
return {
|
|
1641
1945
|
name,
|
|
@@ -2260,16 +2564,17 @@ class PluginsService {
|
|
|
2260
2564
|
}
|
|
2261
2565
|
}
|
|
2262
2566
|
|
|
2263
|
-
copyDirRecursive(sourceDir, destDir) {
|
|
2567
|
+
copyDirRecursive(sourceDir, destDir, options = {}) {
|
|
2264
2568
|
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
2265
2569
|
for (const entry of entries) {
|
|
2570
|
+
if (options.excludeNames?.includes(entry.name)) continue;
|
|
2266
2571
|
const sourcePath = path.join(sourceDir, entry.name);
|
|
2267
2572
|
const destPath = path.join(destDir, entry.name);
|
|
2268
2573
|
if (entry.isDirectory()) {
|
|
2269
2574
|
if (!fs.existsSync(destPath)) {
|
|
2270
2575
|
fs.mkdirSync(destPath, { recursive: true });
|
|
2271
2576
|
}
|
|
2272
|
-
this.copyDirRecursive(sourcePath, destPath);
|
|
2577
|
+
this.copyDirRecursive(sourcePath, destPath, options);
|
|
2273
2578
|
} else {
|
|
2274
2579
|
fs.copyFileSync(sourcePath, destPath);
|
|
2275
2580
|
}
|
|
@@ -2466,6 +2771,9 @@ class PluginsService {
|
|
|
2466
2771
|
|
|
2467
2772
|
buildMarketPluginItem(repo, data = {}) {
|
|
2468
2773
|
const itemRepo = data.repo || repo;
|
|
2774
|
+
const directory = Object.prototype.hasOwnProperty.call(data, 'directory')
|
|
2775
|
+
? data.directory
|
|
2776
|
+
: data.name;
|
|
2469
2777
|
return {
|
|
2470
2778
|
name: data.name,
|
|
2471
2779
|
displayName: data.displayName || '',
|
|
@@ -2483,10 +2791,10 @@ class PluginsService {
|
|
|
2483
2791
|
repoProjectPath: itemRepo.projectPath || '',
|
|
2484
2792
|
repoLocalPath: itemRepo.localPath || '',
|
|
2485
2793
|
repoId: itemRepo.id,
|
|
2486
|
-
directory: normalizeRepoPath(
|
|
2794
|
+
directory: normalizeRepoPath(directory || ''),
|
|
2487
2795
|
installSource: data.installSource || '',
|
|
2488
2796
|
marketplaceFormat: data.marketplaceFormat || '',
|
|
2489
|
-
readmeUrl: this.buildRepoBrowserUrl(itemRepo,
|
|
2797
|
+
readmeUrl: this.buildRepoBrowserUrl(itemRepo, directory || ''),
|
|
2490
2798
|
lspServers: data.lspServers || null,
|
|
2491
2799
|
commands: data.commands || [],
|
|
2492
2800
|
hooks: data.hooks || [],
|
|
@@ -2599,6 +2907,33 @@ class PluginsService {
|
|
|
2599
2907
|
_repoFromGitUrl(url = '', branch = 'main') {
|
|
2600
2908
|
const value = String(url || '').trim();
|
|
2601
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
|
+
|
|
2602
2937
|
let parsed;
|
|
2603
2938
|
try {
|
|
2604
2939
|
parsed = new URL(value);
|
|
@@ -2783,16 +3118,24 @@ class PluginsService {
|
|
|
2783
3118
|
}
|
|
2784
3119
|
|
|
2785
3120
|
const nestedManifestFiles = files.filter(file => {
|
|
2786
|
-
if (this._isCodex())
|
|
2787
|
-
|
|
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
|
+
}
|
|
2788
3127
|
return false;
|
|
2789
3128
|
});
|
|
2790
3129
|
const nestedDirs = new Set();
|
|
2791
3130
|
for (const file of nestedManifestFiles) {
|
|
2792
3131
|
const marker = this._isCodex() ? '/.codex-plugin/plugin.json' : '/.claude-plugin/plugin.json';
|
|
2793
|
-
const
|
|
2794
|
-
|
|
2795
|
-
|
|
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);
|
|
2796
3139
|
const manifest = await readJson(file.path);
|
|
2797
3140
|
marketPlugins.push(this.buildMarketPluginItem(repo, {
|
|
2798
3141
|
name: manifest.name || dir.split('/').pop(),
|