aicodeswitch 1.2.5 → 1.2.6

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 (2) hide show
  1. package/bin/update.js +215 -269
  2. package/package.json +2 -3
package/bin/update.js CHANGED
@@ -1,73 +1,35 @@
1
+ const { spawn } = require('child_process');
2
+ const https = require('https');
1
3
  const path = require('path');
2
4
  const fs = require('fs');
3
5
  const os = require('os');
4
- const https = require('https');
5
- const http = require('http');
6
- const { spawn } = require('child_process');
7
6
  const chalk = require('chalk');
8
- const ora = require('ora');
9
7
  const boxen = require('boxen');
10
- const tar = require('tar');
8
+ const ora = require('ora');
11
9
 
12
- const AICOSWITCH_DIR = path.join(os.homedir(), '.aicodeswitch');
13
- const RELEASES_DIR = path.join(AICOSWITCH_DIR, 'releases');
14
- const CURRENT_FILE = path.join(AICOSWITCH_DIR, 'current');
15
10
  const PACKAGE_NAME = 'aicodeswitch';
11
+ const NPM_REGISTRY = 'registry.npmjs.org';
16
12
 
17
- // 确保目录存在
18
- const ensureDir = (dirPath) => {
19
- if (!fs.existsSync(dirPath)) {
20
- fs.mkdirSync(dirPath, { recursive: true });
21
- }
22
- };
23
-
24
- // 获取当前使用的版本(从 current 文件或本地 package.json)
13
+ // 获取当前版本
25
14
  const getCurrentVersion = () => {
26
- // 先检查是否有 current 文件(更新的版本)
27
- if (fs.existsSync(CURRENT_FILE)) {
28
- try {
29
- const currentPath = fs.readFileSync(CURRENT_FILE, 'utf-8').trim();
30
- const currentPackageJson = path.join(currentPath, 'package.json');
31
- if (fs.existsSync(currentPackageJson)) {
32
- const pkg = JSON.parse(fs.readFileSync(currentPackageJson, 'utf-8'));
33
- return pkg.version;
34
- }
35
- } catch (err) {
36
- // 读取失败,fallback 到本地版本
37
- }
38
- }
39
-
40
- // 使用本地 package.json
15
+ const packageJsonPath = path.join(__dirname, '..', 'package.json');
41
16
  try {
42
- const packageJson = path.join(__dirname, '..', 'package.json');
43
- const pkg = JSON.parse(fs.readFileSync(packageJson, 'utf-8'));
44
- return pkg.version;
17
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
18
+ return packageJson.version;
45
19
  } catch (err) {
46
- return '0.0.0';
20
+ return null;
47
21
  }
48
22
  };
49
23
 
