ai-account-switch 1.9.0 → 1.12.0

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.
@@ -9,6 +9,37 @@ function maskApiKey(apiKey) {
9
9
  return `${apiKey.substring(0, 4)}****${apiKey.substring(apiKey.length - 4)}`;
10
10
  }
11
11
 
12
+ /**
13
+ * Mask sensitive value for display
14
+ * Shows first 2 chars + fixed 6 stars + last 2 chars for sensitive variables
15
+ * 用于显示敏感环境变量值,前2字符+固定6星号+后2字符
16
+ * @param {string} key - Variable name to check if sensitive
17
+ * @param {string} value - Value to mask
18
+ * @returns {string} Masked value or original value if not sensitive
19
+ */
20
+ function maskEnvValue(key, value) {
21
+ if (!key || !value) return value;
22
+
23
+ // Check if variable name contains sensitive keywords
24
+ const isSensitive = key.includes('KEY') || key.includes('TOKEN') || key.includes('SECRET') || key.includes('PASSWORD');
25
+
26
+ if (!isSensitive) {
27
+ return value;
28
+ }
29
+
30
+ // For sensitive values, show first 2 + fixed 6 stars + last 2
31
+ const strValue = String(value);
32
+ if (strValue.length <= 4) {
33
+ // If value is too short, show all stars
34
+ return '*'.repeat(strValue.length);
35
+ }
36
+
37
+ const firstTwo = strValue.substring(0, 2);
38
+ const lastTwo = strValue.substring(strValue.length - 2);
39
+
40
+ return firstTwo + '******' + lastTwo;
41
+ }
42
+
12
43
  /**
13
44
  * Validate account by testing API key
14
45
  * 验证账号的 API key 是否有效
@@ -31,5 +62,6 @@ async function validateAccount(apiKey, apiUrl) {
31
62
 
32
63
  module.exports = {
33
64
  maskApiKey,
65
+ maskEnvValue,
34
66
  validateAccount
35
67
  };
@@ -42,6 +42,17 @@ const {
42
42
  testMcpServer
43
43
  } = require('./mcp');
44
44
 
45
+ const {
46
+ listEnv,
47
+ addEnv,
48
+ setEnv,
49
+ removeEnv,
50
+ unsetEnv,
51
+ showEnv,
52
+ clearEnv,
53
+ editEnv
54
+ } = require('./env');
55
+
45
56
  module.exports = {
46
57
  // Account management commands
47
58
  addAccount,
@@ -74,5 +85,15 @@ module.exports = {
74
85
  disableMcpServer,
75
86
  showEnabledMcpServers,
76
87
  syncMcpConfig,
77
- testMcpServer
88
+ testMcpServer,
89
+
90
+ // Environment variable management commands
91
+ listEnv,
92
+ addEnv,
93
+ setEnv,
94
+ removeEnv,
95
+ unsetEnv,
96
+ showEnv,
97
+ clearEnv,
98
+ editEnv
78
99
  };
@@ -10,6 +10,8 @@ const config = new ConfigManager();
10
10
  */
