coding-tool-x 3.3.6 → 3.3.8

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/web/assets/{Analytics-TtaduRqL.js → Analytics-DLpoDZ2M.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-BP2lLBMN.js → ConfigTemplates-D_hRb55W.js} +1 -1
  4. package/dist/web/assets/Home-BMoFdAwy.css +1 -0
  5. package/dist/web/assets/Home-DNwp-0J-.js +1 -0
  6. package/dist/web/assets/{PluginManager-HmISlyMK.js → PluginManager-JXsyym1s.js} +1 -1
  7. package/dist/web/assets/{ProjectList-DoN8Hjbu.js → ProjectList-DZWSeb-q.js} +1 -1
  8. package/dist/web/assets/{SessionList-Da8BYzNi.js → SessionList-Cs624DR3.js} +1 -1
  9. package/dist/web/assets/{SkillManager-DqLAXh9o.js → SkillManager-bEliz7qz.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-B_TxOgPW.js → WorkspaceManager-J3RecFGn.js} +1 -1
  11. package/dist/web/assets/{icons-B29onFfZ.js → icons-Cuc23WS7.js} +1 -1
  12. package/dist/web/assets/index-BXeSvAwU.js +2 -0
  13. package/dist/web/assets/index-DWAC3Tdv.css +1 -0
  14. package/dist/web/index.html +3 -3
  15. package/package.json +3 -2
  16. package/src/commands/daemon.js +44 -6
  17. package/src/commands/toggle-proxy.js +100 -5
  18. package/src/config/default.js +1 -1
  19. package/src/config/model-metadata.js +2 -2
  20. package/src/config/model-metadata.json +7 -2
  21. package/src/config/paths.js +102 -19
  22. package/src/server/api/channels.js +9 -0
  23. package/src/server/api/codex-channels.js +9 -0
  24. package/src/server/api/codex-proxy.js +22 -11
  25. package/src/server/api/gemini-proxy.js +22 -11
  26. package/src/server/api/mcp.js +26 -4
  27. package/src/server/api/oauth-credentials.js +163 -0
  28. package/src/server/api/opencode-proxy.js +22 -10
  29. package/src/server/api/plugins.js +3 -1
  30. package/src/server/api/proxy.js +39 -44
  31. package/src/server/api/skills.js +91 -13
  32. package/src/server/codex-proxy-server.js +1 -11
  33. package/src/server/index.js +26 -2
  34. package/src/server/services/channels.js +18 -22
  35. package/src/server/services/codex-channels.js +124 -175
  36. package/src/server/services/codex-config.js +2 -5
  37. package/src/server/services/codex-settings-manager.js +12 -348
  38. package/src/server/services/config-export-service.js +572 -117
  39. package/src/server/services/gemini-channels.js +11 -9
  40. package/src/server/services/mcp-client.js +70 -13
  41. package/src/server/services/mcp-service.js +74 -29
  42. package/src/server/services/model-detector.js +1 -0
  43. package/src/server/services/native-keychain.js +243 -0
  44. package/src/server/services/native-oauth-adapters.js +890 -0
  45. package/src/server/services/oauth-credentials-service.js +786 -0
  46. package/src/server/services/oauth-utils.js +49 -0
  47. package/src/server/services/opencode-channels.js +13 -9
  48. package/src/server/services/opencode-settings-manager.js +169 -16
  49. package/src/server/services/plugins-service.js +22 -1
  50. package/src/server/services/settings-manager.js +13 -0
  51. package/src/server/services/skill-service.js +712 -332
  52. package/src/utils/port-helper.js +87 -2
  53. package/dist/web/assets/Home-BsSioaaB.css +0 -1
  54. package/dist/web/assets/Home-CbbyopS-.js +0 -1
  55. package/dist/web/assets/index-By3mDEvx.js +0 -2
  56. package/dist/web/assets/index-CsWInMQV.css +0 -1
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const { PATHS, NATIVE_PATHS } = require('../../config/paths');
5
+ const { clearNativeOAuth } = require('./native-oauth-adapters');
5
6
 
