coding-tool-x 3.6.10 → 3.6.11

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.
@@ -6,6 +6,8 @@
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
  const AdmZip = require('adm-zip');
9
+ const toml = require('toml');
10
+ const tomlStringify = require('@iarna/toml').stringify;
9
11
  const configTemplatesService = require('./config-templates-service');
10
12
  const channelsService = require('./channels');
11
13
  const codexChannelsService = require('./codex-channels');
@@ -14,6 +16,7 @@ const opencodeChannelsService = require('./opencode-channels');
14
16
  const { AgentsService } = require('./agents-service');
15
17
  const { CommandsService } = require('./commands-service');
16
18
  const { SkillService } = require('./skill-service');
19
+ const { PluginsService } = require('./plugins-service');
17
20
  const { PATHS, NATIVE_PATHS } = require('../../config/paths');
18
21
 
19
22
  const CONFIG_VERSION = '1.4.0';
@@ -48,6 +51,14 @@ const GEMINI_SETTINGS_PATH = path.join(path.dirname(NATIVE_PATHS.gemini.env), 's
48
51
  const AGENT_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
49
52
  const COMMAND_PLATFORMS = ['claude', 'gemini', 'opencode'];
50
53
  const SKILL_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
54
+ const PLUGIN_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode', 'pi'];
55
+ const CLAUDE_MARKETPLACES_REGISTRY = path.join(CLAUDE_PLUGINS_DIR, 'known_marketplaces.json');
56
+ const CODEX_PLUGINS_DIR = path.join(path.dirname(NATIVE_PATHS.codex.config), 'plugins');
57
+ const CODEX_PLUGINS_CACHE_DIR = path.join(CODEX_PLUGINS_DIR, 'cache');
58
+ const OPENCODE_PLUGINS_DIR = path.join(NATIVE_PATHS.opencode.config, 'plugins');
59
+ const OPENCODE_LEGACY_PLUGINS_DIR = path.join(NATIVE_PATHS.opencode.config, 'plugin');
60
+ const PI_SETTINGS_PATH = NATIVE_PATHS.pi?.settings || path.join(PATHS.base, 'pi-settings.json');
61
+ const PI_EXTENSIONS_DIR = NATIVE_PATHS.pi?.extensions || path.join(PATHS.base, 'pi-extensions');
51
62
 
52
63
  function getOpenCodeConfigPaths() {
53
64
  try {
@@ -517,7 +528,7 @@ function syncImportedChannelsToNativeConfigs(importChannelsByType, nativeConfigs
517
528
  }
518
529
 
519
530
  try {
520
- if (!hasNativeConfigFor('codex')) {
531
+ if (!hasNativeConfigFor('codex') && (importChannelsByType.codex || []).length > 0) {
521
532
  codexChannelsService.writeCodexConfigForMultiChannel(importChannelsByType.codex || []);
522
533
  }
523
534
  } catch (err) {
@@ -543,7 +554,7 @@ function syncImportedChannelsToNativeConfigs(importChannelsByType, nativeConfigs
543
554
  }
544
555
 
545
556
  try {
546
- if (!hasNativeConfigFor('opencode')) {
557
+ if (!hasNativeConfigFor('opencode') && (importChannelsByType.opencode || []).length > 0) {
547
558
  const openCodeSpecs = getNativeConfigSpecs().opencode || {};
548
559
  const preferredSpec = [
549
560
  openCodeSpecs.opencodeJsonc,
@@ -656,6 +667,295 @@ function collectPluginFiles(pluginDir, basePath = '') {
656
667
  return files;
657
668
  }
658
669
 
670
+ function getOpenCodePluginsDir() {
671
+ if (fs.existsSync(OPENCODE_LEGACY_PLUGINS_DIR) && !fs.existsSync(OPENCODE_PLUGINS_DIR)) {
672
+ return OPENCODE_LEGACY_PLUGINS_DIR;
673
+ }
674
+ return OPENCODE_PLUGINS_DIR;
675
+ }
676
+
677
+ function detectPluginManifest(pluginDir, candidates = []) {
678
+ for (const candidate of candidates) {
679
+ const manifestPath = path.join(pluginDir, candidate);
680
+ const manifest = readJsonFileSafe(manifestPath);
681
+ if (manifest) {
682
+ return {
683
+ manifest,
684
+ manifestPath: candidate
685
+ };
686
+ }
687
+ }
688
+ return {
689
+ manifest: {},
690
+ manifestPath: ''
691
+ };
692
+ }
693
+
694
+ function buildManagedPluginExportItem(plugin, platform, baseDir, options = {}) {
695
+ const directory = plugin.directory || plugin.name;
696
+ const installPath = plugin.installPath || (directory ? path.join(baseDir, directory) : '');
697
+ if (!installPath || !fs.existsSync(installPath)) {
698
+ return null;
699
+ }
700
+
701
+ const pluginDir = assertPathInside(baseDir, installPath);
702
+ if (!pluginDir) {
703
+ return null;
704
+ }
705
+
706
+ const { manifest, manifestPath } = detectPluginManifest(pluginDir, options.manifestCandidates || []);
707
+ const files = collectPluginFiles(pluginDir);
708
+ if (files.length === 0) {
709
+ return null;
710
+ }
711
+
712
+ return {
713
+ platform,
714
+ type: options.type || 'managed',
715
+ pluginType: plugin.pluginType || options.pluginType || 'local',
716
+ pluginKind: plugin.pluginKind || options.pluginKind || 'plugin',
717
+ name: manifest.name || plugin.name || path.basename(pluginDir),
718
+ version: manifest.version || plugin.version || '1.0.0',
719
+ description: manifest.description || plugin.description || '',
720
+ author: manifest.author || plugin.author || '',
721
+ directory: path.relative(baseDir, pluginDir),
722
+ marketplace: plugin.marketplace || '',
723
+ enabled: plugin.enabled !== false,
724
+ installed: plugin.installed !== false,
725
+ source: plugin.source || '',
726
+ installSource: plugin.installSource || '',
727
+ repoId: plugin.repoId || '',
728
+ repoProvider: plugin.repoProvider || '',
729
+ repoOwner: plugin.repoOwner || '',
730
+ repoName: plugin.repoName || '',
731
+ repoBranch: plugin.repoBranch || '',
732
+ repoDirectory: plugin.repoDirectory || '',
733
+ repoHost: plugin.repoHost || '',
734
+ repoProjectPath: plugin.repoProjectPath || '',
735
+ repoLocalPath: plugin.repoLocalPath || '',
736
+ repoUrl: plugin.repoUrl || '',
737
+ manifest,
738
+ manifestPath,
739
+ files
740
+ };
741
+ }
742
+
743
+ function assertPathInside(baseDir, targetPath) {
744
+ if (!baseDir || !targetPath) return null;
745
+ const resolvedBase = path.resolve(baseDir);
746
+ const resolvedTarget = path.resolve(targetPath);
747
+ const relative = path.relative(resolvedBase, resolvedTarget);
748
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
749
+ return null;
750
+ }
751
+ return resolvedTarget;
752
+ }
753
+
754
+ function readJsonConfigSnapshot(filePath) {
755
+ const content = readJsonFileSafe(filePath);
756
+ return content ? { path: filePath, content } : null;
757
+ }
758
+
759
+ function exportPluginControlSnapshot(platform, service) {
760
+ const snapshot = {};
761
+ const reposPath = PATHS.pluginRepos?.[platform];
762
+ const marketCachePath = PATHS.pluginMarketCache?.[platform];
763
+ const repos = readJsonConfigSnapshot(reposPath);
764
+ const marketCache = readJsonConfigSnapshot(marketCachePath);
765
+
766
+ if (repos) {
767
+ snapshot.repos = repos;
768
+ }
769
+ if (marketCache) {
770
+ snapshot.marketCache = marketCache;
771
+ }
772
+
773
+ try {
774
+ const serviceRepos = service.getRepos?.() || [];
775
+ if (serviceRepos.length > 0) {
776
+ snapshot.resolvedRepos = serviceRepos;
777
+ }
778
+ } catch (err) {
779
+ // repo metadata is supplemental; file snapshots remain the source of truth
780
+ }
781
+
782
+ return snapshot;
783
+ }
784
+
785
+ function exportClaudePluginsByPlatform(service) {
786
+ const installedPlugins = service.listPlugins().plugins || [];
787
+ const plugins = [];
788
+
789
+ for (const plugin of installedPlugins) {
790
+ if (plugin.installPath && fs.existsSync(plugin.installPath)) {
791
+ const pluginDir = assertPathInside(CLAUDE_PLUGINS_DIR, plugin.installPath)
792
+ || assertPathInside(LEGACY_PLUGINS_DIR, plugin.installPath);
793
+ if (pluginDir) {
794
+ const { manifest, manifestPath } = detectPluginManifest(pluginDir, [
795
+ '.claude-plugin/plugin.json',
796
+ 'plugin.json',
797
+ 'package.json'
798
+ ]);
799
+ const files = collectPluginFiles(pluginDir);
800
+ if (files.length > 0) {
801
+ plugins.push({
802
+ platform: 'claude',
803
+ type: 'native',
804
+ pluginType: plugin.pluginType || 'native',
805
+ pluginKind: plugin.pluginKind || 'plugin',
806
+ name: manifest.name || plugin.name || path.basename(pluginDir),
807
+ marketplace: plugin.marketplace || '',
808
+ version: manifest.version || plugin.version || '1.0.0',
809
+ description: manifest.description || plugin.description || '',
810
+ author: manifest.author || plugin.author || '',
811
+ directory: path.relative(CLAUDE_PLUGINS_DIR, pluginDir),
812
+ enabled: plugin.enabled !== false,
813
+ installedAt: plugin.installedAt,
814
+ scope: plugin.scope,
815
+ source: plugin.source || '',
816
+ repoId: plugin.repoId || '',
817
+ repoProvider: plugin.repoProvider || '',
818
+ repoOwner: plugin.repoOwner || '',
819
+ repoName: plugin.repoName || '',
820
+ repoBranch: plugin.repoBranch || '',
821
+ repoDirectory: plugin.repoDirectory || '',
822
+ repoHost: plugin.repoHost || '',
823
+ repoProjectPath: plugin.repoProjectPath || '',
824
+ repoLocalPath: plugin.repoLocalPath || '',
825
+ repoUrl: plugin.repoUrl || '',
826
+ manifest,
827
+ manifestPath,
828
+ files
829
+ });
830
+ }
831
+ }
832
+ }
833
+ }
834
+
835
+ const legacyPluginNames = new Set(plugins.map(plugin => plugin.name));
836
+ for (const plugin of exportLegacyPlugins()) {
837
+ if (!legacyPluginNames.has(plugin.name)) {
838
+ plugins.push({ ...plugin, platform: 'claude' });
839
+ }
840
+ }
841
+
842
+ const control = exportPluginControlSnapshot('claude', service);
843
+ const installedRegistry = readJsonConfigSnapshot(NATIVE_PLUGINS_REGISTRY);
844
+ const knownMarketplaces = readJsonConfigSnapshot(CLAUDE_MARKETPLACES_REGISTRY);
845
+ const legacyRegistry = readJsonConfigSnapshot(LEGACY_PLUGINS_REGISTRY);
846
+ if (installedRegistry) control.installedRegistry = installedRegistry;
847
+ if (knownMarketplaces) control.knownMarketplaces = knownMarketplaces;
848
+ if (legacyRegistry) control.legacyRegistry = legacyRegistry;
849
+
850
+ return { plugins, control };
851
+ }
852
+
853
+ function exportCodexPluginsByPlatform(service) {
854
+ const plugins = (service.listPlugins().plugins || [])
855
+ .map(plugin => buildManagedPluginExportItem(plugin, 'codex', CODEX_PLUGINS_CACHE_DIR, {
856
+ type: 'codex-cache',
857
+ pluginType: 'cache',
858
+ manifestCandidates: ['.codex-plugin/plugin.json', 'plugin.json', 'package.json']
859
+ }))
860
+ .filter(Boolean);
861
+ const control = exportPluginControlSnapshot('codex', service);
862
+ const codexConfig = readNativeConfigSnapshot({ path: NATIVE_PATHS.codex.config, format: 'text' });
863
+ if (codexConfig) {
864
+ control.nativeConfig = codexConfig;
865
+ }
866
+ return { plugins, control };
867
+ }
868
+
869
+ function exportOpenCodePluginsByPlatform(service) {
870
+ const pluginsDir = getOpenCodePluginsDir();
871
+ const plugins = (service.listPlugins().plugins || [])
872
+ .map((plugin) => {
873
+ if (plugin.pluginType === 'npm' || plugin.source === 'opencode-config') {
874
+ return {
875
+ platform: 'opencode',
876
+ type: 'opencode-package',
877
+ pluginType: 'npm',
878
+ pluginKind: plugin.pluginKind || 'plugin',
879
+ name: plugin.name,
880
+ directory: plugin.directory || plugin.name,
881
+ version: plugin.version || 'latest',
882
+ description: plugin.description || '',
883
+ enabled: plugin.enabled !== false,
884
+ installed: plugin.installed !== false,
885
+ source: plugin.source || 'opencode-config'
886
+ };
887
+ }
888
+ return buildManagedPluginExportItem(plugin, 'opencode', pluginsDir, {
889
+ type: 'opencode-local',
890
+ pluginType: plugin.pluginType || 'local',
891
+ manifestCandidates: ['package.json', 'plugin.json']
892
+ });
893
+ })
894
+ .filter(Boolean);
895
+ const control = exportPluginControlSnapshot('opencode', service);
896
+ return { plugins, control };
897
+ }
898
+
899
+ function exportPiPluginsByPlatform(service) {
900
+ const plugins = (service.listPlugins().plugins || [])
901
+ .map((plugin) => {
902
+ if (plugin.pluginType === 'package' || plugin.source === 'pi-settings') {
903
+ return {
904
+ platform: 'pi',
905
+ type: 'pi-package',
906
+ pluginType: 'package',
907
+ pluginKind: plugin.pluginKind || 'package',
908
+ name: plugin.name,
909
+ directory: plugin.directory || plugin.name,
910
+ version: plugin.version || 'latest',
911
+ description: plugin.description || '',
912
+ enabled: plugin.enabled !== false,
913
+ installed: plugin.installed !== false,
914
+ source: plugin.source || 'pi-settings'
915
+ };
916
+ }
917
+ return buildManagedPluginExportItem(plugin, 'pi', PI_EXTENSIONS_DIR, {
918
+ type: 'pi-extension',
919
+ pluginType: plugin.pluginType || 'extension',
920
+ pluginKind: plugin.pluginKind || 'extension',
921
+ manifestCandidates: ['pi.json', 'extension.json', 'plugin.json', 'package.json']
922
+ });
923
+ })
924
+ .filter(Boolean);
925
+ const control = exportPluginControlSnapshot('pi', service);
926
+ const piSettings = readNativeConfigSnapshot({ path: PI_SETTINGS_PATH, format: 'json' });
927
+ if (piSettings) {
928
+ control.nativeSettings = piSettings;
929
+ }
930
+ return { plugins, control };
931
+ }
932
+
933
+ function exportPluginsSnapshotByPlatform() {
934
+ return PLUGIN_PLATFORMS.reduce((result, platform) => {
935
+ try {
936
+ const service = new PluginsService(platform);
937
+ if (platform === 'claude') {
938
+ result[platform] = exportClaudePluginsByPlatform(service);
939
+ } else if (platform === 'codex') {
940
+ result[platform] = exportCodexPluginsByPlatform(service);
941
+ } else if (platform === 'opencode') {
942
+ result[platform] = exportOpenCodePluginsByPlatform(service);
943
+ } else if (platform === 'pi') {
944
+ result[platform] = exportPiPluginsByPlatform(service);
945
+ } else {
946
+ result[platform] = {
947
+ plugins: service.listPlugins().plugins || [],
948
+ control: exportPluginControlSnapshot(platform, service)
949
+ };
950
+ }
951
+ } catch (err) {
952
+ console.warn(`[ConfigExport] Failed to export plugins for ${platform}:`, err.message);
953
+ result[platform] = { plugins: [], control: {} };
954
+ }
955
+ return result;
956
+ }, {});
957
+ }
958
+
659
959
  function exportSkillsSnapshot(platform = 'claude') {
660
960
  const skillService = new SkillService(platform);
661
961
  const installedSkills = skillService.getInstalledSkills();
@@ -814,6 +1114,381 @@ function exportPluginsSnapshot() {
814
1114
  return [...legacyPlugins, ...nativePlugins];
815
1115
  }
816
1116
 
1117
+ function getPluginsByPlatformItems(plugins = [], pluginsByPlatform = {}) {
1118
+ const result = Object.fromEntries(PLUGIN_PLATFORMS.map(platform => [platform, []]));
1119
+ const hasStructuredPlugins = pluginsByPlatform
1120
+ && typeof pluginsByPlatform === 'object'
1121
+ && Object.keys(pluginsByPlatform).length > 0;
1122
+
1123
+ if (hasStructuredPlugins) {
1124
+ for (const platform of PLUGIN_PLATFORMS) {
1125
+ const platformSnapshot = pluginsByPlatform[platform];
1126
+ const platformPlugins = Array.isArray(platformSnapshot?.plugins)
1127
+ ? platformSnapshot.plugins
1128
+ : (Array.isArray(platformSnapshot) ? platformSnapshot : []);
1129
+ result[platform] = platformPlugins.map(plugin => ({
1130
+ ...plugin,
1131
+ platform
1132
+ }));
1133
+ }
1134
+ }
1135
+
1136
+ if (!hasStructuredPlugins && Array.isArray(plugins)) {
1137
+ for (const plugin of plugins) {
1138
+ const platform = PLUGIN_PLATFORMS.includes(plugin?.platform) ? plugin.platform : 'claude';
1139
+ result[platform].push({ ...plugin, platform });
1140
+ }
1141
+ }
1142
+
1143
+ return result;
1144
+ }
1145
+
1146
+ function writePluginFiles(pluginDir, files = []) {
1147
+ let failed = false;
1148
+
1149
+ for (const file of files) {
1150
+ const filePath = resolveSafePath(pluginDir, file.path);
1151
+ if (!filePath) {
1152
+ failed = true;
1153
+ break;
1154
+ }
1155
+
1156
+ ensureDir(path.dirname(filePath));
1157
+
1158
+ try {
1159
+ if (file.encoding === SKILL_FILE_ENCODING) {
1160
+ fs.writeFileSync(filePath, Buffer.from(file.content || '', SKILL_FILE_ENCODING));
1161
+ } else {
1162
+ fs.writeFileSync(filePath, file.content || '', file.encoding || 'utf8');
1163
+ }
1164
+ } catch (err) {
1165
+ console.error(`[ConfigImport] Failed to write plugin file ${file.path}:`, err);
1166
+ failed = true;
1167
+ break;
1168
+ }
1169
+ }
1170
+
1171
+ return !failed && files.length > 0;
1172
+ }
1173
+
1174
+ function pluginNameForPath(plugin = {}) {
1175
+ return plugin.name || plugin.id || plugin.directory || 'plugin';
1176
+ }
1177
+
1178
+ function sanitizePackageName(value = '') {
1179
+ return String(value || '')
1180
+ .trim()
1181
+ .replace(/\\/g, '/')
1182
+ .split('/')
1183
+ .filter(Boolean)
1184
+ .pop()
1185
+ ?.replace(/[^a-zA-Z0-9._@+-]+/g, '-')
1186
+ .replace(/^-+|-+$/g, '') || 'plugin';
1187
+ }
1188
+
1189
+ function resolvePluginImportDir(baseDir, plugin = {}) {
1190
+ const directory = plugin.directory && !String(plugin.directory).includes('..')
1191
+ ? plugin.directory
1192
+ : sanitizePackageName(pluginNameForPath(plugin));
1193
+ return resolveSafePath(baseDir, directory);
1194
+ }
1195
+
1196
+ function readTomlFileSafe(filePath) {
1197
+ if (!filePath || !fs.existsSync(filePath)) return {};
1198
+ try {
1199
+ const content = fs.readFileSync(filePath, 'utf8');
1200
+ return content.trim() ? toml.parse(content) : {};
1201
+ } catch (err) {
1202
+ return {};
1203
+ }
1204
+ }
1205
+
1206
+ function mergeTomlFile(filePath, updates = {}, overwrite = true) {
1207
+ if (!filePath || !updates || typeof updates !== 'object') {
1208
+ return 'failed';
1209
+ }
1210
+ if (fs.existsSync(filePath) && !overwrite) {
1211
+ return 'skipped';
1212
+ }
1213
+ const existing = readTomlFileSafe(filePath);
1214
+ const nextValue = {
1215
+ ...existing,
1216
+ ...updates,
1217
+ plugins: {
1218
+ ...((existing.plugins && typeof existing.plugins === 'object') ? existing.plugins : {}),
1219
+ ...((updates.plugins && typeof updates.plugins === 'object') ? updates.plugins : {})
1220
+ },
1221
+ marketplaces: {
1222
+ ...((existing.marketplaces && typeof existing.marketplaces === 'object') ? existing.marketplaces : {}),
1223
+ ...((updates.marketplaces && typeof updates.marketplaces === 'object') ? updates.marketplaces : {})
1224
+ }
1225
+ };
1226
+ if (Object.keys(nextValue.plugins).length === 0) delete nextValue.plugins;
1227
+ if (Object.keys(nextValue.marketplaces).length === 0) delete nextValue.marketplaces;
1228
+
1229
+ ensureDir(path.dirname(filePath));
1230
+ fs.writeFileSync(filePath, tomlStringify(JSON.parse(JSON.stringify(nextValue))), 'utf8');
1231
+ return 'success';
1232
+ }
1233
+
1234
+ function mergePluginRepoConfig(platform, snapshot = {}, overwrite = true) {
1235
+ const reposPath = PATHS.pluginRepos?.[platform];
1236
+ const marketCachePath = PATHS.pluginMarketCache?.[platform];
1237
+ let changed = 0;
1238
+ let skipped = 0;
1239
+ let failed = 0;
1240
+
1241
+ const reposContent = snapshot.control?.repos?.content;
1242
+ if (reposPath && reposContent && typeof reposContent === 'object') {
1243
+ const status = writeJsonFileAbsolute(reposPath, reposContent, overwrite);
1244
+ if (status === 'success') changed++;
1245
+ else if (status === 'skipped') skipped++;
1246
+ else failed++;
1247
+ }
1248
+
1249
+ const marketCacheContent = snapshot.control?.marketCache?.content;
1250
+ if (marketCachePath && marketCacheContent && typeof marketCacheContent === 'object') {
1251
+ const status = writeJsonFileAbsolute(marketCachePath, marketCacheContent, overwrite);
1252
+ if (status === 'success') changed++;
1253
+ else if (status === 'skipped') skipped++;
1254
+ else failed++;
1255
+ }
1256
+
1257
+ return { changed, skipped, failed };
1258
+ }
1259
+
1260
+ function mergeCodexPluginConfig(snapshot = {}, plugins = [], overwrite = true) {
1261
+ const nativeConfig = snapshot.control?.nativeConfig;
1262
+ if (nativeConfig?.content !== undefined) {
1263
+ return writeNativeConfigAbsolute({ path: NATIVE_PATHS.codex.config, format: 'text' }, nativeConfig, overwrite);
1264
+ }
1265
+
1266
+ const pluginEntries = {};
1267
+ const marketplaces = {};
1268
+ for (const plugin of plugins) {
1269
+ if (!plugin?.name) continue;
1270
+ const pluginKey = plugin.marketplace ? `${plugin.name}@${plugin.marketplace}` : plugin.name;
1271
+ pluginEntries[pluginKey] = { enabled: plugin.enabled !== false };
1272
+ if (plugin.marketplace && plugin.repoUrl) {
1273
+ marketplaces[plugin.marketplace] = {
1274
+ source_type: plugin.repoLocalPath ? 'local' : 'git',
1275
+ source: plugin.repoUrl,
1276
+ last_updated: new Date().toISOString()
1277
+ };
1278
+ }
1279
+ }
1280
+
1281
+ if (Object.keys(pluginEntries).length === 0 && Object.keys(marketplaces).length === 0) {
1282
+ return 'skipped';
1283
+ }
1284
+
1285
+ return mergeTomlFile(NATIVE_PATHS.codex.config, {
1286
+ ...(Object.keys(pluginEntries).length > 0 ? { plugins: pluginEntries } : {}),
1287
+ ...(Object.keys(marketplaces).length > 0 ? { marketplaces } : {})
1288
+ }, overwrite);
1289
+ }
1290
+
1291
+ function writeOpenCodePluginConfig(packages = [], overwrite = true) {
1292
+ if (packages.length === 0) return 'skipped';
1293
+ const openCodePaths = getOpenCodeConfigPaths();
1294
+ const configPath = [
1295
+ openCodePaths.opencodec,
1296
+ openCodePaths.opencode,
1297
+ openCodePaths.config
1298
+ ].find(filePath => filePath && fs.existsSync(filePath))
1299
+ || openCodePaths.opencode
1300
+ || path.join(NATIVE_PATHS.opencode.config, 'opencode.json');
1301
+ const existing = readJsonFileSafe(configPath) || {};
1302
+ if (fs.existsSync(configPath) && !overwrite) {
1303
+ return 'skipped';
1304
+ }
1305
+ const existingPlugins = Array.isArray(existing.plugin) ? existing.plugin : [];
1306
+ const mergedPlugins = Array.from(new Set([...existingPlugins, ...packages]));
1307
+ return writeJsonFileAbsolute(configPath, { ...existing, plugin: mergedPlugins }, true);
1308
+ }
1309
+
1310
+ function writePiPluginSettings(plugins = [], snapshot = {}, overwrite = true) {
1311
+ const nativeSettings = snapshot.control?.nativeSettings;
1312
+ if (nativeSettings?.content !== undefined) {
1313
+ return writeNativeConfigAbsolute({ path: PI_SETTINGS_PATH, format: 'json' }, nativeSettings, overwrite);
1314
+ }
1315
+
1316
+ const settings = readJsonFileSafe(PI_SETTINGS_PATH) || {};
1317
+ if (fs.existsSync(PI_SETTINGS_PATH) && !overwrite) {
1318
+ return 'skipped';
1319
+ }
1320
+ const existingPackages = Array.isArray(settings.packages) ? settings.packages : [];
1321
+ const existingDisabled = Array.isArray(settings.disabledPackages) ? settings.disabledPackages : [];
1322
+ const packagePlugins = plugins.filter(plugin => plugin.type === 'pi-package' || plugin.pluginType === 'package');
1323
+ const packages = packagePlugins.map(plugin => plugin.name).filter(Boolean);
1324
+ const disabledPackages = packagePlugins
1325
+ .filter(plugin => plugin.enabled === false)
1326
+ .map(plugin => plugin.name)
1327
+ .filter(Boolean);
1328
+
1329
+ if (packages.length === 0 && disabledPackages.length === 0) {
1330
+ return 'skipped';
1331
+ }
1332
+
1333
+ return writeJsonFileAbsolute(PI_SETTINGS_PATH, {
1334
+ ...settings,
1335
+ packages: Array.from(new Set([...existingPackages, ...packages])),
1336
+ disabledPackages: Array.from(new Set([...existingDisabled, ...disabledPackages]))
1337
+ }, true);
1338
+ }
1339
+
1340
+ function importPluginToDirectory(platform, plugin, baseDir, overwrite) {
1341
+ const files = Array.isArray(plugin.files) ? plugin.files : [];
1342
+ if (files.length === 0) {
1343
+ return 'skipped';
1344
+ }
1345
+
1346
+ const pluginDir = resolvePluginImportDir(baseDir, plugin);
1347
+ if (!pluginDir) {
1348
+ return 'failed';
1349
+ }
1350
+
1351
+ if (fs.existsSync(pluginDir)) {
1352
+ if (!overwrite) {
1353
+ return 'skipped';
1354
+ }
1355
+ fs.rmSync(pluginDir, { recursive: true, force: true });
1356
+ }
1357
+
1358
+ ensureDir(pluginDir);
1359
+ const wroteFiles = writePluginFiles(pluginDir, files);
1360
+ if (!wroteFiles) {
1361
+ return 'failed';
1362
+ }
1363
+
1364
+ const sourceMeta = {
1365
+ repoId: plugin.repoId || '',
1366
+ repoProvider: plugin.repoProvider || '',
1367
+ repoOwner: plugin.repoOwner || '',
1368
+ repoName: plugin.repoName || '',
1369
+ repoBranch: plugin.repoBranch || '',
1370
+ repoDirectory: plugin.repoDirectory || '',
1371
+ repoHost: plugin.repoHost || '',
1372
+ repoProjectPath: plugin.repoProjectPath || '',
1373
+ repoLocalPath: plugin.repoLocalPath || '',
1374
+ repoUrl: plugin.repoUrl || '',
1375
+ marketplace: plugin.marketplace || '',
1376
+ source: plugin.source || ''
1377
+ };
1378
+ if (Object.values(sourceMeta).some(Boolean)) {
1379
+ writeJsonFileAbsolute(path.join(pluginDir, '.cc-tool-plugin-source.json'), sourceMeta, true);
1380
+ }
1381
+
1382
+ if (platform === 'claude') {
1383
+ updateClaudePluginRegistry(plugin, pluginDir);
1384
+ }
1385
+
1386
+ return 'success';
1387
+ }
1388
+
1389
+ function applyImportStatus(results, status) {
1390
+ if (status === 'success') {
1391
+ results.plugins.success++;
1392
+ } else if (status === 'skipped') {
1393
+ results.plugins.skipped++;
1394
+ } else {
1395
+ results.plugins.failed++;
1396
+ }
1397
+ }
1398
+
1399
+ function importPluginsByPlatformSnapshot(snapshotByPlatform = {}, legacyPlugins = [], overwrite = true, results) {
1400
+ const pluginsByPlatform = getPluginsByPlatformItems(legacyPlugins, snapshotByPlatform);
1401
+
1402
+ for (const platform of PLUGIN_PLATFORMS) {
1403
+ const snapshot = snapshotByPlatform?.[platform] && typeof snapshotByPlatform[platform] === 'object'
1404
+ ? snapshotByPlatform[platform]
1405
+ : {};
1406
+ const repoStatus = mergePluginRepoConfig(platform, snapshot, overwrite);
1407
+ results.plugins.success += repoStatus.changed;
1408
+ results.plugins.skipped += repoStatus.skipped;
1409
+ results.plugins.failed += repoStatus.failed;
1410
+
1411
+ const plugins = pluginsByPlatform[platform] || [];
1412
+ const hasControl = snapshot?.control && Object.keys(snapshot.control).length > 0;
1413
+
1414
+ if (platform === 'codex' && (hasControl || plugins.length > 0)) {
1415
+ applyImportStatus(results, mergeCodexPluginConfig(snapshot, plugins, overwrite));
1416
+ } else if (platform === 'opencode' && (hasControl || plugins.length > 0)) {
1417
+ const packages = plugins
1418
+ .filter(plugin => plugin.type === 'opencode-package' || plugin.pluginType === 'npm')
1419
+ .map(plugin => plugin.name || plugin.directory)
1420
+ .filter(Boolean);
1421
+ applyImportStatus(results, writeOpenCodePluginConfig(packages, overwrite));
1422
+ } else if (platform === 'pi' && (hasControl || plugins.length > 0)) {
1423
+ applyImportStatus(results, writePiPluginSettings(plugins, snapshot, overwrite));
1424
+ }
1425
+
1426
+ for (const plugin of plugins) {
1427
+ try {
1428
+ if (platform === 'codex') {
1429
+ applyImportStatus(results, importPluginToDirectory(platform, plugin, CODEX_PLUGINS_CACHE_DIR, overwrite));
1430
+ } else if (platform === 'opencode') {
1431
+ if (plugin.type === 'opencode-package' || plugin.pluginType === 'npm') {
1432
+ continue;
1433
+ }
1434
+ applyImportStatus(results, importPluginToDirectory(platform, plugin, getOpenCodePluginsDir(), overwrite));
1435
+ } else if (platform === 'pi') {
1436
+ if (plugin.type === 'pi-package' || plugin.pluginType === 'package') {
1437
+ continue;
1438
+ }
1439
+ applyImportStatus(results, importPluginToDirectory(platform, plugin, PI_EXTENSIONS_DIR, overwrite));
1440
+ } else if (platform === 'claude') {
1441
+ const isLegacy = plugin.type === 'legacy';
1442
+ applyImportStatus(results, importPluginToDirectory(
1443
+ platform,
1444
+ plugin,
1445
+ isLegacy ? LEGACY_PLUGINS_DIR : CLAUDE_PLUGINS_DIR,
1446
+ overwrite
1447
+ ));
1448
+ }
1449
+ } catch (err) {
1450
+ console.error(`[ConfigImport] Failed to import ${platform} plugin ${plugin?.name}:`, err);
1451
+ results.plugins.failed++;
1452
+ }
1453
+ }
1454
+ }
1455
+ }
1456
+
1457
+ function updateClaudePluginRegistry(plugin, pluginDir) {
1458
+ const pluginName = plugin.name || path.basename(pluginDir);
1459
+ const marketplace = plugin.marketplace || '';
1460
+ const nativeKey = marketplace ? `${pluginName}@${marketplace}` : pluginName;
1461
+ const installed = readJsonFileSafe(NATIVE_PLUGINS_REGISTRY) || { version: 2, plugins: {} };
1462
+ installed.version = installed.version || 2;
1463
+ installed.plugins = installed.plugins && typeof installed.plugins === 'object' ? installed.plugins : {};
1464
+ installed.plugins[nativeKey] = [{
1465
+ scope: plugin.scope || 'user',
1466
+ installPath: pluginDir,
1467
+ version: plugin.version || '1.0.0',
1468
+ installedAt: plugin.installedAt || new Date().toISOString(),
1469
+ lastUpdated: new Date().toISOString(),
1470
+ ...(plugin.source ? { source: plugin.source } : {}),
1471
+ ...(plugin.repoUrl ? { repoUrl: plugin.repoUrl } : {}),
1472
+ ...(plugin.repoProvider ? { repoProvider: plugin.repoProvider } : {}),
1473
+ ...(plugin.repoOwner ? { repoOwner: plugin.repoOwner } : {}),
1474
+ ...(plugin.repoName ? { repoName: plugin.repoName } : {}),
1475
+ ...(plugin.repoBranch ? { repoBranch: plugin.repoBranch } : {}),
1476
+ ...(plugin.repoDirectory ? { repoDirectory: plugin.repoDirectory } : {}),
1477
+ ...(plugin.repoHost ? { repoHost: plugin.repoHost } : {}),
1478
+ ...(plugin.repoProjectPath ? { repoProjectPath: plugin.repoProjectPath } : {}),
1479
+ ...(plugin.repoLocalPath ? { repoLocalPath: plugin.repoLocalPath } : {}),
1480
+ ...(plugin.repoId ? { repoId: plugin.repoId } : {})
1481
+ }];
1482
+ writeJsonFileAbsolute(NATIVE_PLUGINS_REGISTRY, installed, true);
1483
+
1484
+ const settings = readJsonFileSafe(CLAUDE_SETTINGS_PATH) || {};
1485
+ settings.enabledPlugins = settings.enabledPlugins && typeof settings.enabledPlugins === 'object'
1486
+ ? settings.enabledPlugins
1487
+ : {};
1488
+ settings.enabledPlugins[nativeKey] = plugin.enabled !== false;
1489
+ writeJsonFileAbsolute(CLAUDE_SETTINGS_PATH, settings, true);
1490
+ }
1491
+
817
1492
  function writeTextFile(baseDir, relativePath, content, overwrite) {
818
1493
  if (!content && content !== '') {
819
1494
  return 'failed';
@@ -871,6 +1546,7 @@ function exportAllConfigs() {
871
1546
 
872
1547
  // 获取 Plugins 配置
873
1548
  const plugins = exportPluginsSnapshot();
1549
+ const pluginsByPlatform = exportPluginsSnapshotByPlatform();
874
1550
  const nativeConfigs = exportNativeConfigs();
875
1551
  const oauthCredentials = readJsonFileSafe(PATHS.oauthCredentials);
876
1552
 
@@ -923,6 +1599,7 @@ function exportAllConfigs() {
923
1599
  commandsByPlatform,
924
1600
  mcpServers: mcpServers || [],
925
1601
  plugins: plugins || [],
1602
+ pluginsByPlatform,
926
1603
  markdownFiles: markdownFiles,
927
1604
  uiConfig: uiConfig,
928
1605
  prompts: prompts,
@@ -1017,6 +1694,8 @@ async function importConfigs(importData, options = {}) {
1017
1694
  skillsByPlatform = {},
1018
1695
  commands = [],
1019
1696
  commandsByPlatform = {},
1697
+ plugins = [],
1698
+ pluginsByPlatform = {},
1020
1699
  mcpServers = [],
1021
1700
  markdownFiles = {},
1022
1701
  uiConfig = null,
@@ -1252,8 +1931,17 @@ async function importConfigs(importData, options = {}) {
1252
1931
  }
1253
1932
  }
1254
1933
 
1255
- // 导入 Plugins
1256
- if (importData.data.plugins && importData.data.plugins.length > 0) {
1934
+ // 导入 Plugins(优先使用多平台结构,兼容旧结构 plugins)
1935
+ if (pluginsByPlatform && typeof pluginsByPlatform === 'object' && Object.keys(pluginsByPlatform).length > 0) {
1936
+ importPluginsByPlatformSnapshot(pluginsByPlatform, plugins, overwrite, results);
1937
+ }
1938
+
1939
+ // 导入旧版 Plugins 结构。新包已经通过 pluginsByPlatform 处理过,避免重复写入。
1940
+ if (
1941
+ (!pluginsByPlatform || Object.keys(pluginsByPlatform).length === 0) &&
1942
+ importData.data.plugins &&
1943
+ importData.data.plugins.length > 0
1944
+ ) {
1257
1945
  const plugins = importData.data.plugins;
1258
1946
 
1259
1947
  try {