ai-rulez 1.0.0 → 1.1.1
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/install.js +271 -37
- package/package.json +1 -1
package/install.js
CHANGED
|
@@ -1,12 +1,46 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const https = require('https');
|
|
4
|
-
const
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { exec, spawn } = require('child_process');
|
|
5
7
|
const { promisify } = require('util');
|
|
6
8
|
|
|
7
9
|
const execAsync = promisify(exec);
|
|
8
10
|
|
|
9
11
|
const REPO_NAME = 'Goldziher/ai-rulez';
|
|
12
|
+
const DOWNLOAD_TIMEOUT = 30000; // 30 seconds
|
|
13
|
+
const MAX_RETRIES = 3;
|
|
14
|
+
const RETRY_DELAY = 2000; // 2 seconds
|
|
15
|
+
|
|
16
|
+
async function calculateSHA256(filePath) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const hash = crypto.createHash('sha256');
|
|
19
|
+
const stream = fs.createReadStream(filePath);
|
|
20
|
+
|
|
21
|
+
stream.on('data', (data) => hash.update(data));
|
|
22
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
23
|
+
stream.on('error', reject);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function getExpectedChecksum(checksumPath, filename) {
|
|
28
|
+
try {
|
|
29
|
+
const checksumContent = fs.readFileSync(checksumPath, 'utf8');
|
|
30
|
+
const lines = checksumContent.split('\n');
|
|
31
|
+
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const parts = line.trim().split(/\s+/);
|
|
34
|
+
if (parts.length >= 2 && parts[1] === filename) {
|
|
35
|
+
return parts[0];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.warn('Warning: Could not parse checksums file:', error.message);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
10
44
|
|
|
11
45
|
function getPlatform() {
|
|
12
46
|
const platform = process.platform;
|
|
@@ -28,13 +62,17 @@ function getPlatform() {
|
|
|
28
62
|
const mappedPlatform = platformMap[platform];
|
|
29
63
|
const mappedArch = archMap[arch];
|
|
30
64
|
|
|
31
|
-
if (!mappedPlatform
|
|
32
|
-
throw new Error(`Unsupported
|
|
65
|
+
if (!mappedPlatform) {
|
|
66
|
+
throw new Error(`Unsupported operating system: ${platform}. Supported platforms: darwin (macOS), linux, win32 (Windows)`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!mappedArch) {
|
|
70
|
+
throw new Error(`Unsupported architecture: ${arch}. Supported architectures: x64, arm64, ia32`);
|
|
33
71
|
}
|
|
34
72
|
|
|
35
|
-
|
|
73
|
+
|
|
36
74
|
if (mappedPlatform === 'windows' && mappedArch === 'arm64') {
|
|
37
|
-
throw new Error('Windows ARM64 is not supported');
|
|
75
|
+
throw new Error('Windows ARM64 is not currently supported. Please use x64 or ia32 version.');
|
|
38
76
|
}
|
|
39
77
|
|
|
40
78
|
return {
|
|
@@ -47,81 +85,264 @@ function getBinaryName(platform) {
|
|
|
47
85
|
return platform === 'windows' ? 'ai-rulez.exe' : 'ai-rulez';
|
|
48
86
|
}
|
|
49
87
|
|
|
50
|
-
async function downloadBinary(url, dest) {
|
|
88
|
+
async function downloadBinary(url, dest, retryCount = 0) {
|
|
51
89
|
return new Promise((resolve, reject) => {
|
|
52
90
|
const file = fs.createWriteStream(dest);
|
|
91
|
+
const protocol = url.startsWith('https') ? https : http;
|
|
53
92
|
|
|
54
|
-
|
|
93
|
+
const request = protocol.get(url, { timeout: DOWNLOAD_TIMEOUT }, (response) => {
|
|
55
94
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
95
|
+
file.close();
|
|
96
|
+
try { fs.unlinkSync(dest); } catch {} // Clean up partial file
|
|
97
|
+
downloadBinary(response.headers.location, dest, retryCount)
|
|
98
|
+
.then(resolve)
|
|
99
|
+
.catch(reject);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (response.statusCode !== 200) {
|
|
104
|
+
file.close();
|
|
105
|
+
try { fs.unlinkSync(dest); } catch {} // Clean up partial file
|
|
106
|
+
const error = new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`);
|
|
107
|
+
|
|
108
|
+
if (retryCount < MAX_RETRIES) {
|
|
109
|
+
console.log(`Download failed, retrying in ${RETRY_DELAY/1000}s... (${retryCount + 1}/${MAX_RETRIES})`);
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
downloadBinary(url, dest, retryCount + 1)
|
|
112
|
+
.then(resolve)
|
|
113
|
+
.catch(reject);
|
|
114
|
+
}, RETRY_DELAY);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
reject(error);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let downloadedBytes = 0;
|
|
123
|
+
response.on('data', (chunk) => {
|
|
124
|
+
downloadedBytes += chunk.length;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
response.pipe(file);
|
|
128
|
+
|
|
129
|
+
file.on('finish', () => {
|
|
130
|
+
file.close();
|
|
131
|
+
if (downloadedBytes === 0) {
|
|
132
|
+
try { fs.unlinkSync(dest); } catch {}
|
|
133
|
+
reject(new Error('Downloaded file is empty'));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
console.log(`Downloaded ${downloadedBytes} bytes`);
|
|
137
|
+
resolve();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
file.on('error', (err) => {
|
|
141
|
+
file.close();
|
|
142
|
+
try { fs.unlinkSync(dest); } catch {}
|
|
143
|
+
reject(err);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
request.on('timeout', () => {
|
|
148
|
+
request.destroy();
|
|
149
|
+
file.close();
|
|
150
|
+
try { fs.unlinkSync(dest); } catch {}
|
|
151
|
+
|
|
152
|
+
if (retryCount < MAX_RETRIES) {
|
|
153
|
+
console.log(`Download timeout, retrying in ${RETRY_DELAY/1000}s... (${retryCount + 1}/${MAX_RETRIES})`);
|
|
154
|
+
setTimeout(() => {
|
|
155
|
+
downloadBinary(url, dest, retryCount + 1)
|
|
156
|
+
.then(resolve)
|
|
157
|
+
.catch(reject);
|
|
158
|
+
}, RETRY_DELAY);
|
|
159
|
+
return;
|
|
72
160
|
}
|
|
73
|
-
|
|
161
|
+
|
|
162
|
+
reject(new Error('Download timeout after multiple retries'));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
request.on('error', (err) => {
|
|
166
|
+
file.close();
|
|
167
|
+
try { fs.unlinkSync(dest); } catch {}
|
|
168
|
+
|
|
169
|
+
if (retryCount < MAX_RETRIES) {
|
|
170
|
+
console.log(`Download error, retrying in ${RETRY_DELAY/1000}s... (${retryCount + 1}/${MAX_RETRIES})`);
|
|
171
|
+
setTimeout(() => {
|
|
172
|
+
downloadBinary(url, dest, retryCount + 1)
|
|
173
|
+
.then(resolve)
|
|
174
|
+
.catch(reject);
|
|
175
|
+
}, RETRY_DELAY);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
reject(err);
|
|
180
|
+
});
|
|
74
181
|
});
|
|
75
182
|
}
|
|
76
183
|
|
|
77
184
|
async function extractArchive(archivePath, extractDir, platform) {
|
|
78
185
|
if (platform === 'windows') {
|
|
79
|
-
// Use PowerShell
|
|
80
|
-
|
|
186
|
+
// Use safer PowerShell execution with proper escaping
|
|
187
|
+
const escapedArchivePath = archivePath.replace(/'/g, "''");
|
|
188
|
+
const escapedExtractDir = extractDir.replace(/'/g, "''");
|
|
189
|
+
|
|
190
|
+
const powershellCommand = [
|
|
191
|
+
'powershell.exe',
|
|
192
|
+
'-NoProfile',
|
|
193
|
+
'-ExecutionPolicy', 'Bypass',
|
|
194
|
+
'-Command',
|
|
195
|
+
`Expand-Archive -LiteralPath '${escapedArchivePath}' -DestinationPath '${escapedExtractDir}' -Force`
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
await new Promise((resolve, reject) => {
|
|
199
|
+
const child = spawn(powershellCommand[0], powershellCommand.slice(1), {
|
|
200
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
201
|
+
windowsHide: true
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
let stderr = '';
|
|
205
|
+
child.stderr.on('data', (data) => {
|
|
206
|
+
stderr += data.toString();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
child.on('close', (code) => {
|
|
210
|
+
if (code === 0) {
|
|
211
|
+
resolve();
|
|
212
|
+
} else {
|
|
213
|
+
reject(new Error(`PowerShell extraction failed with code ${code}: ${stderr}`));
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
child.on('error', reject);
|
|
218
|
+
});
|
|
81
219
|
} else {
|
|
82
|
-
// Use
|
|
83
|
-
await
|
|
220
|
+
// Use spawn instead of exec for better security and error handling
|
|
221
|
+
await new Promise((resolve, reject) => {
|
|
222
|
+
const child = spawn('tar', ['-xzf', archivePath, '-C', extractDir], {
|
|
223
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
let stderr = '';
|
|
227
|
+
child.stderr.on('data', (data) => {
|
|
228
|
+
stderr += data.toString();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
child.on('close', (code) => {
|
|
232
|
+
if (code === 0) {
|
|
233
|
+
resolve();
|
|
234
|
+
} else {
|
|
235
|
+
reject(new Error(`tar extraction failed with code ${code}: ${stderr}`));
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
child.on('error', reject);
|
|
240
|
+
});
|
|
84
241
|
}
|
|
85
242
|
}
|
|
86
243
|
|
|
87
244
|
async function install() {
|
|
88
245
|
try {
|
|
246
|
+
// Check Node.js version compatibility
|
|
247
|
+
const nodeVersion = process.version;
|
|
248
|
+
const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]);
|
|
249
|
+
if (majorVersion < 20) {
|
|
250
|
+
console.error(`Error: Node.js ${nodeVersion} is not supported. Please upgrade to Node.js 20 or later.`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
|
|
89
254
|
const { os, arch } = getPlatform();
|
|
90
255
|
const binaryName = getBinaryName(os);
|
|
91
256
|
|
|
92
|
-
|
|
257
|
+
|
|
93
258
|
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
|
|
94
259
|
const version = packageJson.version;
|
|
95
260
|
|
|
96
|
-
|
|
261
|
+
|
|
97
262
|
const archiveExt = os === 'windows' ? 'zip' : 'tar.gz';
|
|
98
263
|
const archiveName = `ai-rulez_${version}_${os}_${arch}.${archiveExt}`;
|
|
99
264
|
const downloadUrl = `https://github.com/${REPO_NAME}/releases/download/v${version}/${archiveName}`;
|
|
265
|
+
const checksumUrl = `https://github.com/${REPO_NAME}/releases/download/v${version}/checksums.txt`;
|
|
100
266
|
|
|
101
267
|
console.log(`Downloading ai-rulez ${version} for ${os}/${arch}...`);
|
|
102
268
|
console.log(`URL: ${downloadUrl}`);
|
|
103
269
|
|
|
104
|
-
|
|
270
|
+
|
|
105
271
|
const binDir = path.join(__dirname, 'bin');
|
|
106
272
|
if (!fs.existsSync(binDir)) {
|
|
107
273
|
fs.mkdirSync(binDir, { recursive: true });
|
|
108
274
|
}
|
|
109
275
|
|
|
110
|
-
|
|
276
|
+
|
|
111
277
|
const archivePath = path.join(__dirname, archiveName);
|
|
278
|
+
|
|
279
|
+
// Download checksums first for verification
|
|
280
|
+
console.log('Downloading checksums...');
|
|
281
|
+
const checksumPath = path.join(__dirname, 'checksums.txt');
|
|
282
|
+
try {
|
|
283
|
+
await downloadBinary(checksumUrl, checksumPath);
|
|
284
|
+
} catch (checksumError) {
|
|
285
|
+
console.warn('Warning: Could not download checksums, skipping verification');
|
|
286
|
+
}
|
|
287
|
+
|
|
112
288
|
await downloadBinary(downloadUrl, archivePath);
|
|
113
289
|
|
|
114
|
-
//
|
|
290
|
+
// Verify checksum if available
|
|
291
|
+
if (fs.existsSync(checksumPath)) {
|
|
292
|
+
console.log('Verifying checksum...');
|
|
293
|
+
const expectedHash = await getExpectedChecksum(checksumPath, archiveName);
|
|
294
|
+
if (expectedHash) {
|
|
295
|
+
const actualHash = await calculateSHA256(archivePath);
|
|
296
|
+
if (actualHash !== expectedHash) {
|
|
297
|
+
throw new Error(`Checksum verification failed. Expected: ${expectedHash}, Got: ${actualHash}`);
|
|
298
|
+
}
|
|
299
|
+
console.log('✓ Checksum verified');
|
|
300
|
+
}
|
|
301
|
+
fs.unlinkSync(checksumPath);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
115
305
|
console.log('Extracting binary...');
|
|
116
306
|
await extractArchive(archivePath, binDir, os);
|
|
117
307
|
|
|
118
|
-
|
|
308
|
+
|
|
309
|
+
const binaryPath = path.join(binDir, binaryName);
|
|
310
|
+
if (!fs.existsSync(binaryPath)) {
|
|
311
|
+
throw new Error(`Binary not found after extraction: ${binaryPath}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
119
315
|
if (os !== 'windows') {
|
|
120
|
-
const binaryPath = path.join(binDir, binaryName);
|
|
121
316
|
fs.chmodSync(binaryPath, 0o755);
|
|
122
317
|
}
|
|
123
318
|
|
|
124
|
-
//
|
|
319
|
+
// Verify binary is executable
|
|
320
|
+
try {
|
|
321
|
+
await new Promise((resolve, reject) => {
|
|
322
|
+
const testCommand = os === 'windows' ? [binaryPath, '--version'] : [binaryPath, '--version'];
|
|
323
|
+
const child = spawn(testCommand[0], testCommand.slice(1), {
|
|
324
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
325
|
+
timeout: 5000
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
child.on('close', (code) => {
|
|
329
|
+
// Any exit code is fine, we just want to verify it can execute
|
|
330
|
+
resolve();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
child.on('error', (err) => {
|
|
334
|
+
if (err.code === 'ENOENT') {
|
|
335
|
+
reject(new Error('Downloaded binary is not executable'));
|
|
336
|
+
} else {
|
|
337
|
+
resolve(); // Other errors are OK for version check
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
} catch (verifyError) {
|
|
342
|
+
console.warn('Warning: Could not verify binary execution:', verifyError.message);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
|
|
125
346
|
fs.unlinkSync(archivePath);
|
|
126
347
|
|
|
127
348
|
console.log(`✅ ai-rulez ${version} installed successfully for ${os}/${arch}!`);
|
|
@@ -134,7 +355,20 @@ async function install() {
|
|
|
134
355
|
}
|
|
135
356
|
}
|
|
136
357
|
|
|
137
|
-
|
|
358
|
+
|
|
359
|
+
// Export functions for testing
|
|
360
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
361
|
+
module.exports = {
|
|
362
|
+
getPlatform,
|
|
363
|
+
getBinaryName,
|
|
364
|
+
downloadBinary,
|
|
365
|
+
extractArchive,
|
|
366
|
+
calculateSHA256,
|
|
367
|
+
getExpectedChecksum,
|
|
368
|
+
install
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
138
372
|
if (require.main === module) {
|
|
139
373
|
install();
|
|
140
374
|
}
|
package/package.json
CHANGED