6
7
  /**
7
8
  * Gemini 渠道管理服务(多渠道架构)
@@ -252,14 +253,6 @@ function updateChannel(channelId, updates) {
252
253
  console.log(`[Gemini Single-channel mode] Enabled "${nextChannel.name}", disabled all others`);
253
254
  }
254
255
 
255
- // Prevent disabling last enabled channel when proxy is OFF
256
- if (!isProxyRunning && !nextChannel.enabled && oldChannel.enabled) {
257
- const enabledCount = data.channels.filter(ch => ch.enabled).length;
258
- if (enabledCount === 0) {
259
- throw new Error('无法禁用最后一个启用的渠道。请先启用其他渠道或启动动态切换。');
260
- }
261
- }
262
-
263
256
  saveChannels(data);
264
257
 
265
258
  // Only sync .env when proxy is OFF.
@@ -299,6 +292,8 @@ function applyChannelToSettings(channelId, channels = null) {
299
292
  saveChannels(data);
300
293
  }
301
294
 
295
+ clearNativeOAuth('gemini');
296
+
302
297
  const geminiDir = getGeminiDir();
303
298
 
304
299
  if (!fs.existsSync(geminiDir)) {
@@ -455,6 +450,12 @@ function saveChannelOrder(order) {
455
450
  saveChannels(data);
456
451
  }
457
452
 
453
+ function disableAllChannels() {
454
+ const data = loadChannels();
455
+ data.channels.forEach(ch => { ch.enabled = false; });
456
+ saveChannels(data);
457
+ }
458
+
458
459
  module.exports = {
459
460
  getChannels,
460
461
  createChannel,
@@ -465,5 +466,6 @@ module.exports = {
465
466
  saveChannelOrder,
466
467
  isProxyConfig,
467
468
  getGeminiDir,
468
- applyChannelToSettings
469
+ applyChannelToSettings,
470
+ disableAllChannels
469
471
  };
@@ -107,6 +107,65 @@ function resolveWindowsSpawnCommand(command, env, cwd) {
107
107
  return normalizedCommand;
108
108
  }
109
109
 
110
+ function getCommandInstallHint(command) {
111
+ const normalized = path.basename(stripWrappingQuotes(command)).toLowerCase()
112
+
113
+ if (['node', 'node.exe', 'npm', 'npm.cmd', 'npx', 'npx.cmd'].includes(normalized)) {
114
+ return '请先安装 Node.js,并确认 `node` / `npm` / `npx` 已加入 PATH。'
115
+ }
116
+
117
+ if (['uv', 'uv.exe', 'uvx', 'uvx.exe'].includes(normalized)) {
118
+ return '请先安装 `uv`,并确认 `uv` / `uvx` 可以在终端直接执行。'
119
+ }
120
+
121
+ if (['python', 'python.exe', 'python3', 'py'].includes(normalized)) {
122
+ return '请先安装 Python,并确认对应命令已加入 PATH。'
123
+ }
124
+
125
+ if (process.platform === 'win32' && ['netstat', 'taskkill'].includes(normalized)) {
126
+ return '请确认 Windows 自带命令可用,并检查 `C:\\Windows\\System32` 是否在 PATH 中。'
127
+ }
128
+
129
+ return `请确认已安装 "${command}",或改用可执行文件的绝对路径。`
130
+ }
131
+
132
+ function createMissingCommandHint(command, resolvedCommand, env = {}) {
133
+ const pathKey = getPathEnvKey(env)
134
+ const pathValue = typeof env[pathKey] === 'string' ? env[pathKey] : ''
135
+ const pathPreview = pathValue
136
+ ? pathValue.split(path.delimiter).slice(0, 5).join(path.delimiter)
137
+ : ''
138
+ const commandHint = resolvedCommand === command
139
+ ? command
140
+ : `${command} (resolved: ${resolvedCommand})`
141
+
142
+ const details = [
143
+ getCommandInstallHint(command),
144
+ process.platform === 'win32'
145
+ ? 'Windows 可优先尝试 `.cmd` / `.exe` 文件,必要时直接填写绝对路径。'
146
+ : `可尝试填写绝对路径(例如 \`/usr/bin/node\` 或 \`$(which ${command})\`)。`
147
+ ]
148
+
149
+ if (pathPreview) {
150
+ details.push(`当前 PATH 前 5 项: ${pathPreview}`)
151
+ } else {
152
+ details.push('当前 PATH 为空,请检查环境变量配置。')
153
+ }
154
+
155
+ return {
156
+ type: 'missing-command',
157
+ command,
158
+ resolvedCommand,
159
+ title: `命令 "${commandHint}" 未找到`,
160
+ details
161
+ }
162
+ }
163
+
164
+ function buildMissingCommandMessage(command, resolvedCommand, env = {}) {
165
+ const hint = createMissingCommandHint(command, resolvedCommand, env)
166
+ return [hint.title, ...hint.details].join('\n')
167
+ }
168
+
110
169
  // ============================================================================
111
170
  // McpClient
112
171
  // ============================================================================
@@ -356,19 +415,11 @@ class McpClient extends EventEmitter {
356
415
 
357
416
  this._child.on('error', (err) => {
358
417
  if (err.code === 'ENOENT') {
359
- const pathKey = getPathEnvKey(mergedEnv);
360
- const pathValue = typeof mergedEnv[pathKey] === 'string' ? mergedEnv[pathKey] : '';
361
- const pathHint = pathValue
362
- ? `\n Current PATH: ${pathValue.split(path.delimiter).slice(0, 5).join(path.delimiter)}\n (showing first 5 entries)`
363
- : '\n PATH is not set!';
364
- const commandHint = resolvedCommand === command
365
- ? command
366
- : `${command} (resolved: ${resolvedCommand})`;
418
+ const hint = createMissingCommandHint(command, resolvedCommand, mergedEnv)
367
419
  settle(new McpClientError(
368
- `Command "${commandHint}" not found. Please check:\n` +
369
- ` 1. Is "${command}" installed?\n` +
370
- ` 2. Try using absolute path${process.platform === 'win32' ? ' (e.g., C:\\\\Program Files\\\\nodejs\\\\npx.cmd)' : ` (e.g., /usr/bin/node or $(which ${command}))`}\n` +
371
- ` 3. Check your PATH environment variable${pathHint}`
420
+ buildMissingCommandMessage(command, resolvedCommand, mergedEnv),
421
+ undefined,
422
+ { hint }
372
423
  ));
373
424
  } else {
374
425
  settle(new McpClientError(`Failed to start process: ${err.message}`));
@@ -873,5 +924,11 @@ async function createClient(serverSpec, options = {}) {
873
924
  module.exports = {
874
925
  McpClient,
875
926
  McpClientError,
876
- createClient
927
+ createClient,
928
+ buildMissingCommandMessage,
929
+ createMissingCommandHint,
930
+ _test: {
931
+ createMissingCommandHint,
932
+ buildMissingCommandMessage
933
+ }
877
934
  };
@@ -11,7 +11,7 @@ const toml = require('@iarna/toml');
11
11
  const { spawn } = require('child_process');
12
12
  const http = require('http');
13
13
  const https = require('https');
14
- const { McpClient } = require('./mcp-client');
14
+ const { McpClient, buildMissingCommandMessage, createMissingCommandHint } = require('./mcp-client');
15
15
  const { NATIVE_PATHS } = require('../../config/paths');
16
16
  const { resolvePreferredHomeDir } = require('../../utils/home-dir');
17
17
 
@@ -290,15 +290,16 @@ function writeJsonFile(filePath, data) {
290
290
  * 安全读取 TOML 文件
291
291
  */
