aicodeswitch 1.2.0 → 1.2.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.
Files changed (2) hide show
  1. package/bin/update.js +125 -44
  2. package/package.json +3 -2
package/bin/update.js CHANGED
@@ -2,16 +2,17 @@ const path = require('path');
2
2
  const fs = require('fs');
3
3
  const os = require('os');
4
4
  const https = require('https');
5
+ const http = require('http');
5
6
  const { spawn } = require('child_process');
6
7
  const chalk = require('chalk');
7
8
  const ora = require('ora');
8
9
  const boxen = require('boxen');
10
+ const tar = require('tar');
9
11
 
10
12
  const AICOSWITCH_DIR = path.join(os.homedir(), '.aicodeswitch');
11
13
  const RELEASES_DIR = path.join(AICOSWITCH_DIR, 'releases');
12
14
  const CURRENT_FILE = path.join(AICOSWITCH_DIR, 'current');
13
15
  const PACKAGE_NAME = 'aicodeswitch';
14
- const NPM_REGISTRY = 'https://registry.npmjs.org';
15
16
 
16
17
  // 确保目录存在
17
18
  const ensureDir = (dirPath) => {
@@ -96,40 +97,67 @@ const getLatestVersion = () => {
96
97
  });
97
98
  };
98
99
 
99
- // 使用 npm 安装指定版本到指定目录
100
- const installPackage = (version, targetDir) => {
100
+ // 下载 tarball 文件
101
+ const downloadTarball = (url, destPath) => {
101
102
  return new Promise((resolve, reject) => {
102
- const npmProcess = spawn('npm', [
103
- 'install',
104
- `${PACKAGE_NAME}@${version}`,
105
- '--prefix',
106
- targetDir,
107
- '--no-save',
108
- '--no-package-lock',
109
- '--no-bin-links'
110
- ]);
111
-
112
- let stderr = '';
113
-
114
- npmProcess.stderr.on('data', (data) => {
115
- stderr += data.toString();
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
+ });
116
134
  });
117
135
 
118
- npmProcess.on('close', (code) => {
119
- if (code === 0) {
120
- // npm install 会把包安装到 targetDir/node_modules/ 目录下
121
- const packageDir = path.join(targetDir, 'node_modules', PACKAGE_NAME);
122
- if (fs.existsSync(packageDir)) {
123
- resolve(packageDir);
124
- } else {
125
- reject(new Error('Package installation directory not found'));
126
- }
127
- } else {
128
- reject(new Error(`npm install failed: ${stderr}`));
136
+ req.on('error', (err) => {
137
+ if (fs.existsSync(destPath)) {
138
+ fs.unlink(destPath, () => {});
129
139
  }
140
+ reject(err);
130
141
  });
131
142
 
132
- npmProcess.on('error', reject);
143
+ req.setTimeout(60000, () => {
144
+ req.destroy();
145
+ if (fs.existsSync(destPath)) {
146
+ fs.unlink(destPath, () => {});
147
+ }
148
+ reject(new Error('Download timeout'));
149
+ });
150
+
151
+ req.end();
152
+ });
153
+ };
154
+
155
+ // 解压 tarball 到指定目录
156
+ const extractTarball = (tarballPath, destDir) => {
157
+ return tar.x({
158
+ file: tarballPath,
159
+ cwd: destDir,
160
+ strip: 1, // 去掉 package 目录层级
133
161
  });
134
162
  };
135
163
 
@@ -159,6 +187,37 @@ const restart = () => {
159
187
  });
160
188
  };
161
189
 
190
+ // 清理旧版本的下载文件(保留最近 3 个版本)
191
+ const cleanupOldVersions = () => {
192
+ try {
193
+ if (!fs.existsSync(RELEASES_DIR)) {
194
+ return;
195
+ }
196
+
197
+ const versions = fs.readdirSync(RELEASES_DIR)
198
+ .filter(item => {
199
+ const itemPath = path.join(RELEASES_DIR, item);
200
+ return fs.statSync(itemPath).isDirectory();
201
+ })
202
+ .sort((a, b) => {
203
+ // 按版本号降序排序
204
+ return compareVersions(b, a);
205
+ });
206
+
207
+ // 保留最近 3 个版本,删除其他版本
208
+ if (versions.length > 3) {
209
+ const versionsToDelete = versions.slice(3);
210
+ versionsToDelete.forEach(version => {
211
+ const versionPath = path.join(RELEASES_DIR, version);
212
+ fs.rmSync(versionPath, { recursive: true, force: true });
213
+ });
214
+ }
215
+ } catch (err) {
216
+ // 清理失败不影响更新流程
217
+ console.error(chalk.yellow(`Warning: Failed to cleanup old versions: ${err.message}`));
218
+ }
219
+ };
220
+
162
221
  // 主更新逻辑
