ai-rulez 1.6.1 → 2.0.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 CHANGED
@@ -1,415 +1,452 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const https = require('https');
4
- const http = require('http');
5
- const crypto = require('crypto');
6
- const { exec, spawn } = require('child_process');
7
- const { promisify } = require('util');
8
-
9
- const execAsync = promisify(exec);
10
-
11
- const REPO_NAME = 'Goldziher/ai-rulez';
12
- const DOWNLOAD_TIMEOUT = 30000; // 30 seconds
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const https = require("node:https");
4
+ const http = require("node:http");
5
+ const crypto = require("node:crypto");
6
+ const { exec, spawn } = require("node:child_process");
7
+ const { promisify } = require("node:util");
8
+
9
+ const _execAsync = promisify(exec);
10
+
11
+ const REPO_NAME = "Goldziher/ai-rulez";
12
+ const DOWNLOAD_TIMEOUT = 30000;
13
13
  const MAX_RETRIES = 3;
14
- const RETRY_DELAY = 2000; // 2 seconds
14
+ const RETRY_DELAY = 2000;
15
15
 
16
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
- });
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
25
  }
26
26
 
27
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
- }
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
43
  }
44
44
 
45
45
  function getPlatform() {
46
- const platform = process.platform;
47
- const arch = process.arch;
48
-
49
- const platformMap = {
50
- 'darwin': 'darwin',
51
- 'linux': 'linux',
52
- 'win32': 'windows'
53
- };
54
-
55
- const archMap = {
56
- 'x64': 'amd64',
57
- 'arm64': 'arm64',
58
- 'ia32': '386',
59
- 'x32': '386'
60
- };
61
-
62
- const mappedPlatform = platformMap[platform];
63
- const mappedArch = archMap[arch];
64
-
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`);
71
- }
72
-
73
-
74
- if (mappedPlatform === 'windows' && mappedArch === 'arm64') {
75
- throw new Error('Windows ARM64 is not currently supported. Please use x64 or ia32 version.');
76
- }
77
-
78
- return {
79
- os: mappedPlatform,
80
- arch: mappedArch
81
- };
46
+ const platform = process.platform;
47
+ const arch = process.arch;
48
+
49
+ const platformMap = {
50
+ darwin: "darwin",
51
+ linux: "linux",
52
+ win32: "windows",
53
+ };
54
+
55
+ const archMap = {
56
+ x64: "amd64",
57
+ arm64: "arm64",
58
+ ia32: "386",
59
+ x32: "386",
60
+ };
61
+
62
+ const mappedPlatform = platformMap[platform];
63
+ const mappedArch = archMap[arch];
64
+
65
+ if (!mappedPlatform) {
66
+ throw new Error(
67
+ `Unsupported operating system: ${platform}. Supported platforms: darwin (macOS), linux, win32 (Windows)`,
68
+ );
69
+ }
70
+
71
+ if (!mappedArch) {
72
+ throw new Error(
73
+ `Unsupported architecture: ${arch}. Supported architectures: x64, arm64, ia32`,
74
+ );
75
+ }
76
+
77
+ if (mappedPlatform === "windows" && mappedArch === "arm64") {
78
+ throw new Error(
79
+ "Windows ARM64 is not currently supported. Please use x64 or ia32 version.",
80
+ );
81
+ }
82
+
83
+ return {
84
+ os: mappedPlatform,
85
+ arch: mappedArch,
86
+ };
82
87
  }
83
88
 
84
89
  function getBinaryName(platform) {
85
- // Use a different name to avoid conflict with the wrapper script
86
- return platform === 'windows' ? 'ai-rulez-bin.exe' : 'ai-rulez-bin';
90
+ return platform === "windows" ? "ai-rulez-bin.exe" : "ai-rulez-bin";
87
91
  }
88
92
 
89
93
  async function downloadBinary(url, dest, retryCount = 0) {
90
- return new Promise((resolve, reject) => {
91
- const file = fs.createWriteStream(dest);
92
- const protocol = url.startsWith('https') ? https : http;
93
-
94
- const request = protocol.get(url, { timeout: DOWNLOAD_TIMEOUT }, (response) => {
95
- if (response.statusCode === 302 || response.statusCode === 301) {
96
- file.close();
97
- try { fs.unlinkSync(dest); } catch {} // Clean up partial file
98
- downloadBinary(response.headers.location, dest, retryCount)
99
- .then(resolve)
100
- .catch(reject);
101
- return;
102
- }
103
-
104
- if (response.statusCode !== 200) {
105
- file.close();
106
- try { fs.unlinkSync(dest); } catch {} // Clean up partial file
107
- const error = new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`);
108
-
109
- if (retryCount < MAX_RETRIES) {
110
- console.log(`Download failed, retrying in ${RETRY_DELAY/1000}s... (${retryCount + 1}/${MAX_RETRIES})`);
111
- setTimeout(() => {
112
- downloadBinary(url, dest, retryCount + 1)
113
- .then(resolve)
114
- .catch(reject);
115
- }, RETRY_DELAY);
116
- return;
117
- }
118
-
119
- reject(error);
120
- return;
121
- }
122
-
123
- let downloadedBytes = 0;
124
- response.on('data', (chunk) => {
125
- downloadedBytes += chunk.length;
126
- });
127
-
128
- response.pipe(file);
129
-
130
- file.on('finish', () => {
131
- file.close();
132
- if (downloadedBytes === 0) {
133
- try { fs.unlinkSync(dest); } catch {}
134
- reject(new Error('Downloaded file is empty'));
135
- return;
136
- }
137
- console.log(`Downloaded ${downloadedBytes} bytes`);
138
- resolve();
139
- });
140
-
141
- file.on('error', (err) => {
142
- file.close();
143
- try { fs.unlinkSync(dest); } catch {}
144
- reject(err);
145
- });
146
- });
147
-
148
- request.on('timeout', () => {
149
- request.destroy();
150
- file.close();
151
- try { fs.unlinkSync(dest); } catch {}
152
-
153
- if (retryCount < MAX_RETRIES) {
154
- console.log(`Download timeout, retrying in ${RETRY_DELAY/1000}s... (${retryCount + 1}/${MAX_RETRIES})`);
155
- setTimeout(() => {
156
- downloadBinary(url, dest, retryCount + 1)
157
- .then(resolve)
158
- .catch(reject);
159
- }, RETRY_DELAY);
160
- return;
161
- }
162
-
163
- reject(new Error('Download timeout after multiple retries'));
164
- });
165
-
166
- request.on('error', (err) => {
167
- file.close();
168
- try { fs.unlinkSync(dest); } catch {}
169
-
170
- if (retryCount < MAX_RETRIES) {
171
- console.log(`Download error, retrying in ${RETRY_DELAY/1000}s... (${retryCount + 1}/${MAX_RETRIES})`);
172
- setTimeout(() => {
173
- downloadBinary(url, dest, retryCount + 1)
174
- .then(resolve)
175
- .catch(reject);
176
- }, RETRY_DELAY);
177
- return;
178
- }
179
-
180
- reject(err);
181
- });
182
- });
94
+ return new Promise((resolve, reject) => {
95
+ const file = fs.createWriteStream(dest);
96
+ const protocol = url.startsWith("https") ? https : http;
97
+
98
+ const request = protocol.get(
99
+ url,
100
+ { timeout: DOWNLOAD_TIMEOUT },
101
+ (response) => {
102
+ if (response.statusCode === 302 || response.statusCode === 301) {
103
+ file.close();
104
+ try {
105
+ fs.unlinkSync(dest);
106
+ } catch {}
107
+ downloadBinary(response.headers.location, dest, retryCount)
108
+ .then(resolve)
109
+ .catch(reject);
110
+ return;
111
+ }
112
+
113
+ if (response.statusCode !== 200) {
114
+ file.close();
115
+ try {
116
+ fs.unlinkSync(dest);
117
+ } catch {}
118
+ const error = new Error(
119
+ `HTTP ${response.statusCode}: ${response.statusMessage}`,
120
+ );
121
+
122
+ if (retryCount < MAX_RETRIES) {
123
+ console.log(
124
+ `Download failed, retrying in ${RETRY_DELAY / 1000}s... (${retryCount + 1}/${MAX_RETRIES})`,
125
+ );
126
+ setTimeout(() => {
127
+ downloadBinary(url, dest, retryCount + 1)
128
+ .then(resolve)
129
+ .catch(reject);
130
+ }, RETRY_DELAY);
131
+ return;
132
+ }
133
+
134
+ reject(error);
135
+ return;
136
+ }
137
+
138
+ let downloadedBytes = 0;
139
+ response.on("data", (chunk) => {
140
+ downloadedBytes += chunk.length;
141
+ });
142
+
143
+ response.pipe(file);
144
+
145
+ file.on("finish", () => {
146
+ file.close();
147
+ if (downloadedBytes === 0) {
148
+ try {
149
+ fs.unlinkSync(dest);
150
+ } catch {}
151
+ reject(new Error("Downloaded file is empty"));
152
+ return;
153
+ }
154
+ console.log(`Downloaded ${downloadedBytes} bytes`);
155
+ resolve();
156
+ });
157
+
158
+ file.on("error", (err) => {
159
+ file.close();
160
+ try {
161
+ fs.unlinkSync(dest);
162
+ } catch {}
163
+ reject(err);
164
+ });
165
+ },
166
+ );
167
+
168
+ request.on("timeout", () => {
169
+ request.destroy();
170
+ file.close();
171
+ try {
172
+ fs.unlinkSync(dest);
173
+ } catch {}
174
+
175
+ if (retryCount < MAX_RETRIES) {
176
+ console.log(
177
+ `Download timeout, retrying in ${RETRY_DELAY / 1000}s... (${retryCount + 1}/${MAX_RETRIES})`,
178
+ );
179
+ setTimeout(() => {
180
+ downloadBinary(url, dest, retryCount + 1)
181
+ .then(resolve)
182
+ .catch(reject);
183
+ }, RETRY_DELAY);
184
+ return;
185
+ }
186
+
187
+ reject(new Error("Download timeout after multiple retries"));
188
+ });
189
+
190
+ request.on("error", (err) => {
191
+ file.close();
192
+ try {
193
+ fs.unlinkSync(dest);
194
+ } catch {}
195
+
196
+ if (retryCount < MAX_RETRIES) {
197
+ console.log(
198
+ `Download error, retrying in ${RETRY_DELAY / 1000}s... (${retryCount + 1}/${MAX_RETRIES})`,
199
+ );
200
+ setTimeout(() => {
201
+ downloadBinary(url, dest, retryCount + 1)
202
+ .then(resolve)
203
+ .catch(reject);
204
+ }, RETRY_DELAY);
205
+ return;
206
+ }
207
+
208
+ reject(err);
209
+ });
210
+ });
183
211
  }