292
292
  function readTomlFile(filePath, defaultValue = {}) {
293
+ if (!fs.existsSync(filePath)) {
294
+ return defaultValue;
295
+ }
296
+
293
297
  try {
294
- if (fs.existsSync(filePath)) {
295
- const content = fs.readFileSync(filePath, 'utf-8');
296
- return toml.parse(content);
297
- }
298
+ const content = fs.readFileSync(filePath, 'utf-8');
299
+ return toml.parse(content);
298
300
  } catch (err) {
299
- console.error(`[MCP] Failed to read ${filePath}:`, err.message);
301
+ throw new Error(`Failed to parse ${filePath}: ${err.message}`);
300
302
  }
301
- return defaultValue;
302
303
  }
303
304
 
304
305
  /**
@@ -501,6 +502,19 @@ function resolveWindowsSpawnCommand(command, env, cwd) {
501
502
  return normalizedCommand;
502
503
  }
503
504
 
505
+ function extractMcpHint(error) {
506
+ return error?.data?.hint || error?.hint || null;
507
+ }
508
+
509
+ function buildMcpFailureResult(error, fallbackMessage, duration) {
510
+ const hint = extractMcpHint(error);
511
+ return {
512
+ message: hint?.title || fallbackMessage || error?.message || '操作失败',
513
+ hint,
514
+ duration
515
+ };
516
+ }
517
+
504
518
  // ============================================================================
505
519
  // MCP 数据管理
506
520
  // ============================================================================
@@ -580,14 +594,14 @@ async function saveServer(server, options = {}) {
580
594
  server.apps = normalizeServerApps(server.apps, previousApps || DEFAULT_SERVER_APPS);
581
595
  }
582
596
 
583
- servers[server.id] = server;
584
- writeJsonFile(MCP_SERVERS_FILE, servers);
585
-
586
597
  // 同步到各平台配置
587
598
  if (syncPlatforms) {
588
599
  await syncServerToAllPlatforms(server, previousApps);
589
600
  }
590
601
 
602
+ servers[server.id] = server;
603
+ writeJsonFile(MCP_SERVERS_FILE, servers);
604
+
591
605
  return server;
592
606
  }
593
607
 
@@ -602,12 +616,12 @@ async function deleteServer(id) {
602
616
  return false;
603
617
  }
604
618
 
605
- delete servers[id];
606
- writeJsonFile(MCP_SERVERS_FILE, servers);
607
-
608
619
  // 从所有平台配置中移除
609
620
  await removeServerFromAllPlatforms(id);
610
621
 
622
+ delete servers[id];
623
+ writeJsonFile(MCP_SERVERS_FILE, servers);
624
+
611
625
  return true;
612
626
  }
613
627
 
@@ -629,8 +643,6 @@ async function toggleServerApp(serverId, app, enabled) {
629
643
  server.apps[app] = enabled;
630
644
  server.updatedAt = Date.now();
631
645
 
632
- writeJsonFile(MCP_SERVERS_FILE, servers);
633
-
634
646
  // 同步到对应平台
635
647
  if (enabled) {
636
648
  await syncServerToPlatform(server, app);
@@ -638,6 +650,8 @@ async function toggleServerApp(serverId, app, enabled) {
638
650
  await removeServerFromPlatform(serverId, app);
639
651
  }
640
652
 
653
+ writeJsonFile(MCP_SERVERS_FILE, servers);
654
+
641
655
  return server;
642
656
  }
643
657
 
@@ -777,6 +791,7 @@ async function removeServerFromPlatform(serverId, platform) {
777
791
  console.log(`[MCP] Removed "${serverId}" from ${platform}`);
778
792
  } catch (err) {
779
793
  console.error(`[MCP] Failed to remove "${serverId}" from ${platform}:`, err.message);
794
+ throw err;
780
795
  }
781
796
  }
782
797
 
@@ -820,14 +835,22 @@ function removeFromClaudeConfig(serverId) {
820
835
  * 同步到 Codex 配置
821
836
  */
