@qoder-ai/qodercli 0.1.9 → 0.1.11

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/package.json +12 -12
  2. package/scripts/install.js +218 -47
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qoder-ai/qodercli",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "qodercli - npm installer",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -44,37 +44,37 @@
44
44
  ],
45
45
  "preferGlobal": true,
46
46
  "binaries": {
47
- "version": "0.1.9",
47
+ "version": "0.1.11",
48
48
  "files": [
49
49
  {
50
50
  "os": "linux",
51
51
  "arch": "amd64",
52
- "url": "https://download.qoder.com/qodercli/releases/0.1.9/qodercli_0.1.9_linux_amd64.tar.gz",
53
- "sha256": "31e016555884d0af6605aae401e0cf348be0afba5c81b6d862ca8442bf283a74"
52
+ "url": "https://download.qoder.com/qodercli/releases/0.1.11/qodercli_0.1.11_linux_amd64.tar.gz",
53
+ "sha256": "6f0268c127497b6fd0328d1e8c9cfc230e6e80c5ee168a336ee8af01fc658085"
54
54
  },
55
55
  {
56
56
  "os": "linux",
57
57
  "arch": "arm64",
58
- "url": "https://download.qoder.com/qodercli/releases/0.1.9/qodercli_0.1.9_linux_arm64.tar.gz",
59
- "sha256": "a631c4cdef2ff37624aaafbac8a6b5db70f89483897c18213de0c43b3e33a81d"
58
+ "url": "https://download.qoder.com/qodercli/releases/0.1.11/qodercli_0.1.11_linux_arm64.tar.gz",
59
+ "sha256": "295e3a6093ff088369d8eb7a2b47450132b1fbc92cb7a55fcf31c04e10bfd9a6"
60
60
  },
61
61
  {
62
62
  "os": "darwin",
63
63
  "arch": "amd64",
64
- "url": "https://download.qoder.com/qodercli/releases/0.1.9/qodercli_0.1.9_darwin_amd64.zip",
65
- "sha256": "e7df08de42e212007611383e38e46900c403755b4677cfe6c3a4a6f5da034fd5"
64
+ "url": "https://download.qoder.com/qodercli/releases/0.1.11/qodercli_0.1.11_darwin_amd64.zip",
65
+ "sha256": "73a8175015d4d61bfec61ff0ce014a1ce1b514f7781909150fe35d6e0e868fe7"
66
66
  },
67
67
  {
68
68
  "os": "darwin",
69
69
  "arch": "arm64",
70
- "url": "https://download.qoder.com/qodercli/releases/0.1.9/qodercli_0.1.9_darwin_arm64.zip",
71
- "sha256": "d8d398e6507b704936128477d90dd3cce64bd8ac25deee7a17f375f4c6ee7674"
70
+ "url": "https://download.qoder.com/qodercli/releases/0.1.11/qodercli_0.1.11_darwin_arm64.zip",
71
+ "sha256": "ca310ccbc650cc3b4660b14a55fb20cc50cf0344cac56b3b82ef08833f9194cf"
72
72
  },
73
73
  {
74
74
  "os": "windows",
75
75
  "arch": "amd64",
76
- "url": "https://download.qoder.com/qodercli/releases/0.1.9/qodercli_0.1.9_windows_amd64.zip",
77
- "sha256": "7679a23fa24398e5319f9f85556110ead5ae18c145c82d7d3b3852483eec20cf"
76
+ "url": "https://download.qoder.com/qodercli/releases/0.1.11/qodercli_0.1.11_windows_amd64.zip",
77
+ "sha256": "705fd28170052511f452fcfb2caeeee97aaaaf61e0bb8034e5c6bb6c4d9ec58e"
78
78
  }
79
79
  ]
80
80
  }
@@ -6,15 +6,30 @@ const https = require('https');
6
6
  const http = require('http');
7
7
  const { execSync } = require('child_process');
8
8
  const crypto = require('crypto');
9
+ const os = require('os');
9
10
 
10
11
  // Configuration
11
12
  const BINARY_NAME = 'qodercli';
12
13
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
13
14
  const BIN_DIR = path.join(PACKAGE_ROOT, 'bin');
14
15
  const PACKAGE_JSON_PATH = path.join(PACKAGE_ROOT, 'package.json');
16
+ const INSTALL_METHOD = 'npm';
15
17
 
