@adversity/coding-tool-x 3.0.0 → 3.0.2

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.
@@ -15,13 +15,28 @@ const { CommandsService } = require('./commands-service');
15
15
  const { RulesService } = require('./rules-service');
16
16
  const { SkillService } = require('./skill-service');
17
17
 
18
- const CONFIG_VERSION = '1.1.0';
18
+ const CONFIG_VERSION = '1.2.0';
19
19
  const SKILL_FILE_ENCODING = 'base64';
20
20
  const SKILL_IGNORE_DIRS = new Set(['.git']);
21
21
  const SKILL_IGNORE_FILES = new Set(['.DS_Store']);
22
22
  const CC_TOOL_DIR = path.join(os.homedir(), '.claude', 'cc-tool');
23
23
  const LEGACY_CC_TOOL_DIR = path.join(os.homedir(), '.cc-tool');
24
24
  const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
25
+ const LEGACY_PLUGINS_DIR = path.join(LEGACY_CC_TOOL_DIR, 'plugins', 'installed');
26
+ const LEGACY_PLUGINS_REGISTRY = path.join(LEGACY_CC_TOOL_DIR, 'plugins', 'registry.json');
27
+ const NATIVE_PLUGINS_REGISTRY = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
28
+ const PLUGIN_IGNORE_DIRS = new Set(['.git', 'node_modules', '.DS_Store']);
29
+ const PLUGIN_IGNORE_FILES = new Set(['.DS_Store']);
30
+ const PLUGIN_SENSITIVE_PATTERNS = [
31
+ /\.env$/i,
32
+ /\.env\./i,
33
+ /credentials?\.json$/i,
34
+ /secrets?\.json$/i,
35
+ /\.key$/i,
36
+ /\.pem$/i,
37
+ /\.p12$/i,
38
+ /\.pfx$/i
39
+ ];
25
40
  const CC_UI_CONFIG_PATH = path.join(CC_TOOL_DIR, 'ui-config.json');
26
41
  const CC_TERMINAL_CONFIG_PATH = path.join(CC_TOOL_DIR, 'terminal-config.json');
27
42
  const CC_TERMINAL_COMMANDS_PATH = path.join(CC_TOOL_DIR, 'terminal-commands.json');
@@ -108,6 +123,7 @@ function buildExportReadme(exportData) {
108
123
  ## 📦 包含内容
109
124
  - 权限模板、配置模板、频道配置、工作区、收藏
110
125
  - Agents / Skills / Commands / Rules
126
+ - 插件 (Plugins)
111
127
  - MCP 服务器配置
112
128
  - UI 配置(主题、面板显示、排序等)
113
129
  - 终端配置与 CLI 命令配置
@@ -224,6 +240,56 @@ function collectSkillFiles(baseDir) {
224
240
  return files;
225
241
  }
226
242
 
243
+ function collectPluginFiles(pluginDir, basePath = '') {
244
+ const files = [];
245
+ const stack = [pluginDir];
246
+
247
+ while (stack.length > 0) {
248
+ const currentDir = stack.pop();
249
+ let entries = [];
250
+ try {
251
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
252
+ } catch (err) {
253
+ continue;
254
+ }
255
+
256
+ for (const entry of entries) {
257
+ if (entry.isDirectory()) {
258
+ if (PLUGIN_IGNORE_DIRS.has(entry.name)) {
259
+ continue;
260
+ }
261
+ stack.push(path.join(currentDir, entry.name));
262
+ } else if (entry.isFile()) {
263
+ if (PLUGIN_IGNORE_FILES.has(entry.name)) {
264
+ continue;
265
+ }
266
+ const fullPath = path.join(currentDir, entry.name);
267
+ const relativePath = path.relative(pluginDir, fullPath);
268
+
269
+ // Skip sensitive files
270
+ const isSensitive = PLUGIN_SENSITIVE_PATTERNS.some(pattern => pattern.test(entry.name));
271
+ if (isSensitive) {
272
+ continue;
273
+ }
274
+
275
+ try {
276
+ const content = fs.readFileSync(fullPath);
277
+ files.push({
278
+ path: relativePath,
279
+ encoding: SKILL_FILE_ENCODING,
280
+ content: content.toString(SKILL_FILE_ENCODING)
281
+ });
282
+ } catch (err) {
283
+ continue;
284
+ }
285
+ }
286
+ }
287
+ }
288
+
289
+ files.sort((a, b) => a.path.localeCompare(b.path));
290
+ return files;
291
+ }
292
+
227
293
  function exportSkillsSnapshot() {
228
294
  const skillService = new SkillService();
229
295
  const installedSkills = skillService.getInstalledSkills();
@@ -244,6 +310,113 @@ function exportSkillsSnapshot() {
244
310
  }).filter(Boolean);
245
311
  }