50
- // 比较版本号
51
- const compareVersions = (v1, v2) => {
52
- const parts1 = v1.split('.').map(Number);
53
- const parts2 = v2.split('.').map(Number);
54
-
55
- for (let i = 0; i < 3; i++) {
56
- if (parts1[i] > parts2[i]) return 1;
57
- if (parts1[i] < parts2[i]) return -1;
58
- }
59
- return 0;
60
- };
61
-
62
- // 从 npm registry 获取最新版本
24
+ // 从 npm 获取最新版本
63
25
  const getLatestVersion = () => {
64
26
  return new Promise((resolve, reject) => {
65
27
  const options = {
66
- hostname: 'registry.npmjs.org',
28
+ hostname: NPM_REGISTRY,
67
29
  path: `/${PACKAGE_NAME}`,
68
30
  method: 'GET',
69
31
  headers: {
70
- 'User-Agent': 'aicodeswitch'
32
+ 'User-Agent': 'aicodeswitch-update'
71
33
  }
72
34
  };
73
35
 
@@ -81,164 +43,126 @@ const getLatestVersion = () => {
81
43
  res.on('end', () => {
82
44
  try {
83
45
  const packageInfo = JSON.parse(data);
84
- const latestVersion = packageInfo['dist-tags'].latest;
85
- resolve({
86
- version: latestVersion,
87
- tarball: packageInfo.versions[latestVersion].dist.tarball
88
- });
46
+ resolve(packageInfo['dist-tags'].latest);
89
47
  } catch (err) {
90
- reject(new Error('Failed to parse package info from npm'));
48
+ reject(new Error('Failed to parse npm response'));
91
49
  }
92
50
  });
93
51
  });
94
52
 
95
- req.on('error', reject);
96
- req.end();
97
- });
98
- };
99
-
100
- // 下载 tarball 文件
101
- const downloadTarball = (url, destPath) => {
102
- return new Promise((resolve, reject) => {
103
- const urlObj = new URL(url);
104
- const protocol = urlObj.protocol === 'http:' ? http : https;
105
-
106
- const requestOptions = {
107
- hostname: urlObj.hostname,
108
- port: urlObj.port,
109
- path: urlObj.pathname + urlObj.search,
110
- method: 'GET',
111
- headers: {
112
- 'User-Agent': 'aicodeswitch'
113
- }
114
- };
115
-
116
- const req = protocol.request(requestOptions, (res) => {
117
- if (res.statusCode !== 200) {
118
- reject(new Error(`Failed to download: HTTP ${res.statusCode}`));
119
- return;
120
- }
121
-
122
- const fileStream = fs.createWriteStream(destPath);
123
- res.pipe(fileStream);
124
-
125
- fileStream.on('finish', () => {
126
- fileStream.close();
127
- resolve(destPath);
128
- });
129
-
130
- fileStream.on('error', (err) => {
131
- fs.unlink(destPath, () => {});
132
- reject(err);
133
- });
134
- });
135
-
136
53
  req.on('error', (err) => {
137
- if (fs.existsSync(destPath)) {
138
- fs.unlink(destPath, () => {});
139
- }
140
54
  reject(err);
141
55
  });
142
56
 
143
- req.setTimeout(60000, () => {
57
+ req.setTimeout(10000, () => {
144
58
  req.destroy();
145
- if (fs.existsSync(destPath)) {
146
- fs.unlink(destPath, () => {});
147
- }
148
- reject(new Error('Download timeout'));
59
+ reject(new Error('Request timeout'));
149
60
  });
150
61
 
151
62
  req.end();
152
63
  });
153
64
  };
154
65
 
155
- // 解压 tarball 到指定目录
156
- const extractTarball = (tarballPath, destDir) => {
157
- return tar.x({
158
- file: tarballPath,
159
- cwd: destDir,
160
- strip: 1, // 去掉 package 目录层级
161
- });
162
- };
163
-
164
- // 安装 npm 依赖
165
- const installDependencies = (dir) => {
66
+ // 执行命令
67
+ const execCommand = (command, args, options = {}) => {
166
68
  return new Promise((resolve, reject) => {
167
- console.log(chalk.cyan('Installing dependencies...'));
168
-
169
- const installProcess = spawn('npm', ['install', '--production'], {
170
- cwd: dir,
171
- stdio: 'inherit'
69
+ const proc = spawn(command, args, {
70
+ stdio: options.silent ? 'pipe' : 'inherit',
71
+ ...options
172
72
  });
173
73
 
174
- installProcess.on('close', (code) => {
74
+ let output = '';
75
+ let errorOutput = '';
76
+
77
+ if (options.silent) {
78
+ proc.stdout.on('data', (data) => {
79
+ output += data.toString();
80
+ });
81
+ proc.stderr.on('data', (data) => {
82
+ errorOutput += data.toString();
83
+ });
84
+ }
85
+
86
+ proc.on('close', (code) => {
175
87
  if (code === 0) {
176
- console.log(chalk.green('Dependencies installed successfully'));
177
- resolve();
88
+ resolve({ output, errorOutput });
178
89
  } else {
179
- reject(new Error(`npm install failed with exit code ${code}`));
90
+ reject({ code, output, errorOutput });
180
91
  }
181
92
  });
182
93
 
183
- installProcess.on('error', reject);
94
+ proc.on('error', (err) => {
95
+ reject(err);
96
+ });
184
97
  });
185
98
  };
186
99
 
187
- // 更新 current 文件
188
- const updateCurrentFile = (versionPath) => {
189
- fs.writeFileSync(CURRENT_FILE, versionPath);
190
- };
100
+ // 检查是否需要 sudo 权限
101
+ const needsSudo = () => {
102
+ const npmPrefix = process.env.npm_config_prefix || '/usr/local';
103
+ const globalInstallPath = path.join(npmPrefix, 'lib', 'node_modules');
104
+ const aicosPath = path.join(globalInstallPath, PACKAGE_NAME);
191
105
 
192
- // 执行 restart
193
- const restart = () => {
194
- return new Promise((resolve, reject) => {
195
- const restartScript = path.join(__dirname, 'restart.js');
106
+ // 如果全局安装路径存在且不可写,可能需要 sudo
107
+ if (fs.existsSync(globalInstallPath)) {
108
+ try {
109
+ fs.accessSync(globalInstallPath, fs.constants.W_OK);
110
+ return false;
111
+ } catch (err) {
112
+ return true;
113
+ }
114
+ }
196
115
 
197
- const restartProcess = spawn('node', [restartScript], {
198
- stdio: 'inherit'
199
- });
116
+ // 检查当前 aicos 安装位置
117
+ const currentLink = path.join(__dirname, 'cli.js');
118
+ const realPath = fs.realpathSync(currentLink);
200
119
 
201
- restartProcess.on('close', (code) => {
202
- if (code === 0) {
203
- resolve();
204
- } else {
205
- reject(new Error(`Restart failed with exit code ${code}`));
206
- }
207
- });
120
+ // 如果在全局目录下,需要检查权限
121
+ if (realPath.includes('/usr/local/') || realPath.includes('/usr/lib/')) {
122
+ try {
123
+ fs.accessSync(path.dirname(realPath), fs.constants.W_OK);
124
+ return false;
125
+ } catch (err) {
126
+ return true;
127
+ }
128
+ }
208
129
 
209
- restartProcess.on('error', reject);
210
- });
130
+ return false;
211
131
  };
212
132
 
213
- // 清理旧版本的下载文件(保留最近 3 个版本)
214
- const cleanupOldVersions = () => {
133
+ // 停止服务器
134
+ const stopServer = async () => {
135
+ const stopPath = path.join(__dirname, 'stop.js');
215
136
  try {
216
- if (!fs.existsSync(RELEASES_DIR)) {
217
- return;
218
- }
219
-
220
- const versions = fs.readdirSync(RELEASES_DIR)
221
- .filter(item => {
222
- const itemPath = path.join(RELEASES_DIR, item);
223
- return fs.statSync(itemPath).isDirectory();
224
- })
225
- .sort((a, b) => {
226
- // 按版本号降序排序
227
- return compareVersions(b, a);
228
- });
137
+ await execCommand('node', [stopPath], { silent: true });
138
+ return true;
139
+ } catch (err) {
140
+ // 停止失败可能是因为服务未运行,这不是致命错误
141
+ return false;
142
+ }
143
+ };
229
144
 
230
- // 保留最近 3 个版本,删除其他版本
231
- if (versions.length > 3) {
232
- const versionsToDelete = versions.slice(3);
233
- versionsToDelete.forEach(version => {
234
- const versionPath = path.join(RELEASES_DIR, version);
235
- fs.rmSync(versionPath, { recursive: true, force: true });
236
- });
237
- }
145
+ // 启动服务器
146
+ const startServer = async () => {
147
+ const startPath = path.join(__dirname, 'start.js');
148
+ try {
149
+ await execCommand('node', [startPath], { silent: true });
150
+ return true;
238
151
  } catch (err) {
239
- // 清理失败不影响更新流程
240
- console.error(chalk.yellow(`Warning: Failed to cleanup old versions: ${err.message}`));
152
+ return false;
153
+ }
154
+ };
155
+
156
+ // 比较版本号
157
+ const compareVersions = (v1, v2) => {
158
+ const parts1 = v1.split('.').map(Number);
159
+ const parts2 = v2.split('.').map(Number);
160
+
161
+ for (let i = 0; i < 3; i++) {
162
+ if (parts1[i] > parts2[i]) return 1;
163
+ if (parts1[i] < parts2[i]) return -1;
241
164
  }
165
+ return 0;
242
166
  };
243
167
 
244
168
  // 主更新逻辑
@@ -246,125 +170,147 @@ const update = async () => {
246
170
  console.log('\n');
247
171
 
248
172
  const currentVersion = getCurrentVersion();
249
- const spinner = ora({
173
+ if (!currentVersion) {
174
+ console.log(boxen(
175
+ chalk.red.bold('✗ Failed to read current version\n\n') +
176
+ chalk.white('Please reinstall the package.'),
177
+ {
178
+ padding: 1,
179
+ margin: 1,
180
+ borderStyle: 'round',
181
+ borderColor: 'red'
182
+ }
183
+ ));
184
+ console.log('');
185
+ process.exit(1);
186
+ }
187
+
188
+ // 显示当前版本
189
+ console.log(chalk.cyan('📦 Current Version: ') + chalk.white.bold(currentVersion));
190
+ console.log('');
191
+
192
+ // 检查最新版本
193
+ const checkSpinner = ora({
250
194
  text: chalk.cyan('Checking for updates...'),
251
- color: 'cyan'
195
+ color: 'cyan',
196
+ hideCursor: false
252
197
  }).start();
253
198
 
199
+ let latestVersion;
254
200
  try {
255
- // 获取最新版本信息
256
- const latestInfo = await getLatestVersion();
257
- const latestVersion = latestInfo.version;
258
-
259
- spinner.succeed(chalk.green(`Latest version: ${chalk.bold(latestVersion)}`));
260
-
261
- // 检查是否需要更新
262
- const comparison = compareVersions(latestVersion, currentVersion);
263
-
264
- if (comparison <= 0) {
265
- console.log(chalk.yellow(`\n✓ You are already on the latest version (${chalk.bold(currentVersion)})\n`));
266
- process.exit(0);
267
- }
268
-
269
- console.log(chalk.cyan(`\n📦 Update available: ${chalk.bold(currentVersion)} → ${chalk.bold(latestVersion)}\n`));
270
-
271
- // 确保目录存在
272
- ensureDir(RELEASES_DIR);
273
-
274
- // 创建版本目录
275
- const versionDir = path.join(RELEASES_DIR, latestVersion);
276
- ensureDir(versionDir);
277
-
278
- // 下载 tarball
279
- const downloadSpinner = ora({
280
- text: chalk.cyan('Downloading from npm...'),
281
- color: 'cyan'
282
- }).start();
283
-
284
- const tarballPath = path.join(versionDir, 'package.tgz');
285
-
286
- try {
287
- await downloadTarball(latestInfo.tarball, tarballPath);
288
- downloadSpinner.succeed(chalk.green('Download completed'));
289
- } catch (err) {
290
- downloadSpinner.fail(chalk.red('Download failed'));
291
- console.log(chalk.red(`Error: ${err.message}\n`));
292
- process.exit(1);
293
- }
294
-
295
- // 解压 tarball
296
- const extractSpinner = ora({
297
- text: chalk.cyan('Extracting package...'),
298
- color: 'cyan'
299
- }).start();
300
-
301
- try {
302
- await extractTarball(tarballPath, versionDir);
303
- extractSpinner.succeed(chalk.green('Package extracted'));
304
- } catch (err) {
305
- extractSpinner.fail(chalk.red('Extraction failed'));
306
- console.log(chalk.red(`Error: ${err.message}\n`));
307
- process.exit(1);
308
- } finally {
309
- // 删除 tarball 文件
310
- if (fs.existsSync(tarballPath)) {
311
- fs.unlinkSync(tarballPath);
312
- }
313
- }
314
-
315
- // 安装依赖
316
- const installSpinner = ora({
317
- text: chalk.cyan('Installing dependencies...'),
318
- color: 'cyan'
319
- }).start();
320
-
321
- try {
322
- await installDependencies(versionDir);
323
- installSpinner.succeed(chalk.green('Dependencies installed'));
324
- } catch (err) {
325
- installSpinner.fail(chalk.red('Dependencies installation failed'));
326
- console.log(chalk.red(`Error: ${err.message}\n`));
327
- process.exit(1);
328
- }
201
+ latestVersion = await getLatestVersion();
202
+ checkSpinner.succeed(chalk.green('Checked for updates'));
203
+ } catch (err) {
204
+ checkSpinner.fail(chalk.red('Failed to check for updates'));
205
+ console.log(chalk.yellow(`\nError: ${err.message}\n`));
206
+ console.log(chalk.white('You can manually update by running:\n'));
207
+ console.log(chalk.cyan(' npm update -g aicodeswitch\n'));
208
+ process.exit(1);
209
+ }
329
210
 
330
- // 更新 current 文件
331
- updateCurrentFile(versionDir);
211
+ console.log(chalk.cyan('📦 Latest Version: ') + chalk.white.bold(latestVersion));
212
+ console.log('');
332
213
 
333
- // 清理旧版本
334
- cleanupOldVersions();
214
+ // 比较版本
215
+ const versionCompare = compareVersions(latestVersion, currentVersion);
335
216
 
336
- // 显示更新成功信息
217
+ if (versionCompare <= 0) {
337
218
  console.log(boxen(
338
- chalk.green.bold(' Update Successful!\n\n') +
339
- chalk.white('Version: ') + chalk.cyan.bold(latestVersion) + '\n' +
340
- chalk.white('Location: ') + chalk.gray(versionDir) + '\n\n' +
341
- chalk.gray('Restarting server with the new version...'),
219
+ chalk.green.bold(' You are already using the latest version!\n\n') +
220
+ chalk.white(`Current version: ${chalk.cyan.bold(currentVersion)}\n`) +
221
+ chalk.white(`Latest version: ${chalk.cyan.bold(latestVersion)}`),
342
222
  {
343
223
  padding: 1,
344
224
  margin: 1,
345
- borderStyle: 'double',
225
+ borderStyle: 'round',
346
226
  borderColor: 'green'
347
227
  }
348
228
  ));
229
+ console.log('');
230
+ process.exit(0);
231
+ }
349
232
 
350
- // 重启服务器
351
- try {
352
- await restart();
353
- } catch (err) {
354
- console.log(chalk.yellow(`\n⚠️ Update completed, but restart failed: ${err.message}`));
355
- console.log(chalk.cyan('Please manually run: ') + chalk.yellow('aicos restart\n'));
356
- process.exit(1);
233
+ // 有新版本可用
234
+ console.log(boxen(
235
+ chalk.yellow.bold('⬆️ New version available!\n\n') +
236
+ chalk.white('Current: ') + chalk.gray(currentVersion) + '\n' +
237
+ chalk.white('Latest: ') + chalk.green.bold(latestVersion) + '\n\n' +
238
+ chalk.gray('Preparing to update...'),
239
+ {
240
+ padding: 1,
241
+ margin: 1,
242
+ borderStyle: 'round',
243
+ borderColor: 'yellow'
357
244
  }
245
+ ));
246
+ console.log('');
247
+
248
+ // 检查是否需要 sudo
249
+ const needSudo = needsSudo();
250
+ if (needSudo) {
251
+ console.log(chalk.yellow.bold('⚠️ Note: '));
252
+ console.log(chalk.white('This operation may require ') + chalk.yellow.bold('sudo') + chalk.white(' privileges.'));
253
+ console.log(chalk.gray('If prompted, please enter your password.\n'));
254
+ }
358
255
 
359
- process.exit(0);
256
+ // 停止服务器
257
+ const stopSpinner = ora({
258
+ text: chalk.cyan('Stopping server...'),
259
+ color: 'cyan'
260
+ }).start();
261
+
262
+ await stopServer();
263
+ stopSpinner.succeed(chalk.green('Server stopped'));
360
264
 
265
+ // 执行更新
266
+ const updateSpinner = ora({
267
+ text: chalk.cyan('Updating to latest version...'),
268
+ color: 'cyan'
269
+ }).start();
270
+
271
+ try {
272
+ const npmArgs = ['install', '-g', `${PACKAGE_NAME}@latest`];
273
+ if (needSudo) {
274
+ npmArgs.unshift('sudo');
275
+ }
276
+
277
+ await execCommand(needSudo ? 'sudo' : 'npm', npmArgs);
278
+ updateSpinner.succeed(chalk.green('Update successful!'));
361
279
  } catch (err) {
362
- spinner.fail(chalk.red('Update check failed'));
363
- console.log(chalk.red(`Error: ${err.message}\n`));
364
- console.log(chalk.gray('You can check for updates manually at:\n'));
365
- console.log(chalk.cyan(' https://www.npmjs.com/package/aicodeswitch\n'));
280
+ updateSpinner.fail(chalk.red('Update failed'));
281
+ console.log(chalk.yellow(`\nUpdate failed with error code ${err.code || 'unknown'}\n`));
282
+ console.log(chalk.white('You can try manually updating:\n'));
283
+ console.log(chalk.cyan(` ${needSudo ? 'sudo ' : ''}npm install -g ${PACKAGE_NAME}@latest\n`));
284
+
285
+ // 尝试重新启动服务器
286
+ console.log(chalk.yellow('Attempting to restart server...\n'));
287
+ await startServer();
366
288
  process.exit(1);
367
289
  }
290
+
291
+ console.log('');
292
+ console.log(boxen(
293
+ chalk.green.bold('✓ Successfully updated!\n\n') +
294
+ chalk.white('Previous version: ') + chalk.gray(currentVersion) + '\n' +
295
+ chalk.white('New version: ') + chalk.green.bold(latestVersion) + '\n\n' +
296
+ chalk.gray('Starting server...'),
297
+ {
298
+ padding: 1,
299
+ margin: 1,
300
+ borderStyle: 'double',
301
+ borderColor: 'green'
302
+ }
303
+ ));
304
+ console.log('');
305
+
306
+ // 启动服务器
307
+ await startServer();
308
+
309
+ console.log('');
310
+ console.log(chalk.cyan('💡 Tips:\n'));
311
+ console.log(chalk.white(' • Check version: ') + chalk.cyan('aicos version'));
312
+ console.log(chalk.white(' • View logs: ') + chalk.gray('tail -f ~/.aicodeswitch/server.log'));
313
+ console.log('\n');
368
314
  };
369
315
 
370
316
  module.exports = update();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicodeswitch",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "description": "A tool to help you manage AI programming tools to access large language models locally. It allows your Claude Code, Codex and other tools to no longer be limited to official models.",
5
5
  "author": "tangshuang",
6
6
  "license": "MIT",
@@ -55,8 +55,7 @@
55
55
  "express": "^4.18.2",
56
56
  "jsonwebtoken": "^9.0.3",
57
57
  "level": "^8.0.0",
58
- "ora": "^5.4.1",
59
- "tar": "^7.4.3"
58
+ "ora": "^5.4.1"
60
59
  },
61
60
  "devDependencies": {
62
61
  "@types/better-sqlite3": "^7.6.9",