16
18
  class QoderInstaller {
17
19
  constructor() {
20
+ // Mark this as an installation process
21
+ process.env.QODER_CLI_INSTALL = '1';
22
+
23
+ // Initialize logging variables
24
+ this.logFile = null;
25
+ this.logStream = null;
26
+ this.originalConsoleLog = console.log;
27
+ this.originalConsoleError = console.error;
28
+
29
+ // Setup logging first to capture all output including platform detection
30
+ this.setupLogging();
31
+
32
+ // Detect platform and architecture (will be logged if they fail)
18
33
  this.platform = this.detectPlatform();
19
34
  this.arch = this.detectArch();
20
35
  this.binPath = path.join(BIN_DIR, BINARY_NAME + (process.platform === 'win32' ? '.exe' : ''));
@@ -41,15 +56,106 @@ class QoderInstaller {
41
56
  }
42
57
  }
43
58
 
59
+ setupLogging() {
60
+ try {
61
+ // Create log directory: ~/.qoder/logs on all platforms
62
+ const logDir = path.join(os.homedir(), '.qoder', 'logs');
63
+ if (!fs.existsSync(logDir)) {
64
+ fs.mkdirSync(logDir, { recursive: true });
65
+ }
66
+
67
+ // Create log file with timestamp
68
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').split('Z')[0];
69
+ this.logFile = path.join(logDir, `qodercli_install_${INSTALL_METHOD}_${timestamp}.log`);
70
+
71
+ // Create write stream
72
+ this.logStream = fs.createWriteStream(this.logFile, { flags: 'a' });
73
+
74
+ // Write log header
75
+ this.logStream.write(`Installation started at ${new Date().toISOString()}\n`);
76
+ this.logStream.write(`Installation method: ${INSTALL_METHOD}\n`);
77
+ this.logStream.write(`Platform: ${process.platform}/${process.arch}\n`);
78
+ this.logStream.write(`Node.js version: ${process.version}\n`);
79
+ this.logStream.write('================================\n\n');
80
+
81
+ // Create latest log marker
82
+ // Unix: symlink immediately, Windows: will copy after completion
83
+ const latestLogLink = path.join(logDir, 'qodercli_install.log');
84
+ try {
85
+ if (process.platform !== 'win32') {
86
+ // Unix: use symlink (immediate)
87
+ if (fs.existsSync(latestLogLink)) {
88
+ fs.unlinkSync(latestLogLink);
89
+ }
90
+ fs.symlinkSync(this.logFile, latestLogLink);
91
+ }
92
+ // Windows: will copy complete log in closeLogging()
93
+ } catch (e) {
94
+ // Ignore errors - not critical
95
+ }
96
+
97
+ // Redirect console.log and console.error to both terminal and log file
98
+ const self = this;
99
+ console.log = function(...args) {
100
+ const message = args.map(arg =>
101
+ typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
102
+ ).join(' ');
103
+ self.originalConsoleLog.apply(console, args);
104
+ if (self.logStream) {
105
+ self.logStream.write(message + '\n');
106
+ }
107
+ };
108
+
109
+ console.error = function(...args) {
110
+ const message = args.map(arg =>
111
+ typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
112
+ ).join(' ');
113
+ self.originalConsoleError.apply(console, args);
114
+ if (self.logStream) {
115
+ self.logStream.write('[ERROR] ' + message + '\n');
116
+ }
117
+ };
118
+
119
+ // Log file location saved but not printed (will show on error)
120
+
121
+ } catch (error) {
122
+ // If logging setup fails, continue without logging
123
+ this.originalConsoleError.call(console, 'Warning: Failed to setup logging:', error.message);
124
+ this.originalConsoleError.call(console, 'Installation will continue without logging');
125
+ }
126
+ }
127
+
128
+ closeLogging() {
129
+ if (this.logStream) {
130
+ this.logStream.end();
131
+ this.logStream = null;
132
+ }
133
+
134
+ // Update latest log marker after installation completes
135
+ if (this.logFile && process.platform === 'win32') {
136
+ try {
137
+ const logDir = path.dirname(this.logFile);
138
+ const latestLogLink = path.join(logDir, 'qodercli_install.log');
139
+ fs.copyFileSync(this.logFile, latestLogLink);
140
+ } catch (e) {
141
+ // Ignore errors - not critical
142
+ }
143
+ }
144
+
145
+ // Restore original console methods
146
+ console.log = this.originalConsoleLog;
147
+ console.error = this.originalConsoleError;
148
+ }
149
+
44
150
  loadPackageInfo() {
45
151
  try {
46
152
  const packageJson = fs.readFileSync(PACKAGE_JSON_PATH, 'utf8');
47
153
  const packageInfo = JSON.parse(packageJson);
48
-
154
+
49
155
  if (!packageInfo.binaries || !packageInfo.binaries.files) {
50
156
  throw new Error('Binary information missing in package configuration');
51
157
  }
52
-
158
+
53
159
  return packageInfo;
54
160
  } catch (error) {
55
161
  throw new Error(`Unable to read package configuration: ${error.message}`);
@@ -58,7 +164,7 @@ class QoderInstaller {
58
164
 
59
165
  findBinaryInfo() {
60
166
  const files = this.packageInfo.binaries.files;
61
- const targetFile = files.find(file =>
167
+ const targetFile = files.find(file =>
62
168
  file.os === this.platform && file.arch === this.arch
63
169
  );
64
170
 
@@ -71,11 +177,11 @@ class QoderInstaller {
71
177
 
72
178
  async downloadBinary(url, expectedSha256) {
73
179
  console.log(`Downloading binary: ${url}`);
74
-
180
+
75
181
  // Create temporary directory for download operations
76
182
  const os = require('os');
77
183
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'qodercli-install-'));
78
-
184
+
79
185
  // Ensure target directory exists
80
186
  if (!fs.existsSync(BIN_DIR)) {
81
187
  fs.mkdirSync(BIN_DIR, { recursive: true });
@@ -84,29 +190,29 @@ class QoderInstaller {
84
190
  // Download file to temporary directory
85
191
  const filename = path.basename(url);
86
192
  const archivePath = path.join(tempDir, filename);
87
-
193
+
88
194
  try {
89
195
  await this.downloadFile(url, archivePath);
90
-
196
+
91
197
  // Verify checksum
92
198
  console.log('Verifying file integrity...');
93
199
  const actualSha256 = this.calculateSha256(archivePath);
94
200
  if (actualSha256 !== expectedSha256) {
95
201
  throw new Error(`Checksum mismatch. Expected: ${expectedSha256}, Got: ${actualSha256}`);
96
202
  }
97
-
203
+
98
204
  // Extract file to temporary directory first
99
205
  console.log('Extracting binary...');
100
206
  const extractDir = path.join(tempDir, 'extract');
101
207
  fs.mkdirSync(extractDir, { recursive: true });
102
208
  await this.extractArchive(archivePath, filename, extractDir);
103
-
209
+
104
210
  // Move extracted binary to final destination
105
211
  const extractedBinary = this.findExtractedBinary(extractDir);
106
212
  if (extractedBinary.length === 0) {
107
213
  throw new Error(`Binary file not found after extraction in ${extractDir}`);
108
214
  }
109
-
215
+
110
216
  // Try rename first (efficient), fallback to copy+delete if cross-device
111
217
  try {
112
218
  fs.renameSync(extractedBinary[0], this.binPath);
@@ -120,19 +226,16 @@ class QoderInstaller {
120
226
  throw error;
121
227
  }
122
228
  }
123
-
229
+
124
230
  // Set executable permission
125
231
  if (process.platform !== 'win32') {
126
232
  fs.chmodSync(this.binPath, 0o755);
127
233
  }
128
-
234
+
129
235
  // Create installation source marker
130
236
  const sourceFile = path.join(BIN_DIR, '.qodercli-install-resource');
131
237
  fs.writeFileSync(sourceFile, 'npm', 'utf8');
132
-
133
- // Verify installation
134
- this.verifyInstallation();
135
-
238
+
136
239
  } catch (error) {
137
240
  throw error;
138
241
  } finally {
@@ -149,7 +252,7 @@ class QoderInstaller {
149
252
  if (filename.endsWith('.zip')) {
150
253
  // Extract ZIP file using Node.js packages first
151
254
  let extracted = false;
152
-
255
+
153
256
  // Method 1: Use adm-zip package (preferred)
154
257
  try {
155
258
  const AdmZip = require('adm-zip');
@@ -160,7 +263,7 @@ class QoderInstaller {
160
263
  } catch (error) {
161
264
  console.log('adm-zip extraction failed, trying system commands...', error.message);
162
265
  }
163
-
266
+
164
267
  // Method 2: System command fallbacks
165
268
  if (!extracted) {
166
269
  if (process.platform === 'win32') {
@@ -192,7 +295,7 @@ class QoderInstaller {
192
295
  }
193
296
  }
194
297
  }
195
-
298
+
196
299
  if (!extracted) {
197
300
  const platform = process.platform === 'win32' ? 'Windows' : 'Unix';
198
301
  throw new Error(`ZIP extraction failed on ${platform}. Please ensure extraction tools are available.`);
@@ -200,7 +303,7 @@ class QoderInstaller {
200
303
  } else {
201
304
  // Extract tar.gz file using Node.js tar package first
202
305
  let extracted = false;
203
-
306
+
204
307
  // Method 1: Use tar package (preferred)
205
308
  try {
206
309
  const tar = require('tar');
@@ -214,7 +317,7 @@ class QoderInstaller {
214
317
  } catch (error) {
215
318
  console.log('Node.js tar extraction failed, trying system tar command...', error.message);
216
319
  }
217
-
320
+
218
321
  // Method 2: System tar command fallback
219
322
  if (!extracted) {
220
323
  try {
@@ -240,13 +343,13 @@ class QoderInstaller {
240
343
  findExtractedBinary(searchDir) {
241
344
  const results = [];
242
345
  const expectedFilename = BINARY_NAME + (process.platform === 'win32' ? '.exe' : '');
243
-
346
+
244
347
  try {
245
348
  const items = fs.readdirSync(searchDir, { withFileTypes: true });
246
-
349
+
247
350
  for (const item of items) {
248
351
  const fullPath = path.join(searchDir, item.name);
249
-
352
+
250
353
  if (item.isDirectory()) {
251
354
  // Recursively search in subdirectories
252
355
  results.push(...this.findExtractedBinary(fullPath));
@@ -257,7 +360,7 @@ class QoderInstaller {
257
360
  } catch (error) {
258
361
  console.warn(`Unable to search directory ${searchDir}:`, error.message);
259
362
  }
260
-
363
+
261
364
  return results;
262
365
  }
263
366
 
@@ -265,17 +368,25 @@ class QoderInstaller {
265
368
  if (!fs.existsSync(this.binPath)) {
266
369
  throw new Error('Binary installation failed');
267
370
  }
371
+ if (this.logStream?.fd !== undefined) {
372
+ try {
373
+ fs.fsyncSync(this.logStream.fd);
374
+ } catch (e) {
375
+ }
376
+ }
268
377
 
269
378
  try {
270
- // Try to run version command for verification
271
379
  const output = execSync(`"${this.binPath}" --version`, {
272
380
  encoding: 'utf8',
273
- stdio: 'pipe'
381
+ stdio: 'pipe',
382
+ env: { ...process.env, QODER_CLI_INSTALL: '1' }
274
383
  });
384
+ const versionInfo = output.trim();
275
385
  console.log('Installation verified successfully');
276
- console.log(`Version info: ${output.trim()}`);
386
+ return versionInfo;
277
387
  } catch (error) {
278
388
  console.warn('Warning: Unable to verify installation, but binary file exists');
389
+ return null;
279
390
  }
280
391
  }
281
392
 
@@ -284,17 +395,17 @@ class QoderInstaller {
284
395
  const file = fs.createWriteStream(filePath);
285
396
  const client = url.startsWith('https:') ? https : http;
286
397
  let cleanupDone = false;
287
-
398
+
288
399
  const cleanup = () => {
289
400
  if (cleanupDone) return;
290
401
  cleanupDone = true;
291
-
402
+
292
403
  try {
293
404
  file.close();
294
405
  } catch (e) {
295
406
  // Ignore errors during cleanup
296
407
  }
297
-
408
+
298
409
  try {
299
410
  if (fs.existsSync(filePath)) {
300
411
  fs.unlinkSync(filePath);
@@ -303,15 +414,24 @@ class QoderInstaller {
303
414
  // Ignore errors during cleanup
304
415
  }
305
416
  };
306
-
307
- const request = client.get(url, (response) => {
417
+ const parsedUrl = new URL(url);
418
+ const options = {
419
+ hostname: parsedUrl.hostname,
420
+ port: parsedUrl.port,
421
+ path: parsedUrl.pathname + parsedUrl.search,
422
+ headers: {
423
+ 'User-Agent': 'qodercli-installer/npm (https://qoder.com)'
424
+ }
425
+ };
426
+
427
+ const request = client.get(options, (response) => {
308
428
  if (response.statusCode === 302 || response.statusCode === 301) {
309
429
  // Handle redirect
310
430
  cleanup();
311
431
  return this.downloadFile(response.headers.location, filePath, timeout)
312
432
  .then(resolve).catch(reject);
313
433
  }
314
-
434
+
315
435
  if (response.statusCode !== 200) {
316
436
  cleanup();
317
437
  reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
@@ -319,14 +439,14 @@ class QoderInstaller {
319
439
  }
320
440
 
321
441
  response.pipe(file);
322
-
442
+
323
443
  file.on('finish', () => {
324
444
  if (!cleanupDone) {
325
445
  file.close();
326
446
  resolve();
327
447
  }
328
448
  });
329
-
449
+
330
450
  file.on('error', (error) => {
331
451
  cleanup();
332
452
  reject(error);
@@ -342,27 +462,27 @@ class QoderInstaller {
342
462
  cleanup();
343
463
  reject(new Error(`Download timeout (${timeout}ms): ${url}`));
344
464
  });
345
-
465
+
346
466
  // Handle process interruption signals
347
467
  const handleSignal = () => {
348
468
  request.destroy();
349
469
  cleanup();
350
470
  reject(new Error('Download interrupted by signal'));
351
471
  };
352
-
472
+
353
473
  process.once('SIGINT', handleSignal);
354
474
  process.once('SIGTERM', handleSignal);
355
-
475
+
356
476
  // Clean up signal handlers when promise resolves/rejects
357
477
  const originalResolve = resolve;
358
478
  const originalReject = reject;
359
-
479
+
360
480
  resolve = (...args) => {
361
481
  process.removeListener('SIGINT', handleSignal);
362
482
  process.removeListener('SIGTERM', handleSignal);
363
483
  originalResolve(...args);
364
484
  };
365
-
485
+
366
486
  reject = (...args) => {
367
487
  process.removeListener('SIGINT', handleSignal);
368
488
  process.removeListener('SIGTERM', handleSignal);
@@ -372,11 +492,14 @@ class QoderInstaller {
372
492
  }
373
493
 
374
494
  async install() {
495
+ let installSuccess = false;
496
+ let versionInfo = null;
497
+
375
498
  try {
376
499
  console.log('Installing Qoder CLI...');
377
500
  console.log(`Target platform: ${this.platform}/${this.arch}`);
378
501
  console.log(`Version: ${this.packageInfo.binaries.version}`);
379
-
502
+
380
503
  // If already installed, reinstall
381
504
  if (fs.existsSync(this.binPath)) {
382
505
  console.log('Existing version detected, will reinstall');
@@ -384,27 +507,75 @@ class QoderInstaller {
384
507
 
385
508
  const binaryInfo = this.findBinaryInfo();
386
509
  await this.downloadBinary(binaryInfo.url, binaryInfo.sha256);
387
-
388
- console.log('✅ Qoder CLI installed successfully!');
389
- console.log(`Run 'npx qodercli --help' to get started`);
390
-
510
+
511
+ // Verify and get version info
512
+ versionInfo = this.verifyInstallation();
513
+ installSuccess = true;
514
+
391
515
  } catch (error) {
516
+ console.error('');
392
517
  console.error('❌ Installation failed:', error.message);
518
+ console.error('');
519
+ if (this.logFile) {
520
+ console.error(`Installation log: ${this.logFile}`);
521
+ console.error('');
522
+ }
523
+ console.error('For help, visit: https://forum.qoder.com/c/bug-reports');
524
+ console.error('');
525
+
526
+ this.closeLogging();
393
527
  process.exit(1);
528
+ } finally {
529
+ // Show final summary
530
+ if (installSuccess) {
531
+ this.showSuccessSummary(versionInfo);
532
+ }
533
+
534
+ // Close logging
535
+ this.closeLogging();
394
536
  }
395
537
  }
538
+
539
+ showSuccessSummary(versionInfo) {
540
+ console.log('');
541
+
542
+ if (versionInfo) {
543
+ console.log(`🎉 ${versionInfo} installed successfully!`);
544
+ } else {
545
+ console.log('🎉 Qoder CLI installed successfully!');
546
+ }
547
+
548
+ console.log('');
549
+ console.log('Get started: qodercli --help');
550
+ console.log('Need help? Visit: https://forum.qoder.com/c/bug-reports');
551
+ console.log('');
552
+ }
396
553
  }
397
554
 
398
555
  // Main program
399
556
  if (require.main === module) {
557
+ let installer = null;
400
558
  try {
401
- const installer = new QoderInstaller();
559
+ installer = new QoderInstaller();
402
560
  installer.install();
403
561
  } catch (error) {
562
+ console.error('');
404
563
  console.error('❌ Failed to initialize installer:', error.message);
564
+ console.error('');
405
565
  console.error('This might be due to Node.js version compatibility issues.');
406
566
  console.error(`Current Node.js version: ${process.version}`);
407
567
  console.error('Required Node.js version: >=14');
568
+ console.error('');
569
+
570
+ // Show log file location if logging was initialized
571
+ // (now possible since setupLogging() runs first)
572
+ if (installer && installer.logFile) {
573
+ console.error(`Installation log: ${installer.logFile}`);
574
+ console.error('');
575
+ }
576
+
577
+ console.error('For help, visit: https://forum.qoder.com/c/bug-reports');
578
+ console.error('');
408
579
  process.exit(1);
409
580
  }
410
581
  }