246
312
 
313
+ function exportLegacyPlugins() {
314
+ const plugins = [];
315
+
316
+ // Check if legacy plugins directory exists
317
+ if (!fs.existsSync(LEGACY_PLUGINS_DIR)) {
318
+ return plugins;
319
+ }
320
+
321
+ // Read registry.json if it exists
322
+ const registry = readJsonFileSafe(LEGACY_PLUGINS_REGISTRY) || {};
323
+
324
+ try {
325
+ const pluginDirs = fs.readdirSync(LEGACY_PLUGINS_DIR, { withFileTypes: true });
326
+
327
+ for (const entry of pluginDirs) {
328
+ if (!entry.isDirectory()) {
329
+ continue;
330
+ }
331
+
332
+ const pluginName = entry.name;
333
+ const pluginDir = path.join(LEGACY_PLUGINS_DIR, pluginName);
334
+ const manifestPath = path.join(pluginDir, 'plugin.json');
335
+
336
+ // Read plugin.json manifest
337
+ const manifest = readJsonFileSafe(manifestPath);
338
+ if (!manifest) {
339
+ continue;
340
+ }
341
+
342
+ // Collect all files in the plugin directory
343
+ const files = collectPluginFiles(pluginDir);
344
+
345
+ plugins.push({
346
+ type: 'legacy',
347
+ name: manifest.name || pluginName,
348
+ version: manifest.version || '1.0.0',
349
+ description: manifest.description || '',
350
+ author: manifest.author || '',
351
+ directory: pluginName,
352
+ manifest: manifest,
353
+ files: files,
354
+ registryEntry: registry[pluginName] || null
355
+ });
356
+ }
357
+ } catch (err) {
358
+ console.warn('[ConfigExport] Failed to export legacy plugins:', err.message);
359
+ }
360
+
361
+ return plugins;
362
+ }
363
+
364
+ function exportNativePlugins() {
365
+ const plugins = [];
366
+
367
+ // Read installed_plugins.json
368
+ const installedPlugins = readJsonFileSafe(NATIVE_PLUGINS_REGISTRY);
369
+ if (!installedPlugins || typeof installedPlugins !== 'object') {
370
+ return plugins;
371
+ }
372
+
373
+ try {
374
+ for (const [pluginId, pluginInfo] of Object.entries(installedPlugins)) {
375
+ if (!pluginInfo || !pluginInfo.installPath) {
376
+ continue;
377
+ }
378
+
379
+ const pluginDir = pluginInfo.installPath;
380
+ if (!fs.existsSync(pluginDir)) {
381
+ continue;
382
+ }
383
+
384
+ // Read package.json manifest
385
+ const manifestPath = path.join(pluginDir, 'package.json');
386
+ const manifest = readJsonFileSafe(manifestPath);
387
+ if (!manifest) {
388
+ continue;
389
+ }
390
+
391
+ // Collect all files in the plugin directory
392
+ const files = collectPluginFiles(pluginDir);
393
+
394
+ plugins.push({
395
+ type: 'native',
396
+ id: pluginId,
397
+ name: manifest.name || pluginId,
398
+ version: manifest.version || '1.0.0',
399
+ description: manifest.description || '',
400
+ author: manifest.author || '',
401
+ installPath: pluginDir,
402
+ manifest: manifest,
403
+ files: files,
404
+ registryEntry: pluginInfo
405
+ });
406
+ }
407
+ } catch (err) {
408
+ console.warn('[ConfigExport] Failed to export native plugins:', err.message);
409
+ }
410
+
411
+ return plugins;
412
+ }
413
+
414
+ function exportPluginsSnapshot() {
415
+ const legacyPlugins = exportLegacyPlugins();
416
+ const nativePlugins = exportNativePlugins();
417
+ return [...legacyPlugins, ...nativePlugins];
418
+ }
419
+
247
420
  function writeTextFile(baseDir, relativePath, content, overwrite) {
248
421
  if (!content && content !== '') {
249
422
  return 'failed';
@@ -332,6 +505,9 @@ function exportAllConfigs() {
332
505
  const mcpService = require('./mcp-service');
333
506
  const mcpServers = mcpService.getAllServers();
334
507
 
508
+ // 获取 Plugins 配置
509
+ const plugins = exportPluginsSnapshot();
510
+
335
511
  // 读取 Markdown 配置文件
336
512
  const { PATHS } = require('../../config/paths');
337
513
  const markdownFiles = {};
@@ -383,6 +559,7 @@ function exportAllConfigs() {
383
559
  commands: commands || [],
384
560
  rules: rules || [],
385
561
  mcpServers: mcpServers || [],
562
+ plugins: plugins || [],
386
563
  markdownFiles: markdownFiles,
387
564
  uiConfig: uiConfig,
388
565
  terminalConfig: terminalConfig,
@@ -450,6 +627,7 @@ function importConfigs(importData, options = {}) {
450
627
  commands: { success: 0, failed: 0, skipped: 0 },
451
628
  rules: { success: 0, failed: 0, skipped: 0 },
452
629
  mcpServers: { success: 0, failed: 0, skipped: 0 },
630
+ plugins: { success: 0, failed: 0, skipped: 0 },
453
631
  markdownFiles: { success: 0, failed: 0, skipped: 0 },
454
632
  uiConfig: { success: 0, failed: 0, skipped: 0 },
455
633
  terminalConfig: { success: 0, failed: 0, skipped: 0 },
@@ -675,6 +853,143 @@ function importConfigs(importData, options = {}) {
675
853
  }
676
854
  }
677
855
 
856
+ // 导入 Plugins
857
+ if (importData.data.plugins && importData.data.plugins.length > 0) {
858
+ const plugins = importData.data.plugins;
859
+
860
+ try {
861
+ for (const plugin of plugins) {
862
+ try {
863
+ const pluginType = plugin.type || 'legacy';
864
+
865
+ // Determine target directory based on plugin type
866
+ let targetDir;
867
+ let registryPath;
868
+
869
+ if (pluginType === 'legacy') {
870
+ targetDir = path.join(LEGACY_PLUGINS_DIR, plugin.directory || plugin.name);
871
+ registryPath = LEGACY_PLUGINS_REGISTRY;
872
+ } else if (pluginType === 'native') {
873
+ // SECURITY: Never trust installPath from import data - construct it safely
874
+ const pluginId = plugin.id || plugin.name;
875
+ if (!pluginId) {
876
+ console.warn('[ConfigImport] Native plugin missing id/name, skipping');
877
+ results.plugins.failed++;
878
+ continue;
879
+ }
880
+ targetDir = path.join(os.homedir(), '.claude', 'plugins', pluginId);
881
+ registryPath = NATIVE_PLUGINS_REGISTRY;
882
+ } else {
883
+ console.warn(`[ConfigImport] Unknown plugin type: ${pluginType}`);
884
+ results.plugins.failed++;
885
+ continue;
886
+ }
887
+
888
+ // Check if plugin exists
889
+ if (fs.existsSync(targetDir)) {
890
+ if (!overwrite) {
891
+ results.plugins.skipped++;
892
+ continue;
893
+ }
894
+ // Remove existing plugin directory
895
+ fs.rmSync(targetDir, { recursive: true, force: true });
896
+ }
897
+
898
+ // Create plugin directory
899
+ ensureDir(targetDir);
900
+
901
+ // Write all plugin files from base64 content
902
+ const files = Array.isArray(plugin.files) ? plugin.files : [];
903
+ let failed = false;
904
+
905
+ for (const file of files) {
906
+ const filePath = resolveSafePath(targetDir, file.path);
907
+ if (!filePath) {
908
+ failed = true;
909
+ break;
910
+ }
911
+
912
+ ensureDir(path.dirname(filePath));
913
+
914
+ try {
915
+ if (file.encoding === SKILL_FILE_ENCODING) {
916
+ fs.writeFileSync(filePath, Buffer.from(file.content || '', SKILL_FILE_ENCODING));
917
+ } else {
918
+ fs.writeFileSync(filePath, file.content || '', file.encoding || 'utf8');
919
+ }
920
+ } catch (err) {
921
+ console.error(`[ConfigImport] Failed to write plugin file ${file.path}:`, err);
922
+ failed = true;
923
+ break;
924
+ }
925
+ }
926
+
927
+ if (failed || files.length === 0) {
928
+ results.plugins.failed++;
929
+ continue;
930
+ }
931
+
932
+ // Update appropriate registry
933
+ try {
934
+ ensureDir(path.dirname(registryPath));
935
+
936
+ if (pluginType === 'legacy') {
937
+ // Update legacy registry.json
938
+ const registry = readJsonFileSafe(registryPath) || {};
939
+ const pluginKey = plugin.directory || plugin.name;
940
+
941
+ if (plugin.registryEntry) {
942
+ registry[pluginKey] = plugin.registryEntry;
943
+ } else {
944
+ registry[pluginKey] = {
945
+ name: plugin.name,
946
+ version: plugin.version,
947
+ description: plugin.description,
948
+ author: plugin.author,
949
+ installedAt: new Date().toISOString()
950
+ };
951
+ }
952
+
953
+ fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2), 'utf8');
954
+ } else if (pluginType === 'native') {
955
+ // Update native installed_plugins.json
956
+ const installedPlugins = readJsonFileSafe(registryPath) || {};
957
+ const pluginId = plugin.id || plugin.name;
958
+
959
+ if (plugin.registryEntry) {
960
+ installedPlugins[pluginId] = {
961
+ ...plugin.registryEntry,
962
+ installPath: targetDir
963
+ };
964
+ } else {
965
+ installedPlugins[pluginId] = {
966
+ name: plugin.name,
967
+ version: plugin.version,
968
+ description: plugin.description,
969
+ author: plugin.author,
970
+ installPath: targetDir,
971
+ installedAt: new Date().toISOString()
972
+ };
973
+ }
974
+
975
+ fs.writeFileSync(registryPath, JSON.stringify(installedPlugins, null, 2), 'utf8');
976
+ }
977
+
978
+ results.plugins.success++;
979
+ } catch (err) {
980
+ console.error(`[ConfigImport] Failed to update plugin registry for ${plugin.name}:`, err);
981
+ results.plugins.failed++;
982
+ }
983
+ } catch (err) {
984
+ console.error(`[ConfigImport] Failed to import plugin ${plugin.name}:`, err);
985
+ results.plugins.failed++;
986
+ }
987
+ }
988
+ } catch (err) {
989
+ console.error('[ConfigImport] 导入 Plugins 失败:', err);
990
+ }
991
+ }
992
+
678
993
  // 导入 Commands
679
994
  if (commands && commands.length > 0) {
680
995
  try {
@@ -752,8 +1067,15 @@ function importConfigs(importData, options = {}) {
752
1067
  // 导入 Markdown 文件
753
1068
  if (markdownFiles && Object.keys(markdownFiles).length > 0 && overwrite) {
754
1069
  const { PATHS } = require('../../config/paths');
1070
+ // SECURITY: Whitelist allowed markdown files to prevent path traversal
1071
+ const ALLOWED_MARKDOWN_FILES = new Set(['CLAUDE.md', 'AGENTS.md', 'GEMINI.md']);
755
1072
  for (const [fileName, content] of Object.entries(markdownFiles)) {
756
1073
  try {
1074
+ if (!ALLOWED_MARKDOWN_FILES.has(fileName)) {
1075
+ console.warn(`[ConfigImport] Skipping disallowed markdown file: ${fileName}`);
1076
+ results.markdownFiles.failed++;
1077
+ continue;
1078
+ }
757
1079
  const filePath = path.join(PATHS.base, fileName);
758
1080
  fs.writeFileSync(filePath, content, 'utf8');
759
1081
  results.markdownFiles.success++;
@@ -952,6 +1274,7 @@ function generateImportSummary(results) {
952
1274
  { key: 'commands', label: 'Commands' },
953
1275
  { key: 'rules', label: 'Rules' },
954
1276
  { key: 'mcpServers', label: 'MCP服务器' },
1277
+ { key: 'plugins', label: '插件' },
955
1278
  { key: 'markdownFiles', label: 'Markdown文件' },
956
1279
  { key: 'uiConfig', label: 'UI配置' },
957
1280
  { key: 'terminalConfig', label: '终端配置' },
@@ -193,10 +193,10 @@ function updateChannel(channelId, updates) {
193
193
  throw new Error('Channel not found');
194
194
  }
195
195
 
196
- const channel = data.channels[index];
196
+ const oldChannel = data.channels[index];
197
197
 
198
198
  // 检查名称冲突
199
- if (updates.name && updates.name !== channel.name) {
199
+ if (updates.name && updates.name !== oldChannel.name) {
200
200
  const existing = data.channels.find(c => c.name === updates.name && c.id !== channelId);
201
201
  if (existing) {
202
202
  throw new Error(`Channel name "${updates.name}" already exists`);
@@ -204,13 +204,43 @@ function updateChannel(channelId, updates) {
204
204
  }
205
205
 
206
206
  data.channels[index] = {
207
- ...channel,
207
+ ...oldChannel,
208
208
  ...updates,
209
209
  id: channelId, // 保持 ID 不变
210
- createdAt: channel.createdAt, // 保持创建时间
210
+ createdAt: oldChannel.createdAt, // 保持创建时间
211
211
  updatedAt: Date.now()
212
212
  };
213
213
 
214
+ // Get proxy status
215
+ const { getGeminiProxyStatus } = require('../gemini-proxy-server');
216
+ const proxyStatus = getGeminiProxyStatus();
217
+ const isProxyRunning = proxyStatus.running;
218
+
219
+ // Fix 1: Detect enabled toggle (false → true) when proxy is OFF
220
+ if (!isProxyRunning && !oldChannel.enabled && data.channels[index].enabled) {
221
+ console.log(`[Gemini Settings-sync] Proxy is OFF and channel "${data.channels[index].name}" was enabled, syncing .env...`);
222
+ applyChannelToSettings(channelId, data.channels);
223
+ }
224
+
225
+ // Fix 2: Single-channel enforcement when proxy is OFF
226
+ if (!isProxyRunning && data.channels[index].enabled && !oldChannel.enabled) {
227
+ // Disable all other channels
228
+ data.channels.forEach((ch, i) => {
229
+ if (i !== index && ch.enabled) {
230
+ ch.enabled = false;
231
+ }
232
+ });
233
+ console.log(`[Gemini Single-channel mode] Enabled "${data.channels[index].name}", disabled all others`);
234
+ }
235
+
236
+ // Fix 3: Prevent disabling last enabled channel when proxy is OFF
237
+ if (!isProxyRunning && !data.channels[index].enabled && oldChannel.enabled) {
238
+ const enabledCount = data.channels.filter(ch => ch.enabled).length;
239
+ if (enabledCount === 0) {
240
+ throw new Error('无法禁用最后一个启用的渠道。请先启用其他渠道或启动动态切换。');
241
+ }
242
+ }
243
+
214
244
  saveChannels(data);
215
245
 
216
246
  // 更新 Gemini 配置文件
@@ -219,6 +249,65 @@ function updateChannel(channelId, updates) {
219
249
  return data.channels[index];
220
250
  }
221
251
 
252
+ /**
253
+ * 将指定渠道应用到 Gemini 配置文件
254
+ *
255
+ * @param {string} channelId - 渠道 ID
256
+ * @param {Array} channels - 渠道列表(可选,避免重复读取)
257
+ * @returns {Object} 应用的渠道
258
+ */
259
+ function applyChannelToSettings(channelId, channels = null) {
260
+ const data = channels ? { channels } : loadChannels();
261
+ const channel = data.channels.find(c => c.id === channelId);
262
+
263
+ if (!channel) {
264
+ throw new Error('Channel not found');
265
+ }
266
+
267
+ const geminiDir = getGeminiDir();
268
+
269
+ if (!fs.existsSync(geminiDir)) {
270
+ fs.mkdirSync(geminiDir, { recursive: true });
271
+ }
272
+
273
+ const envPath = path.join(geminiDir, '.env');
274
+
275
+ // 构建 .env 内容
276
+ const envContent = `GOOGLE_GEMINI_BASE_URL=${channel.baseUrl}
277
+ GEMINI_API_KEY=${channel.apiKey}
278
+ GEMINI_MODEL=${channel.model}
279
+ `;
280
+
281
+ fs.writeFileSync(envPath, envContent, 'utf8');
282
+
283
+ // 设置 .env 文件权限为 600 (仅所有者可读写)
284
+ if (process.platform !== 'win32') {
285
+ fs.chmodSync(envPath, 0o600);
286
+ }
287
+
288
+ // 确保 settings.json 存在并配置正确的认证模式
289
+ const settingsPath = path.join(geminiDir, 'settings.json');
290
+ let settings = {};
291
+
292
+ if (fs.existsSync(settingsPath)) {
293
+ try {
294
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
295
+ } catch (err) {
296
+ console.warn('[Gemini Channels] Failed to read settings.json, creating new');
297
+ }
298
+ }
299
+
300
+ // 设置认证模式为 gemini-api-key(第三方 API)
301
+ settings.security = settings.security || {};
302
+ settings.security.auth = settings.security.auth || {};
303
+ settings.security.auth.selectedType = 'gemini-api-key';
304
+
305
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
306
+
307
+ console.log(`[Gemini Channels] Applied channel ${channel.name} to .env`);
308
+ return channel;
309
+ }
310
+
222
311
  // 删除渠道
223
312
  function deleteChannel(channelId) {
224
313
  const data = loadChannels();
@@ -329,5 +418,6 @@ module.exports = {
329
418
  getEnabledChannels,
330
419
  saveChannelOrder,
331
420
  isProxyConfig,
332
- getGeminiDir
421
+ getGeminiDir,
422
+ applyChannelToSettings
333
423
  };