822
837
  function syncToCodexConfig(server) {
838
+ if (!fs.existsSync(CODEX_CONFIG_PATH)) {
839
+ throw new Error('Codex config.toml not found. Please run Codex CLI at least once before syncing MCP servers.');
840
+ }
841
+
823
842
  const config = readTomlFile(CODEX_CONFIG_PATH, {});
843
+ const nextSpec = convertToCodexFormat(server.server);
824
844
 
825
845
  if (!config.mcp_servers) {
826
846
  config.mcp_servers = {};
827
847
  }
828
848
 
829
- // 转换为 Codex TOML 格式
830
- config.mcp_servers[server.id] = convertToCodexFormat(server.server);
849
+ if (JSON.stringify(config.mcp_servers[server.id] || null) === JSON.stringify(nextSpec)) {
850
+ return;
851
+ }
852
+
853
+ config.mcp_servers[server.id] = nextSpec;
831
854
 
832
855
  writeTomlFile(CODEX_CONFIG_PATH, config);
833
856
  }
@@ -836,10 +859,17 @@ function syncToCodexConfig(server) {
836
859
  * 从 Codex 配置移除
837
860
  */
838
861
  function removeFromCodexConfig(serverId) {
862
+ if (!fs.existsSync(CODEX_CONFIG_PATH)) {
863
+ return;
864
+ }
865
+
839
866
  const config = readTomlFile(CODEX_CONFIG_PATH, {});
840
867
 
841
868
  if (config.mcp_servers && config.mcp_servers[serverId]) {
842
869
  delete config.mcp_servers[serverId];
870
+ if (Object.keys(config.mcp_servers).length === 0) {
871
+ delete config.mcp_servers;
872
+ }
843
873
  writeTomlFile(CODEX_CONFIG_PATH, config);
844
874
  }
845
875
  }
@@ -1301,10 +1331,12 @@ async function testServer(serverId) {
1301
1331
  return { success: false, message: `不支持的服务器类型: ${type}` };
1302
1332
  }
1303
1333
  } catch (err) {
1334
+ const failure = buildMcpFailureResult(err, err.message, Date.now() - startTime);
1304
1335
  return {
1305
1336
  success: false,
1306
- message: err.message,
1307
- duration: Date.now() - startTime
1337
+ message: failure.message,
1338
+ hint: failure.hint,
1339
+ duration: failure.duration
1308
1340
  };
1309
1341
  }