163
222
  const update = async () => {
164
223
  console.log('\n');
@@ -182,7 +241,6 @@ const update = async () => {
182
241
  if (comparison <= 0) {
183
242
  console.log(chalk.yellow(`\n✓ You are already on the latest version (${chalk.bold(currentVersion)})\n`));
184
243
  process.exit(0);
185
- return;
186
244
  }
187
245
 
188
246
  console.log(chalk.cyan(`\n📦 Update available: ${chalk.bold(currentVersion)} → ${chalk.bold(latestVersion)}\n`));
@@ -190,34 +248,58 @@ const update = async () => {
190
248
  // 确保目录存在
191
249
  ensureDir(RELEASES_DIR);
192
250
 
193
- // 安装新版本
194
- const installSpinner = ora({
195
- text: chalk.cyan('Downloading and installing from npm...'),
251
+ // 创建版本目录
252
+ const versionDir = path.join(RELEASES_DIR, latestVersion);
253
+ ensureDir(versionDir);
254
+
255
+ // 下载 tarball
256
+ const downloadSpinner = ora({
257
+ text: chalk.cyan('Downloading from npm...'),
196
258
  color: 'cyan'
197
259
  }).start();
198
260
 
199
- const versionDir = path.join(RELEASES_DIR, latestVersion);
200
- ensureDir(versionDir);
261
+ const tarballPath = path.join(versionDir, 'package.tgz');
201
262
 
202
263
  try {
203
- const packageDir = await installPackage(latestVersion, versionDir);
204
- installSpinner.succeed(chalk.green('Package installed'));
264
+ await downloadTarball(latestInfo.tarball, tarballPath);
265
+ downloadSpinner.succeed(chalk.green('Download completed'));
205
266
  } catch (err) {
206
- installSpinner.fail(chalk.red('Installation failed'));
267
+ downloadSpinner.fail(chalk.red('Download failed'));
207
268
  console.log(chalk.red(`Error: ${err.message}\n`));
208
269
  process.exit(1);
209
- return;
210
270
  }
211
271
 
212
- // 实际的包在 node_modules/aicodeswitch 目录下
213
- const actualPackageDir = path.join(versionDir, 'node_modules', PACKAGE_NAME);
214
- updateCurrentFile(actualPackageDir);
272
+ // 解压 tarball
273
+ const extractSpinner = ora({
274
+ text: chalk.cyan('Extracting package...'),
275
+ color: 'cyan'
276
+ }).start();
277
+
278
+ try {
279
+ await extractTarball(tarballPath, versionDir);
280
+ extractSpinner.succeed(chalk.green('Package extracted'));
281
+ } catch (err) {
282
+ extractSpinner.fail(chalk.red('Extraction failed'));
283
+ console.log(chalk.red(`Error: ${err.message}\n`));
284
+ process.exit(1);
285
+ } finally {
286
+ // 删除 tarball 文件
287
+ if (fs.existsSync(tarballPath)) {
288
+ fs.unlinkSync(tarballPath);
289
+ }
290
+ }
291
+
292
+ // 更新 current 文件
293
+ updateCurrentFile(versionDir);
294
+
295
+ // 清理旧版本
296
+ cleanupOldVersions();
215
297
 
216
298
  // 显示更新成功信息
217
299
  console.log(boxen(
218
300
  chalk.green.bold('✨ Update Successful!\n\n') +
219
301
  chalk.white('Version: ') + chalk.cyan.bold(latestVersion) + '\n' +
220
- chalk.white('Location: ') + chalk.gray(actualPackageDir) + '\n\n' +
302
+ chalk.white('Location: ') + chalk.gray(versionDir) + '\n\n' +
221
303
  chalk.gray('Restarting server with the new version...'),
222
304
  {
223
305
  padding: 1,
@@ -234,7 +316,6 @@ const update = async () => {
234
316
  console.log(chalk.yellow(`\n⚠️ Update completed, but restart failed: ${err.message}`));
235
317
  console.log(chalk.cyan('Please manually run: ') + chalk.yellow('aicos restart\n'));
236
318
  process.exit(1);
237
- return;
238
319
  }
239
320
 
240
321
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicodeswitch",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
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,7 +55,8 @@
55
55
  "express": "^4.18.2",
56
56
  "jsonwebtoken": "^9.0.3",
57
57
  "level": "^8.0.0",
58
- "ora": "^5.4.1"
58
+ "ora": "^5.4.1",
59
+ "tar": "^7.4.3"
59
60
  },
60
61
  "devDependencies": {
61
62
  "@types/better-sqlite3": "^7.6.9",