@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/README.md +327 -2
- package/bundle/builtin/agent-creator/SKILL.md +327 -0
- package/bundle/builtin/hook-config/SKILL.md +480 -0
- package/bundle/builtin/mcp-config/SKILL.md +155 -0
- package/bundle/builtin/skill-creator/SKILL.md +294 -0
- package/bundle/node_modules/@google/gemini-cli-devtools/dist/client/main.js +101 -0
- package/bundle/node_modules/@google/gemini-cli-devtools/dist/src/_client-assets.d.ts +7 -0
- package/bundle/node_modules/@google/gemini-cli-devtools/dist/src/_client-assets.js +9 -0
- package/bundle/node_modules/@google/gemini-cli-devtools/dist/src/index.d.ts +48 -0
- package/bundle/node_modules/@google/gemini-cli-devtools/dist/src/index.js +323 -0
- package/bundle/node_modules/@google/gemini-cli-devtools/dist/src/types.d.ts +36 -0
- package/bundle/node_modules/@google/gemini-cli-devtools/dist/src/types.js +7 -0
- package/bundle/node_modules/@google/gemini-cli-devtools/package.json +34 -0
- package/bundle/policies/sandbox-default.toml +19 -0
- package/bundle/qodercli.js +5170 -0
- package/bundle/sandbox-macos-permissive-open.sb +27 -0
- package/bundle/sandbox-macos-permissive-proxied.sb +37 -0
- package/bundle/sandbox-macos-restrictive-open.sb +96 -0
- package/bundle/sandbox-macos-restrictive-proxied.sb +98 -0
- package/bundle/sandbox-macos-strict-open.sb +131 -0
- package/bundle/sandbox-macos-strict-proxied.sb +133 -0
- package/package.json +28 -81
- package/bin/qodercli +0 -47
- package/scripts/install.js +0 -647
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
|
-
});
|
package/scripts/install.js
DELETED
|
@@ -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;
|