@qoder-ai/qodercli 0.2.3 → 0.2.4-beta

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