11
11
  async function addMcpServer(name) {
12
12
  try {
13
+ const { MCP_SCOPES, DEFAULT_MCP_SCOPE } = require('../config');
14
+
13
15
  if (!name) {
14
16
  const { serverName } = await inquirer.prompt([{
15
17
  type: 'input',
@@ -79,8 +81,22 @@ async function addMcpServer(name) {
79
81
  const { description } = await inquirer.prompt([{ type: 'input', name: 'description', message: 'Enter description:', default: '' }]);
80
82
  serverData.description = description;
81
83
 
84
+ // Ask for default scope
85
+ const { scope } = await inquirer.prompt([{
86
+ type: 'list',
87
+ name: 'scope',
88
+ message: 'Select default scope (默认作用范围):',
89
+ choices: [
90
+ { name: 'local - Only current project (仅当前项目)', value: MCP_SCOPES.LOCAL },
91
+ { name: 'project - Share with project members (与项目成员共享)', value: MCP_SCOPES.PROJECT },
92
+ { name: 'user - All projects for current user (当前用户所有项目)', value: MCP_SCOPES.USER }
93
+ ],
94
+ default: DEFAULT_MCP_SCOPE
95
+ }]);
96
+ serverData.scope = scope;
97
+
82
98
  config.addMcpServer(name, serverData);
83
- console.log(chalk.green(`✓ MCP server '${name}' added successfully!`));
99
+ console.log(chalk.green(`✓ MCP server '${name}' added successfully with scope: ${scope}!`));
84
100
 
85
101
  // Auto-test the server
86
102
  console.log(chalk.cyan('\nTesting server availability...'));
@@ -101,7 +117,7 @@ async function addMcpServer(name) {
101
117
  */
102
118
  async function listMcpServers() {
103
119
  try {
104
- const servers = config.getAllMcpServers();
120
+ const servers = config.getAllAvailableMcpServers();
105
121
  const projectServers = config.getEnabledMcpServers();
106
122
 
107
123
  if (Object.keys(servers).length === 0) {
@@ -110,12 +126,16 @@ async function listMcpServers() {
110
126
  }
111
127
 
112
128
  console.log(chalk.bold.cyan('\n📋 Available MCP servers:\n'));
113
- console.log(' Name Type Active Description');
114
- console.log(' ─────────────────────────────────────────────────────────────');
115
-
116
- Object.values(servers).forEach(server => {
117
- const isActive = projectServers.includes(server.name) ? chalk.green('✓') : ' ';
118
- console.log(` ${server.name.padEnd(12)} ${server.type.padEnd(7)} ${isActive} ${server.description || ''}`);
129
+ console.log(' Name Type Active Scope Description');
130
+ console.log(' ───────────────────────────────────────────────────────────────────────');
131
+
132
+ Object.entries(servers).forEach(([key, server]) => {
133
+ const name = server.name || key;
134
+ const type = server.type || 'unknown';
135
+ const isActive = projectServers.includes(name) ? chalk.green('✓') : ' ';
136
+ const scope = server.scope || 'local';
137
+ const scopeDisplay = scope.padEnd(10);
138
+ console.log(` ${name.padEnd(12)} ${type.padEnd(7)} ${isActive} ${scopeDisplay} ${server.description || ''}`);
119
139
  });
120
140
 
121
141
  const activeCount = projectServers.length;
@@ -133,7 +153,7 @@ async function listMcpServers() {
133
153
  async function showMcpServer(name) {
134
154
  try {
135
155
  if (!name) {
136
- const servers = config.getAllMcpServers();
156
+ const servers = config.getAllAvailableMcpServers();
137
157
  if (Object.keys(servers).length === 0) {
138
158
  console.log(chalk.yellow('No MCP servers configured'));
139
159
  return;
@@ -155,6 +175,7 @@ async function showMcpServer(name) {
155
175
 
156
176
  console.log(chalk.bold.cyan(`\n📋 MCP Server: ${name}\n`));
157
177
  console.log(chalk.bold('Type:'), server.type);
178
+ console.log(chalk.bold('Scope:'), server.scope || 'local');
158
179
  console.log(chalk.bold('Description:'), server.description || 'N/A');
159
180
 
160
181
  if (server.command) console.log(chalk.bold('Command:'), server.command);
@@ -322,8 +343,17 @@ async function removeMcpServer(name) {
322
343
  /**
323
344
  * Enable MCP server for current project
324
345
  */
325
- async function enableMcpServer(name) {
346
+ async function enableMcpServer(name, options = {}) {
326
347
  try {
348
+ const { MCP_SCOPES, DEFAULT_MCP_SCOPE } = require('../config');
349
+ let scope = options.scope || DEFAULT_MCP_SCOPE;
350
+
351
+ // Validate scope
352
+ if (!Object.values(MCP_SCOPES).includes(scope)) {
353
+ console.log(chalk.red(`✗ Invalid scope '${scope}'. Valid scopes: local, project, user`));
354
+ return;
355
+ }
356
+
327
357
  if (!name) {
328
358
  const servers = config.getAllMcpServers();
329
359
  const enabled = config.getEnabledMcpServers();
@@ -341,12 +371,38 @@ async function enableMcpServer(name) {
341
371
  choices: available
342
372
  }]);
343
373
  name = serverName;
374
+
375
+ // Ask for scope if not provided
376
+ if (!options.scope) {
377
+ const { selectedScope } = await inquirer.prompt([{
378
+ type: 'list',
379
+ name: 'selectedScope',
380
+ message: 'Select scope (作用范围):',
381
+ choices: [
382
+ { name: 'local - Only current project (仅当前项目)', value: MCP_SCOPES.LOCAL },
383
+ { name: 'project - Share with project members via .mcp.json (通过 .mcp.json 与项目成员共享)', value: MCP_SCOPES.PROJECT },
384
+ { name: 'user - All projects for current user (当前用户所有项目)', value: MCP_SCOPES.USER }
385
+ ],
386
+ default: DEFAULT_MCP_SCOPE
387
+ }]);
388
+ scope = selectedScope;
389
+ }
344
390
  }
345
391
 
346
- if (config.enableProjectMcpServer(name)) {
392
+ if (config.enableProjectMcpServer(name, scope)) {
347
393
  config.syncMcpConfig();
348
- console.log(chalk.green(`✓ MCP server '${name}' activated for current project`));
394
+ console.log(chalk.green(`✓ MCP server '${name}' activated for current project with scope: ${scope}`));
349
395
  console.log(chalk.green('✓ Claude configuration updated'));
396
+
397
+ // Show scope-specific information
398
+ if (scope === MCP_SCOPES.LOCAL) {
399
+ console.log(chalk.cyan(' Scope: local - Only available in this project'));
400
+ } else if (scope === MCP_SCOPES.PROJECT) {
401
+ console.log(chalk.cyan(' Scope: project - Configuration stored in project, shared with team members'));
402
+ console.log(chalk.gray(' Note: Make sure to commit .ais-project-config to share with your team'));
403
+ } else if (scope === MCP_SCOPES.USER) {
404
+ console.log(chalk.cyan(' Scope: user - Available to all your projects'));
405
+ }
350
406
  } else {
351
407
  console.log(chalk.red(`✗ MCP server '${name}' not found`));
352
408
  }
@@ -408,7 +464,9 @@ async function showEnabledMcpServers() {
408
464
  enabled.forEach(name => {
409
465
  const server = servers[name];
410
466
  if (server) {
411
- console.log(` ${server.name.padEnd(12)} ${server.type.padEnd(7)} ${server.description || ''}`);
467
+ const serverName = server.name || name;
468
+ const type = server.type || 'unknown';
469
+ console.log(` ${String(serverName).padEnd(12)} ${String(type).padEnd(7)} ${server.description || ''}`);
412
470
  }
413
471
  });
414
472
 
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Global Configuration Manager
3
+ * Handles global configuration operations (accounts, MCP servers in home directory)
4
+ */
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const { CONFIG_FILES } = require('../constants');
9
+
10
+ class GlobalConfigManager {
11
+ constructor() {
12
+ this.globalConfigDir = path.join(os.homedir(), CONFIG_FILES.GLOBAL_DIR);
13
+ this.globalConfigFile = path.join(this.globalConfigDir, CONFIG_FILES.GLOBAL_CONFIG);
14
+ this.projectConfigFilename = '.ais-project-config';
15
+ this.ensureConfigExists();
16
+ }
17
+
18
+ /**
19
+ * Ensure configuration directories and files exist
20
+ */
21
+ ensureConfigExists() {
22
+ if (!fs.existsSync(this.globalConfigDir)) {
23
+ fs.mkdirSync(this.globalConfigDir, { recursive: true });
24
+ }
25
+
26
+ if (!fs.existsSync(this.globalConfigFile)) {
27
+ this.save({ accounts: {}, mcpServers: {}, nextAccountId: 1 });
28
+ }
29
+
30
+ this.migrateAccountIds();
31
+ }
32
+
33
+ /**
34
+ * Find project root by searching upwards for .ais-project-config file
35
+ */
36
+ findProjectRoot(startDir = process.cwd()) {
37
+ let currentDir = path.resolve(startDir);
38
+ const rootDir = path.parse(currentDir).root;
39
+
40
+ while (currentDir !== rootDir) {
41
+ const configPath = path.join(currentDir, this.projectConfigFilename);
42
+ if (fs.existsSync(configPath)) {
43
+ return currentDir;
44
+ }
45
+ currentDir = path.dirname(currentDir);
46
+ }
47
+
48
+ const configPath = path.join(rootDir, this.projectConfigFilename);
49
+ if (fs.existsSync(configPath)) {
50
+ return rootDir;
51
+ }
52
+
53
+ return null;
54
+ }
55
+
56
+ /**
57
+ * Read global configuration
58
+ */
59
+ read() {
60
+ try {
61
+ const data = fs.readFileSync(this.globalConfigFile, 'utf8');
62
+ const config = JSON.parse(data);
63
+ if (!config.nextAccountId) {
64
+ config.nextAccountId = 1;
65
+ }
66
+ return config;
67
+ } catch (error) {
68
+ return { accounts: {}, mcpServers: {}, nextAccountId: 1 };
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Save global configuration
74
+ */
75
+ save(config) {
76
+ fs.writeFileSync(this.globalConfigFile, JSON.stringify(config, null, 2), 'utf8');
77
+ }
78
+
79
+ /**
80
+ * Migrate existing accounts to have IDs
81
+ */
82
+ migrateAccountIds() {
83
+ const config = this.read();
84
+ let needsSave = false;
85
+
86
+ if (!config.nextAccountId) {
87
+ config.nextAccountId = 1;
88
+ needsSave = true;
89
+ }
90
+
91
+ Object.keys(config.accounts || {}).forEach(name => {
92
+ if (!config.accounts[name].id) {
93
+ config.accounts[name].id = config.nextAccountId;
94
+ config.nextAccountId++;
95
+ needsSave = true;
96
+ }
97
+ });
98
+
99
+ if (needsSave) {
100
+ this.save(config);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Get account by ID or name
106
+ */
107
+ getAccountByIdOrName(idOrName) {
108
+ const accounts = this.getAllAccounts();
109
+
110
+ const id = parseInt(idOrName, 10);
111
+ if (!isNaN(id)) {
112
+ for (const [name, account] of Object.entries(accounts)) {
113
+ if (account.id === id) {
114
+ return { name, ...account };
115
+ }
116
+ }
117
+ }
118
+
119
+ const account = accounts[idOrName];
120
+ if (account) {
121
+ return { name: idOrName, ...account };
122
+ }
123
+
124
+ return null;
125
+ }
126
+
127
+ /**
128
+ * Add or update an account
129
+ */
130
+ addAccount(name, accountData) {
131
+ const config = this.read();
132
+
133
+ const isNewAccount = !config.accounts[name];
134
+ const accountId = isNewAccount ? config.nextAccountId : config.accounts[name].id;
135
+
136
+ config.accounts[name] = {
137
+ ...accountData,
138
+ id: accountId,
139
+ createdAt: config.accounts[name]?.createdAt || new Date().toISOString(),
140
+ updatedAt: new Date().toISOString()
141
+ };
142
+
143
+ if (isNewAccount) {
144
+ config.nextAccountId++;
145
+ }
146
+
147
+ this.save(config);
148
+ return true;
149
+ }
150
+
151
+ /**
152
+ * Get all accounts
153
+ */
154
+ getAllAccounts() {
155
+ const config = this.read();
156
+ return config.accounts || {};
157
+ }
158
+
159
+ /**
160
+ * Get a specific account
161
+ */
162
+ getAccount(name) {
163
+ const accounts = this.getAllAccounts();
164
+ return accounts[name] || null;
165
+ }
166
+
167
+ /**
168
+ * Remove an account
169
+ */
170
+ removeAccount(name) {
171
+ const config = this.read();
172
+ if (config.accounts[name]) {
173
+ delete config.accounts[name];
174
+ this.save(config);
175
+ return true;
176
+ }
177
+ return false;
178
+ }
179
+
180
+ /**
181
+ * Check if an account exists
182
+ */
183
+ accountExists(name) {
184
+ const accounts = this.getAllAccounts();
185
+ return !!accounts[name];
186
+ }
187
+
188
+ // MCP Server methods
189
+
190
+ /**
191
+ * Add or update an MCP server
192
+ */
193
+ addMcpServer(name, serverData) {
194
+ const config = this.read();
195
+ if (!config.mcpServers) config.mcpServers = {};
196
+
197
+ const { DEFAULT_MCP_SCOPE } = require('../constants');
198
+ if (!serverData.scope) {
199
+ serverData.scope = DEFAULT_MCP_SCOPE;
200
+ }
201
+
202
+ config.mcpServers[name] = {
203
+ ...serverData,
204
+ createdAt: config.mcpServers[name]?.createdAt || new Date().toISOString(),
205
+ updatedAt: new Date().toISOString()
206
+ };
207
+ this.save(config);
208
+ return true;
209
+ }
210
+
211
+ /**
212
+ * Get all MCP servers
213
+ */
214
+ getAllMcpServers() {
215
+ const config = this.read();
216
+ return config.mcpServers || {};
217
+ }
218
+
219
+ /**
220
+ * Get a specific MCP server
221
+ */
222
+ getMcpServer(name) {
223
+ const servers = this.getAllMcpServers();
224
+ return servers[name] || null;
225
+ }
226
+
227
+ /**
228
+ * Update an MCP server
229
+ */
230
+ updateMcpServer(name, serverData) {
231
+ const config = this.read();
232
+ if (!config.mcpServers || !config.mcpServers[name]) return false;
233
+ config.mcpServers[name] = {
234
+ ...serverData,
235
+ createdAt: config.mcpServers[name].createdAt,
236
+ updatedAt: new Date().toISOString()
237
+ };
238
+ this.save(config);
239
+ return true;
240
+ }
241
+
242
+ /**
243
+ * Remove an MCP server
244
+ */
245
+ removeMcpServer(name) {
246
+ const config = this.read();
247
+ if (config.mcpServers && config.mcpServers[name]) {
248
+ delete config.mcpServers[name];
249
+ this.save(config);
250
+ return true;
251
+ }
252
+ return false;
253
+ }
254
+
255
+ /**
256
+ * Get configuration file paths
257
+ */
258
+ getConfigPaths() {
259
+ return {
260
+ global: this.globalConfigFile,
261
+ globalDir: this.globalConfigDir
262
+ };
263
+ }
264
+ }
265
+
266
+ module.exports = GlobalConfigManager;