1310
1342
  }
@@ -1370,10 +1402,11 @@ async function testStdioServer(spec) {
1370
1402
 
1371
1403
  child.on('error', (err) => {
1372
1404
  if (err.code === 'ENOENT') {
1373
- const commandLabel = resolvedCommand === command ? command : `${command} (resolved: ${resolvedCommand})`;
1405
+ const hint = createMissingCommandHint(command, resolvedCommand, mergedEnv);
1374
1406
  done({
1375
1407
  success: false,
1376
- message: `命令 "${commandLabel}" 未找到,请确保已安装(Windows 可尝试 npx.cmd 或绝对路径)`,
1408
+ message: buildMissingCommandMessage(command, resolvedCommand, mergedEnv),
1409
+ hint,
1377
1410
  duration: Date.now() - startTime
1378
1411
  });
1379
1412
  } else {
@@ -1423,10 +1456,12 @@ async function testStdioServer(spec) {
1423
1456
  }, timeout);
1424
1457
 
1425
1458
  } catch (err) {
1459
+ const failure = buildMcpFailureResult(err, `测试失败: ${err.message}`, Date.now() - startTime);
1426
1460
  done({
1427
1461
  success: false,
1428
- message: `测试失败: ${err.message}`,
1429
- duration: Date.now() - startTime
1462
+ message: failure.message,
1463
+ hint: failure.hint,
1464
+ duration: failure.duration
1430
1465
  });
1431
1466
  }
1432
1467
  });
@@ -1571,11 +1606,14 @@ async function getServerTools(serverId) {
1571
1606
  mcpClientPool.delete(serverId);
1572
1607
  }
1573
1608
 
1609
+ const failure = buildMcpFailureResult(err, err.message, Date.now() - startTime);
1574
1610
  return {
1575
1611
  tools: [],
1576
- duration: Date.now() - startTime,
1612
+ duration: failure.duration,
1577
1613
  status: 'error',
1578
- error: err.message
1614
+ error: failure.message,
1615
+ message: failure.message,
1616
+ hint: failure.hint
1579
1617
  };
1580
1618
  }
1581
1619
  }
@@ -1677,14 +1715,17 @@ async function callServerTool(serverId, toolName, arguments = {}) {
1677
1715
  mcpClientPool.delete(serverId);
1678
1716
  }
1679
1717
 
1718
+ const failure = buildMcpFailureResult(err, err.message, Date.now() - startTime);
1680
1719
  return {
1681
1720
  result: {
1682
- error: err.message,
1721
+ error: failure.message,
1683
1722
  code: err.code,
1684
1723
  data: err.data
1685
1724
  },
1686
- duration: Date.now() - startTime,
1687
- isError: true
1725
+ duration: failure.duration,
1726
+ isError: true,
1727
+ message: failure.message,
1728
+ hint: failure.hint
1688
1729
  };
