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/README.md +110 -205
- package/bin/ai-rulez-bin +0 -0
- package/bin/ai-rulez.js +39 -0
- package/install.js +427 -390
- package/package.json +33 -20
package/install.js
CHANGED
|
@@ -1,415 +1,452 @@
|
|
|
1
|
-
const fs = require(
|
|
2
|
-
const path = require(
|
|
3
|
-
const https = require(
|
|
4
|
-
const http = require(
|
|
5
|
-
const crypto = require(
|
|
6
|
-
const { exec, spawn } = require(
|
|
7
|
-
const { promisify } = require(
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const REPO_NAME =
|
|
12
|
-
const DOWNLOAD_TIMEOUT = 30000;
|
|
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;
|
|
14
|
+
const RETRY_DELAY = 2000;
|
|
15
15
|
|
|
16
16
|
async function calculateSHA256(filePath) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
415
|
-
}
|
|
451
|
+
install(false);
|
|
452
|
+
}
|