184
212
 
185
213
  async function extractArchive(archivePath, extractDir, platform) {
186
- if (platform === 'windows') {
187
- // Use safer PowerShell execution with proper escaping
188
- const escapedArchivePath = archivePath.replace(/'/g, "''");
189
- const escapedExtractDir = extractDir.replace(/'/g, "''");
190
-
191
- const powershellCommand = [
192
- 'powershell.exe',
193
- '-NoProfile',
194
- '-ExecutionPolicy', 'Bypass',
195
- '-Command',
196
- `Expand-Archive -LiteralPath '${escapedArchivePath}' -DestinationPath '${escapedExtractDir}' -Force`
197
- ];
198
-
199
- await new Promise((resolve, reject) => {
200
- const child = spawn(powershellCommand[0], powershellCommand.slice(1), {
201
- stdio: ['pipe', 'pipe', 'pipe'],
202
- windowsHide: true
203
- });
204
-
205
- let stderr = '';
206
- child.stderr.on('data', (data) => {
207
- stderr += data.toString();
208
- });
209
-
210
- child.on('close', (code) => {
211
- if (code === 0) {
212
- resolve();
213
- } else {
214
- reject(new Error(`PowerShell extraction failed with code ${code}: ${stderr}`));
215
- }
216
- });
217
-
218
- child.on('error', reject);
219
- });
220
- } else {
221
- // Use spawn instead of exec for better security and error handling
222
- await new Promise((resolve, reject) => {
223
- const child = spawn('tar', ['-xzf', archivePath, '-C', extractDir], {
224
- stdio: ['pipe', 'pipe', 'pipe']
225
- });
226
-
227
- let stderr = '';
228
- child.stderr.on('data', (data) => {
229
- stderr += data.toString();
230
- });
231
-
232
- child.on('close', (code) => {
233
- if (code === 0) {
234
- resolve();
235
- } else {
236
- reject(new Error(`tar extraction failed with code ${code}: ${stderr}`));
237
- }
238
- });
239
-
240
- child.on('error', reject);
241
- });
242
- }
243
- }
214
+ if (platform === "windows") {
215
+ const escapedArchivePath = archivePath.replace(/'/g, "''");
216
+ const escapedExtractDir = extractDir.replace(/'/g, "''");
217
+
218
+ const powershellCommand = [
219
+ "powershell.exe",
220
+ "-NoProfile",
221
+ "-ExecutionPolicy",
222
+ "Bypass",
223
+ "-Command",
224
+ `Expand-Archive -LiteralPath '${escapedArchivePath}' -DestinationPath '${escapedExtractDir}' -Force`,
225
+ ];
226
+
227
+ await new Promise((resolve, reject) => {
228
+ const child = spawn(powershellCommand[0], powershellCommand.slice(1), {
229
+ stdio: ["pipe", "pipe", "pipe"],
230
+ windowsHide: true,
231
+ });
232
+
233
+ let stderr = "";
234
+ child.stderr.on("data", (data) => {
235
+ stderr += data.toString();
236
+ });
237
+
238
+ child.on("close", (code) => {
239
+ if (code === 0) {
240
+ resolve();
241
+ } else {
242
+ reject(
243
+ new Error(
244
+ `PowerShell extraction failed with code ${code}: ${stderr}`,
245
+ ),
246
+ );
247
+ }
248
+ });
249
+
250
+ child.on("error", reject);
251
+ });
252
+ } else {
253
+ await new Promise((resolve, reject) => {
254
+ const child = spawn("tar", ["-xzf", archivePath, "-C", extractDir], {
255
+ stdio: ["pipe", "pipe", "pipe"],
256
+ });
244
257
 
245
- async function install() {
246
- const DEBUG = process.env.AI_RULEZ_DEBUG === '1';
247
-
248
- try {
249
- if (DEBUG) console.error('[install.js] Starting installation');
250
-
251
- // Check Node.js version compatibility
252
- const nodeVersion = process.version;
253
- const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]);
254
- if (majorVersion < 20) {
255
- console.error(`Error: Node.js ${nodeVersion} is not supported. Please upgrade to Node.js 20 or later.`);
256
- process.exit(1);
257
- }
258
-
259
- const { os, arch } = getPlatform();
260
- const binaryName = getBinaryName(os);
261
-
262
- if (DEBUG) console.error(`[install.js] Platform: ${os}/${arch}, Binary name: ${binaryName}`);
263
-
264
- const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
265
- const version = packageJson.version;
266
-
267
-
268
- const archiveExt = os === 'windows' ? 'zip' : 'tar.gz';
269
- const archiveName = `ai-rulez_${version}_${os}_${arch}.${archiveExt}`;
270
- const downloadUrl = `https://github.com/${REPO_NAME}/releases/download/v${version}/${archiveName}`;
271
- const checksumUrl = `https://github.com/${REPO_NAME}/releases/download/v${version}/checksums.txt`;
272
-
273
- console.log(`Downloading ai-rulez ${version} for ${os}/${arch}...`);
274
- console.log(`URL: ${downloadUrl}`);
275
-
276
-
277
- const binDir = path.join(__dirname, 'bin');
278
- if (!fs.existsSync(binDir)) {
279
- fs.mkdirSync(binDir, { recursive: true });
280
- }
281
-
282
-
283
- const archivePath = path.join(__dirname, archiveName);
284
-
285
- // Download checksums first for verification
286
- console.log('Downloading checksums...');
287
- const checksumPath = path.join(__dirname, 'checksums.txt');
288
- try {
289
- await downloadBinary(checksumUrl, checksumPath);
290
- } catch (checksumError) {
291
- console.warn('Warning: Could not download checksums, skipping verification');
292
- }
293
-
294
- await downloadBinary(downloadUrl, archivePath);
295
-
296
- // Verify checksum if available
297
- if (fs.existsSync(checksumPath)) {
298
- console.log('Verifying checksum...');
299
- const expectedHash = await getExpectedChecksum(checksumPath, archiveName);
300
- if (expectedHash) {
301
- const actualHash = await calculateSHA256(archivePath);
302
- if (actualHash !== expectedHash) {
303
- throw new Error(`Checksum verification failed. Expected: ${expectedHash}, Got: ${actualHash}`);
304
- }
305
- console.log('✓ Checksum verified');
306
- }
307
- fs.unlinkSync(checksumPath);
308
- }
309
-
310
-
311
- console.log('Extracting binary...');
312
-
313
- // Extract to a temp directory first to avoid overwriting the wrapper script
314
- const tempExtractDir = path.join(__dirname, '.extract-temp');
315
- if (fs.existsSync(tempExtractDir)) {
316
- fs.rmSync(tempExtractDir, { recursive: true, force: true });
317
- }
318
- fs.mkdirSync(tempExtractDir, { recursive: true });
319
-
320
- await extractArchive(archivePath, tempExtractDir, os);
321
-
322
- // The archive contains 'ai-rulez' or 'ai-rulez.exe', but we need to rename it
323
- // to avoid conflict with the wrapper script
324
- const extractedName = os === 'windows' ? 'ai-rulez.exe' : 'ai-rulez';
325
- const extractedPath = path.join(tempExtractDir, extractedName);
326
- const binaryPath = path.join(binDir, binaryName);
327
-
328
- // Move the extracted binary to the final location
329
- if (fs.existsSync(extractedPath)) {
330
- // Remove old binary if it exists
331
- if (fs.existsSync(binaryPath)) {
332
- fs.unlinkSync(binaryPath);
333
- }
334
- // Move binary from temp to final location
335
- fs.renameSync(extractedPath, binaryPath);
336
- }
337
-
338
- // Clean up temp directory
339
- fs.rmSync(tempExtractDir, { recursive: true, force: true });
340
-
341
- if (!fs.existsSync(binaryPath)) {
342
- throw new Error(`Binary not found after extraction: ${binaryPath}`);
343
- }
344
-
345
-
346
- if (os !== 'windows') {
347
- fs.chmodSync(binaryPath, 0o755);
348
- }
349
-
350
- // Verify binary is executable
351
- try {
352
- await new Promise((resolve, reject) => {
353
- const testCommand = os === 'windows' ? [binaryPath, '--version'] : [binaryPath, '--version'];
354
- const child = spawn(testCommand[0], testCommand.slice(1), {
355
- stdio: ['pipe', 'pipe', 'pipe'],
356
- timeout: 5000
357
- });
358
-
359
- child.on('close', (code) => {
360
- // Any exit code is fine, we just want to verify it can execute
361
- resolve();
362
- });
363
-
364
- child.on('error', (err) => {
365
- if (err.code === 'ENOENT') {
366
- reject(new Error('Downloaded binary is not executable'));
367
- } else {
368
- resolve(); // Other errors are OK for version check
369
- }
370
- });
371
- });
372
- } catch (verifyError) {
373
- console.warn('Warning: Could not verify binary execution:', verifyError.message);
374
- }
375
-
376
-
377
- fs.unlinkSync(archivePath);
378
-
379
- console.log(`✅ ai-rulez ${version} installed successfully for ${os}/${arch}!`);
380
-
381
- if (DEBUG) {
382
- console.error(`[install.js] Installation complete`);
383
- console.error(`[install.js] Binary location: ${binaryPath}`);
384
- console.error(`[install.js] Exiting with code 0`);
385
- }
386
-
387
- // Explicitly exit with success code
388
- process.exit(0);
389
-
390
- } catch (error) {
391
- if (DEBUG) console.error(`[install.js] Installation failed: ${error.message}`);
392
- console.error('Failed to install ai-rulez binary:', error.message);
393
- console.error('You can manually download the binary from:');
394
- console.error(`https://github.com/${REPO_NAME}/releases`);
395
- process.exit(1);
396
- }
258
+ let stderr = "";
259
+ child.stderr.on("data", (data) => {
260
+ stderr += data.toString();
261
+ });
262
+
263
+ child.on("close", (code) => {
264
+ if (code === 0) {
265
+ resolve();
266
+ } else {
267
+ reject(
268
+ new Error(`tar extraction failed with code ${code}: ${stderr}`),
269
+ );
270
+ }
271
+ });
272
+
273
+ child.on("error", reject);
274
+ });
275
+ }
397
276
  }
398
277
 
278
+ async function install(isPostInstall = false) {
279
+ const DEBUG = process.env.AI_RULEZ_DEBUG === "1";
280
+
281
+ try {
282
+ if (DEBUG) console.error("[install.js] Starting installation");
283
+
284
+ const nodeVersion = process.version;
285
+ const majorVersion = parseInt(nodeVersion.slice(1).split(".")[0], 10);
286
+ if (majorVersion < 20) {
287
+ console.error(
288
+ `Error: Node.js ${nodeVersion} is not supported. Please upgrade to Node.js 20 or later.`,
289
+ );
290
+ process.exit(1);
291
+ }
292
+
293
+ const { os, arch } = getPlatform();
294
+ const binaryName = getBinaryName(os);
295
+
296
+ if (DEBUG)
297
+ console.error(
298
+ `[install.js] Platform: ${os}/${arch}, Binary name: ${binaryName}`,
299
+ );
300
+
301
+ const packageJson = JSON.parse(
302
+ fs.readFileSync(path.join(__dirname, "package.json"), "utf8"),
303
+ );
304
+ const version = packageJson.version;
305
+
306
+ const archiveExt = os === "windows" ? "zip" : "tar.gz";
307
+ const archiveName = `ai-rulez_${version}_${os}_${arch}.${archiveExt}`;
308
+ const downloadUrl = `https://github.com/${REPO_NAME}/releases/download/v${version}/${archiveName}`;
309
+ const checksumUrl = `https://github.com/${REPO_NAME}/releases/download/v${version}/checksums.txt`;
310
+
311
+ console.log(`Downloading ai-rulez ${version} for ${os}/${arch}...`);
312
+ console.log(`URL: ${downloadUrl}`);
313
+
314
+ const binDir = path.join(__dirname, "bin");
315
+ if (!fs.existsSync(binDir)) {
316
+ fs.mkdirSync(binDir, { recursive: true });
317
+ }
318
+
319
+ const archivePath = path.join(__dirname, archiveName);
320
+
321
+ console.log("Downloading checksums...");
322
+ const checksumPath = path.join(__dirname, "checksums.txt");
323
+ try {
324
+ await downloadBinary(checksumUrl, checksumPath);
325
+ } catch (_checksumError) {
326
+ console.warn(
327
+ "Warning: Could not download checksums, skipping verification",
328
+ );
329
+ }
330
+
331
+ await downloadBinary(downloadUrl, archivePath);
332
+
333
+ if (fs.existsSync(checksumPath)) {
334
+ console.log("Verifying checksum...");
335
+ const expectedHash = await getExpectedChecksum(checksumPath, archiveName);
336
+ if (expectedHash) {
337
+ const actualHash = await calculateSHA256(archivePath);
338
+ if (actualHash !== expectedHash) {
339
+ throw new Error(
340
+ `Checksum verification failed. Expected: ${expectedHash}, Got: ${actualHash}`,
341
+ );
342
+ }
343
+ console.log("✓ Checksum verified");
344
+ }
345
+ fs.unlinkSync(checksumPath);
346
+ }
347
+
348
+ console.log("Extracting binary...");
349
+
350
+ const tempExtractDir = path.join(__dirname, ".extract-temp");
351
+ if (fs.existsSync(tempExtractDir)) {
352
+ fs.rmSync(tempExtractDir, { recursive: true, force: true });
353
+ }
354
+ fs.mkdirSync(tempExtractDir, { recursive: true });
399
355
 
400
- // Export functions for testing
401
- if (typeof module !== 'undefined' && module.exports) {
402
- module.exports = {
403
- getPlatform,
404
- getBinaryName,
405
- downloadBinary,
406
- extractArchive,
407
- calculateSHA256,
408
- getExpectedChecksum,
409
- install
410
- };
356
+ await extractArchive(archivePath, tempExtractDir, os);
357
+
358
+ const extractedName = os === "windows" ? "ai-rulez.exe" : "ai-rulez";
359
+ const extractedPath = path.join(tempExtractDir, extractedName);
360
+ const binaryPath = path.join(binDir, binaryName);
361
+
362
+ if (fs.existsSync(extractedPath)) {
363
+ if (fs.existsSync(binaryPath)) {
364
+ fs.unlinkSync(binaryPath);
365
+ }
366
+ fs.renameSync(extractedPath, binaryPath);
367
+ }
368
+
369
+ fs.rmSync(tempExtractDir, { recursive: true, force: true });
370
+
371
+ if (!fs.existsSync(binaryPath)) {
372
+ throw new Error(`Binary not found after extraction: ${binaryPath}`);
373
+ }
374
+
375
+ if (os !== "windows") {
376
+ fs.chmodSync(binaryPath, 0o755);
377
+ }
378
+
379
+ try {
380
+ await new Promise((resolve, reject) => {
381
+ const testCommand =
382
+ os === "windows"
383
+ ? [binaryPath, "--version"]
384
+ : [binaryPath, "--version"];
385
+ const child = spawn(testCommand[0], testCommand.slice(1), {
386
+ stdio: ["pipe", "pipe", "pipe"],
387
+ timeout: 5000,
388
+ });
389
+
390
+ child.on("close", (_code) => {
391
+ resolve();
392
+ });
393
+
394
+ child.on("error", (err) => {
395
+ if (err.code === "ENOENT") {
396
+ reject(new Error("Downloaded binary is not executable"));
397
+ } else {
398
+ resolve();
399
+ }
400
+ });
401
+ });
402
+ } catch (verifyError) {
403
+ console.warn(
404
+ "Warning: Could not verify binary execution:",
405
+ verifyError.message,
406
+ );
407
+ }
408
+
409
+ fs.unlinkSync(archivePath);
410
+
411
+ console.log(
412
+ `✅ ai-rulez ${version} installed successfully for ${os}/${arch}!`,
413
+ );
414
+
415
+ if (DEBUG) {
416
+ console.error(`[install.js] Installation complete`);
417
+ console.error(`[install.js] Binary location: ${binaryPath}`);
418
+ console.error(`[install.js] Exiting with code 0`);
419
+ }
420
+
421
+ if (!isPostInstall) {
422
+ process.exit(0);
423
+ }
424
+ } catch (error) {
425
+ if (DEBUG)
426
+ console.error(`[install.js] Installation failed: ${error.message}`);
427
+ console.error("Failed to install ai-rulez binary:", error.message);
428
+ console.error("You can manually download the binary from:");
429
+ console.error(`https://github.com/${REPO_NAME}/releases`);
430
+ if (!isPostInstall) {
431
+ process.exit(1);
432
+ } else {
433
+ throw error;
434
+ }
435
+ }
436
+ }
437
+
438
+ if (typeof module !== "undefined" && module.exports) {
439
+ module.exports = {
440
+ getPlatform,
441
+ getBinaryName,
442
+ downloadBinary,
443
+ extractArchive,
444
+ calculateSHA256,
445
+ getExpectedChecksum,
446
+ install,
447
+ };
411
448
  }
412
449
 
413
450
  if (require.main === module) {
414
- install();
415
- }
451
+ install(false);
452
+ }