1689
1730
  }
1690
1731
  }
@@ -1864,5 +1905,9 @@ module.exports = {
1864
1905
  callServerTool,
1865
1906
  updateServerStatus,
1866
1907
  updateServerOrder,
1867
- exportServers
1908
+ exportServers,
1909
+ _test: {
1910
+ extractMcpHint,
1911
+ buildMcpFailureResult
1912
+ }
1868
1913
  };
@@ -23,6 +23,7 @@ const MODEL_PRIORITY = {
23
23
  'claude-haiku-4-5-20251001'
24
24
  ],
25
25
  codex: [
26
+ 'gpt-5.4',
26
27
  'gpt-5.2-codex',
27
28
  'gpt-5.1-codex-max',
28
29
  'gpt-5.1-codex',
@@ -0,0 +1,243 @@
1
+ const { spawnSync } = require('child_process');
2
+
3
+ function runCommand(command, args, options = {}) {
4
+ const result = spawnSync(command, args, {
5
+ encoding: 'utf8',
6
+ maxBuffer: 10 * 1024 * 1024,
7
+ ...options
8
+ });
9
+
10
+ if (result.error) {
11
+ throw result.error;
12
+ }
13
+
14
+ return result;
15
+ }
16
+
17
+ function getWindowsPowerShellCommand() {
18
+ return process.env.ComSpec && process.env.ComSpec.toLowerCase().includes('powershell')
19
+ ? process.env.ComSpec
20
+ : 'powershell.exe';
21
+ }
22
+
23
+ function runPowerShell(script, env = {}) {
24
+ return runCommand(getWindowsPowerShellCommand(), [
25
+ '-NoProfile',
26
+ '-NonInteractive',
27
+ '-Command',
28
+ script
29
+ ], {
30
+ env: {
31
+ ...process.env,
32
+ ...env
33
+ }
34
+ });
35
+ }
36
+
37
+ function isSupported() {
38
+ if (process.platform === 'darwin') return true;
39
+ if (process.platform === 'win32') return true;
40
+
41
+ if (process.platform === 'linux') {
42
+ try {
43
+ const result = runCommand('secret-tool', ['--help']);
44
+ return result.status === 0 || result.status === 1;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ return false;
51
+ }
52
+
53
+ function getPassword(service, account) {
54
+ if (!service || !account) {
55
+ return null;
56
+ }
57
+
58
+ try {
59
+ if (process.platform === 'darwin') {
60
+ const result = runCommand('security', [
61
+ 'find-generic-password',
62
+ '-a',
63
+ String(account),
64
+ '-w',
65
+ '-s',
66
+ String(service)
67
+ ]);
68
+
69
+ if (result.status !== 0) {
70
+ return null;
71
+ }
72
+
73
+ return String(result.stdout || '').trim() || null;
74
+ }
75
+
76
+ if (process.platform === 'linux') {
77
+ const result = runCommand('secret-tool', [
78
+ 'lookup',
79
+ 'service',
80
+ String(service),
81
+ 'account',
82
+ String(account)
83
+ ]);
84
+
85
+ if (result.status !== 0) {
86
+ return null;
87
+ }
88
+
89
+ return String(result.stdout || '').trim() || null;
90
+ }
91
+
92
+ if (process.platform === 'win32') {
93
+ const script = `
94
+ Add-Type -AssemblyName System.Runtime.WindowsRuntime
95
+ $vault = New-Object Windows.Security.Credentials.PasswordVault
96
+ try {
97
+ $cred = $vault.Retrieve($env:CC_TOOL_SERVICE, $env:CC_TOOL_ACCOUNT)
98
+ $cred.RetrievePassword()
99
+ [Console]::Out.Write($cred.Password)
100
+ exit 0
101
+ } catch {
102
+ exit 2
103
+ }
104
+ `;
105
+ const result = runPowerShell(script, {
106
+ CC_TOOL_SERVICE: String(service),
107
+ CC_TOOL_ACCOUNT: String(account)
108
+ });
109
+
110
+ if (result.status !== 0) {
111
+ return null;
112
+ }
113
+
114
+ return String(result.stdout || '').trim() || null;
115
+ }
116
+ } catch {
117
+ return null;
118
+ }
119
+
120
+ return null;
121
+ }
122
+
123
+ function setPassword(service, account, password) {
124
+ if (!service || !account) {
125
+ return false;
126
+ }
127
+
128
+ try {
129
+ if (process.platform === 'darwin') {
130
+ const result = runCommand('security', [
131
+ 'add-generic-password',
132
+ '-U',
133
+ '-a',
134
+ String(account),
135
+ '-s',
136
+ String(service),
137
+ '-w',
138
+ String(password ?? '')
139
+ ]);
140
+ return result.status === 0;
141
+ }
142
+
143
+ if (process.platform === 'linux') {
144
+ deletePassword(service, account);
145
+ const result = runCommand('secret-tool', [
146
+ 'store',
147
+ '--label',
148
+ String(service),
149
+ 'service',
150
+ String(service),
151
+ 'account',
152
+ String(account)
153
+ ], {
154
+ input: String(password ?? '')
155
+ });
156
+ return result.status === 0;
157
+ }
158
+
159
+ if (process.platform === 'win32') {
160
+ const script = `
161
+ Add-Type -AssemblyName System.Runtime.WindowsRuntime
162
+ $vault = New-Object Windows.Security.Credentials.PasswordVault
163
+ try {
164
+ $existing = $vault.Retrieve($env:CC_TOOL_SERVICE, $env:CC_TOOL_ACCOUNT)
165
+ $vault.Remove($existing)
166
+ } catch {}
167
+ $credential = New-Object Windows.Security.Credentials.PasswordCredential($env:CC_TOOL_SERVICE, $env:CC_TOOL_ACCOUNT, $env:CC_TOOL_PASSWORD)
168
+ $vault.Add($credential)
169
+ exit 0
170
+ `;
171
+ const result = runPowerShell(script, {
172
+ CC_TOOL_SERVICE: String(service),
173
+ CC_TOOL_ACCOUNT: String(account),
174
+ CC_TOOL_PASSWORD: String(password ?? '')
175
+ });
176
+ return result.status === 0;
177
+ }
178
+ } catch {
179
+ return false;
180
+ }
181
+
182
+ return false;
183
+ }
184
+
185
+ function deletePassword(service, account) {
186
+ if (!service || !account) {
187
+ return false;
188
+ }
189
+
190
+ try {
191
+ if (process.platform === 'darwin') {
192
+ const result = runCommand('security', [
193
+ 'delete-generic-password',
194
+ '-a',
195
+ String(account),
196
+ '-s',
197
+ String(service)
198
+ ]);
199
+ return result.status === 0;
200
+ }
201
+
202
+ if (process.platform === 'linux') {
203
+ const result = runCommand('secret-tool', [
204
+ 'clear',
205
+ 'service',
206
+ String(service),
207
+ 'account',
208
+ String(account)
209
+ ]);
210
+ return result.status === 0;
211
+ }
212
+
213
+ if (process.platform === 'win32') {
214
+ const script = `
215
+ Add-Type -AssemblyName System.Runtime.WindowsRuntime
216
+ $vault = New-Object Windows.Security.Credentials.PasswordVault
217
+ try {
218
+ $credential = $vault.Retrieve($env:CC_TOOL_SERVICE, $env:CC_TOOL_ACCOUNT)
219
+ $vault.Remove($credential)
220
+ exit 0
221
+ } catch {
222
+ exit 2
223
+ }
224
+ `;
225
+ const result = runPowerShell(script, {
226
+ CC_TOOL_SERVICE: String(service),
227
+ CC_TOOL_ACCOUNT: String(account)
228
+ });
229
+ return result.status === 0;
230
+ }
231
+ } catch {
232
+ return false;
233
+ }
234
+
235
+ return false;
236
+ }
237
+
238
+ module.exports = {
239
+ isSupported,
240
+ getPassword,
241
+ setPassword,
242
+ deletePassword
243
+ };