@qoder-ai/qodercli 0.0.11-preview → 0.0.14-preview

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.
package/README.md CHANGED
@@ -8,9 +8,9 @@ Qoder CLI is a powerful terminal-based AI assistant that understands your codeba
8
8
 
9
9
  Install Qoder CLI using your preferred method:
10
10
 
11
- ### NPM (Recommended)
11
+ ### Curl + Bash
12
12
  ```sh
13
- npm install -g @qoder-ai/qodercli
13
+ curl -fsSL https://qoder.com/install | bash
14
14
  ```
15
15
 
16
16
  ### Homebrew (macOS & Linux)
@@ -18,9 +18,9 @@ npm install -g @qoder-ai/qodercli
18
18
  brew install --cask QoderAI/qoder/qodercli
19
19
  ```
20
20
 
21
- ### Curl + Bash
21
+ ### NPM
22
22
  ```sh
23
- curl -fsSL https://qoder.com/install | bash
23
+ npm install -g @qoder-ai/qodercli
24
24
  ```
25
25
 
26
26
  ## Usage
@@ -31,7 +31,7 @@ curl -fsSL https://qoder.com/install | bash
31
31
 
32
32
  ## Reporting Bugs
33
33
 
34
- We welcome feedback. Use the `/bug` command to report issues directly within Qoder CLI.
34
+ We welcome feedback. Use the `/feedback` command to report issues directly within Qoder CLI.
35
35
 
36
36
  ## Data collection, usage, and retention
37
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qoder-ai/qodercli",
3
- "version": "0.0.11-preview",
3
+ "version": "0.0.14-preview",
4
4
  "description": "qodercli - npm installer",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -40,37 +40,31 @@
40
40
  ],
41
41
  "preferGlobal": true,
42
42
  "binaries": {
43
- "version": "0.0.11-preview",
43
+ "version": "0.0.14-preview",
44
44
  "files": [
45
45
  {
46
46
  "os": "linux",
47
47
  "arch": "amd64",
48
- "url": "https://download.qoder.com/qodercli/releases/0.0.11-preview/qodercli_0.0.11-preview_linux_amd64.tar.gz",
49
- "sha256": "1d6d3dd9484de5139a0a522117cc9bb33a8be9347c4450505160c816f51aba57"
48
+ "url": "https://download.qoder.com/qodercli/releases/0.0.14-preview/qodercli_0.0.14-preview_linux_amd64.tar.gz",
49
+ "sha256": "44313ff226d7463b73e44193672a91f5fe74040da07101af292ffbe5709bac3d"
50
50
  },
51
51
  {
52
52
  "os": "linux",
53
53
  "arch": "arm64",
54
- "url": "https://download.qoder.com/qodercli/releases/0.0.11-preview/qodercli_0.0.11-preview_linux_arm64.tar.gz",
55
- "sha256": "114922bcf2a130e13105577ddf95b3ac795ea373a8b8fa877379326e432b89a6"
54
+ "url": "https://download.qoder.com/qodercli/releases/0.0.14-preview/qodercli_0.0.14-preview_linux_arm64.tar.gz",
55
+ "sha256": "4643d6559417f85a9f5fa1317e58bd0453b90c06c7550902ea21eb5ea0e73447"
56
56
  },
57
57
  {
58
58
  "os": "darwin",
59
59
  "arch": "amd64",
60
- "url": "https://download.qoder.com/qodercli/releases/0.0.11-preview/qodercli_0.0.11-preview_darwin_amd64.zip",
61
- "sha256": "7497f7b57eadf598106eb519cb305e73260030345ceeab3ce98bc991c0b6c600"
60
+ "url": "https://download.qoder.com/qodercli/releases/0.0.14-preview/qodercli_0.0.14-preview_darwin_amd64.zip",
61
+ "sha256": "818775f24694ee267b9bb863c10753eefedcccd1679bd696412575b675f0c7ec"
62
62
  },
63
63
  {
64
64
  "os": "darwin",
65
65
  "arch": "arm64",
66
- "url": "https://download.qoder.com/qodercli/releases/0.0.11-preview/qodercli_0.0.11-preview_darwin_arm64.zip",
67
- "sha256": "296b4e0bd93e31a682dd865bb9bc707ef59176aa81fb7ba8bfeac15da7d6ecf9"
68
- },
69
- {
70
- "os": "windows",
71
- "arch": "amd64",
72
- "url": "https://download.qoder.com/qodercli/releases/0.0.11-preview/qodercli_0.0.11-preview_windows_amd64.zip",
73
- "sha256": "b96d5bb5d45e4dd23590d79ba05c258684ccd3dcb0aa3a4f1dbfb12287b8ce60"
66
+ "url": "https://download.qoder.com/qodercli/releases/0.0.14-preview/qodercli_0.0.14-preview_darwin_arm64.zip",
67
+ "sha256": "e2af1b4f3ab0532513aff38591cb6a9dc1b34b5a6dd520257e88dcb2550b57a4"
74
68
  }
75
69
  ]
76
70
  }
@@ -72,14 +72,18 @@ class QoderInstaller {
72
72
  async downloadBinary(url, expectedSha256) {
73
73
  console.log(`Downloading binary: ${url}`);
74
74
 
75
- // Ensure directory exists
75
+ // Create temporary directory for download operations
76
+ const os = require('os');
77
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'qodercli-install-'));
78
+
79
+ // Ensure target directory exists
76
80
  if (!fs.existsSync(BIN_DIR)) {
77
81
  fs.mkdirSync(BIN_DIR, { recursive: true });
78
82
  }
79
83
 
80
- // Download file
84
+ // Download file to temporary directory
81
85
  const filename = path.basename(url);
82
- const archivePath = path.join(BIN_DIR, filename);
86
+ const archivePath = path.join(tempDir, filename);
83
87
 
84
88
  try {
85
89
  await this.downloadFile(url, archivePath);
@@ -91,12 +95,19 @@ class QoderInstaller {
91
95
  throw new Error(`Checksum mismatch. Expected: ${expectedSha256}, Got: ${actualSha256}`);
92
96
  }
93
97
 
94
- // Extract file
98
+ // Extract file to temporary directory first
95
99
  console.log('Extracting binary...');
96
- await this.extractArchive(archivePath, filename);
100
+ const extractDir = path.join(tempDir, 'extract');
101
+ fs.mkdirSync(extractDir, { recursive: true });
102
+ await this.extractArchive(archivePath, filename, extractDir);
103
+
104
+ // Move extracted binary to final destination
105
+ const extractedBinary = this.findExtractedBinary(extractDir);
106
+ if (extractedBinary.length === 0) {
107
+ throw new Error(`Binary file not found after extraction in ${extractDir}`);
108
+ }
97
109
 
98
- // Remove archive
99
- fs.unlinkSync(archivePath);
110
+ fs.renameSync(extractedBinary[0], this.binPath);
100
111
 
101
112
  // Set executable permission
102
113
  if (process.platform !== 'win32') {
@@ -111,41 +122,33 @@ class QoderInstaller {
111
122
  this.verifyInstallation();
112
123
 
113
124
  } catch (error) {
114
- // Cleanup failed download
115
- if (fs.existsSync(archivePath)) {
116
- fs.unlinkSync(archivePath);
117
- }
118
125
  throw error;
126
+ } finally {
127
+ // Always cleanup temporary directory
128
+ try {
129
+ fs.rmSync(tempDir, { recursive: true, force: true });
130
+ } catch (cleanupError) {
131
+ console.warn('Warning: Failed to cleanup temporary directory:', cleanupError.message);
132
+ }
119
133
  }
120
134
  }
121
135
 
122
- async extractArchive(archivePath, filename) {
136
+ async extractArchive(archivePath, filename, extractDir) {
123
137
  if (filename.endsWith('.zip')) {
124
138
  // Extract ZIP file
125
139
  if (process.platform === 'win32') {
126
140
  // Windows: Use PowerShell
127
141
  try {
128
- execSync(`powershell -command "Expand-Archive -Path '${archivePath}' -DestinationPath '${BIN_DIR}' -Force"`, {
142
+ execSync(`powershell -command "Expand-Archive -Path '${archivePath}' -DestinationPath '${extractDir}' -Force"`, {
129
143
  stdio: 'pipe'
130
144
  });
131
-
132
- // Additional verification - check if binary file exists after extraction
133
- if (!fs.existsSync(this.binPath)) {
134
- // Maybe the binary is in a subdirectory, let's check
135
- const extractedFiles = this.findExtractedBinary(BIN_DIR);
136
- if (extractedFiles.length > 0) {
137
- fs.renameSync(extractedFiles[0], this.binPath);
138
- } else {
139
- throw new Error(`Binary file not found after extraction. Expected at: ${this.binPath}`);
140
- }
141
- }
142
145
  } catch (error) {
143
146
  throw new Error(`ZIP extraction failed: ${error.message}. Please ensure PowerShell is available.`);
144
147
  }
145
148
  } else {
146
149
  // Unix: Use unzip command
147
150
  try {
148
- execSync(`unzip -o "${archivePath}" -d "${BIN_DIR}"`, {
151
+ execSync(`unzip -o "${archivePath}" -d "${extractDir}"`, {
149
152
  stdio: 'pipe'
150
153
  });
151
154
  } catch (error) {
@@ -155,7 +158,7 @@ class QoderInstaller {
155
158
  } else {
156
159
  // Extract tar.gz file
157
160
  try {
158
- execSync(`tar -xzf "${archivePath}" -C "${BIN_DIR}"`, {
161
+ execSync(`tar -xzf "${archivePath}" -C "${extractDir}"`, {
159
162
  stdio: 'pipe'
160
163
  });
161
164
  } catch (error) {
@@ -217,19 +220,37 @@ class QoderInstaller {
217
220
  return new Promise((resolve, reject) => {
218
221
  const file = fs.createWriteStream(filePath);
219
222
  const client = url.startsWith('https:') ? https : http;
223
+ let cleanupDone = false;
224
+
225
+ const cleanup = () => {
226
+ if (cleanupDone) return;
227
+ cleanupDone = true;
228
+
229
+ try {
230
+ file.close();
231
+ } catch (e) {
232
+ // Ignore errors during cleanup
233
+ }
234
+
235
+ try {
236
+ if (fs.existsSync(filePath)) {
237
+ fs.unlinkSync(filePath);
238
+ }
239
+ } catch (e) {
240
+ // Ignore errors during cleanup
241
+ }
242
+ };
220
243
 
221
244
  const request = client.get(url, (response) => {
222
245
  if (response.statusCode === 302 || response.statusCode === 301) {
223
246
  // Handle redirect
224
- file.close();
225
- fs.unlinkSync(filePath);
247
+ cleanup();
226
248
  return this.downloadFile(response.headers.location, filePath, timeout)
227
249
  .then(resolve).catch(reject);
228
250
  }
229
251
 
230
252
  if (response.statusCode !== 200) {
231
- file.close();
232
- if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
253
+ cleanup();
233
254
  reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
234
255
  return;
235
256
  }
@@ -237,26 +258,53 @@ class QoderInstaller {
237
258
  response.pipe(file);
238
259
 
239
260
  file.on('finish', () => {
240
- file.close();
241
- resolve();
261
+ if (!cleanupDone) {
262
+ file.close();
263
+ resolve();
264
+ }
242
265
  });
243
266
 
244
267
  file.on('error', (error) => {
245
- if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
268
+ cleanup();
246
269
  reject(error);
247
270
  });
248
271
  }).on('error', (error) => {
249
- if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
272
+ cleanup();
250
273
  reject(error);
251
274
  });
252
275
 
253
276
  // Set timeout
254
277
  request.setTimeout(timeout, () => {
255
278
  request.destroy();
256
- file.close();
257
- if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
279
+ cleanup();
258
280
  reject(new Error(`Download timeout (${timeout}ms): ${url}`));
259
281
  });
282
+
283
+ // Handle process interruption signals
284
+ const handleSignal = () => {
285
+ request.destroy();
286
+ cleanup();
287
+ reject(new Error('Download interrupted by signal'));
288
+ };
289
+
290
+ process.once('SIGINT', handleSignal);
291
+ process.once('SIGTERM', handleSignal);
292
+
293
+ // Clean up signal handlers when promise resolves/rejects
294
+ const originalResolve = resolve;
295
+ const originalReject = reject;
296
+
297
+ resolve = (...args) => {
298
+ process.removeListener('SIGINT', handleSignal);
299
+ process.removeListener('SIGTERM', handleSignal);
300
+ originalResolve(...args);
301
+ };
302
+
303
+ reject = (...args) => {
304
+ process.removeListener('SIGINT', handleSignal);
305
+ process.removeListener('SIGTERM', handleSignal);
306
+ originalReject(...args);
307
+ };
260
308
  });
261
309
  }
262
310