ai-rulez 1.6.0 → 2.0.0

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.
Files changed (4) hide show
  1. package/bin/ai-rulez.js +66 -0
  2. package/install.js +420 -389
  3. package/package.json +85 -72
  4. package/README.md +0 -261
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const { spawn } = require("node:child_process");
6
+
7
+ // Import the install logic from install.js
8
+ const installPath = path.join(__dirname, "..", "install.js");
9
+ const installer = require(installPath);
10
+
11
+ const BINARY_NAME = installer.getBinaryName();
12
+ const BINARY_PATH = path.join(__dirname, BINARY_NAME);
13
+
14
+ async function ensureBinary() {
15
+ if (fs.existsSync(BINARY_PATH)) {
16
+ return true;
17
+ }
18
+
19
+ console.log("🚀 First run detected - downloading ai-rulez binary...");
20
+ console.log(" This will only happen once and takes a few seconds.");
21
+ console.log("");
22
+ try {
23
+ await installer.install();
24
+ if (fs.existsSync(BINARY_PATH)) {
25
+ console.log("✅ Download complete! Running ai-rulez...");
26
+ console.log("");
27
+ return true;
28
+ } else {
29
+ throw new Error("Binary not found after installation");
30
+ }
31
+ } catch (error) {
32
+ console.error("❌ Failed to download ai-rulez binary:", error.message);
33
+ process.exit(1);
34
+ }
35
+ }
36
+
37
+ async function main() {
38
+ const hasBinary = await ensureBinary();
39
+
40
+ if (!hasBinary) {
41
+ console.error("Could not find or download ai-rulez binary");
42
+ process.exit(1);
43
+ }
44
+
45
+ // Forward all arguments to the native binary
46
+ const child = spawn(BINARY_PATH, process.argv.slice(2), {
47
+ stdio: "inherit",
48
+ cwd: process.cwd(),
49
+ });
50
+
51
+ child.on("exit", (code) => {
52
+ process.exit(code || 0);
53
+ });
54
+
55
+ child.on("error", (error) => {
56
+ console.error("Failed to start ai-rulez:", error.message);
57
+ process.exit(1);
58
+ });
59
+ }
60
+
61
+ if (require.main === module) {
62
+ main().catch((error) => {
63
+ console.error("Error:", error.message);
64
+ process.exit(1);
65
+ });
66
+ }
package/install.js CHANGED
@@ -1,415 +1,446 @@
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
- }
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
+ });
257
+
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
+ }
243
276
  }
244
277
 
245
278
  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
- }
397
- }
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...");
398
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
+ process.exit(0);
422
+ } catch (error) {
423
+ if (DEBUG)
424
+ console.error(`[install.js] Installation failed: ${error.message}`);
425
+ console.error("Failed to install ai-rulez binary:", error.message);
426
+ console.error("You can manually download the binary from:");
427
+ console.error(`https://github.com/${REPO_NAME}/releases`);
428
+ process.exit(1);
429
+ }
430
+ }
431
+
432
+ if (typeof module !== "undefined" && module.exports) {
433
+ module.exports = {
434
+ getPlatform,
435
+ getBinaryName,
436
+ downloadBinary,
437
+ extractArchive,
438
+ calculateSHA256,
439
+ getExpectedChecksum,
440
+ install,
441
+ };
411
442
  }
412
443
 
413
444
  if (require.main === module) {
414
- install();
415
- }
445
+ install();
446
+ }
package/package.json CHANGED
@@ -1,73 +1,86 @@
1
1
  {
2
- "name": "ai-rulez",
3
- "version": "1.6.0",
4
- "description": "⚡ Lightning-fast CLI tool (written in Go) for managing AI assistant rules - generate configuration files for Claude, Cursor, Windsurf and more",
5
- "keywords": [
6
- "ai",
7
- "ai-assistant",
8
- "ai-rules",
9
- "claude",
10
- "cursor",
11
- "windsurf",
12
- "codeium",
13
- "copilot",
14
- "cli",
15
- "cli-tool",
16
- "configuration",
17
- "config",
18
- "rules",
19
- "generator",
20
- "golang",
21
- "go",
22
- "fast",
23
- "development",
24
- "developer-tools",
25
- "automation",
26
- "workflow",
27
- "productivity",
28
- "pre-commit",
29
- "git-hooks",
30
- "lefthook",
31
- "code-generation",
32
- "ai-development",
33
- "assistant-configuration"
34
- ],
35
- "repository": {
36
- "type": "git",
37
- "url": "https://github.com/Goldziher/ai-rulez.git"
38
- },
39
- "homepage": "https://github.com/Goldziher/ai-rulez#readme",
40
- "bugs": {
41
- "url": "https://github.com/Goldziher/ai-rulez/issues"
42
- },
43
- "license": "MIT",
44
- "author": {
45
- "name": "Na'aman Hirschfeld",
46
- "email": "nhirschfeld@gmail.com",
47
- "url": "https://github.com/Goldziher"
48
- },
49
- "bin": {
50
- "ai-rulez": "./bin/ai-rulez"
51
- },
52
- "scripts": {
53
- "postinstall": "node install.js"
54
- },
55
- "files": [
56
- "bin",
57
- "install.js",
58
- "README.md"
59
- ],
60
- "engines": {
61
- "node": ">=14.0.0"
62
- },
63
- "os": [
64
- "darwin",
65
- "linux",
66
- "win32"
67
- ],
68
- "cpu": [
69
- "x64",
70
- "arm64",
71
- "ia32"
72
- ]
73
- }
2
+ "name": "ai-rulez",
3
+ "version": "2.0.0",
4
+ "description": "⚡ One config to rule them all. Centralized AI assistant configuration management - generate rules for Claude, Cursor, Copilot, Windsurf and more from a single YAML file.",
5
+ "keywords": [
6
+ "ai",
7
+ "ai-assistant",
8
+ "ai-rules",
9
+ "claude",
10
+ "cursor",
11
+ "copilot",
12
+ "windsurf",
13
+ "gemini",
14
+ "cline",
15
+ "continue-dev",
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "cli",
19
+ "configuration",
20
+ "config",
21
+ "rules",
22
+ "generator",
23
+ "golang",
24
+ "go",
25
+ "development",
26
+ "developer-tools",
27
+ "automation",
28
+ "workflow",
29
+ "productivity",
30
+ "pre-commit",
31
+ "git-hooks",
32
+ "code-generation",
33
+ "ai-development",
34
+ "assistant-configuration",
35
+ "monorepo",
36
+ "presets",
37
+ "agents"
38
+ ],
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/Goldziher/ai-rulez.git"
42
+ },
43
+ "homepage": "https://goldziher.github.io/ai-rulez/",
44
+ "bugs": {
45
+ "url": "https://github.com/Goldziher/ai-rulez/issues"
46
+ },
47
+ "funding": {
48
+ "type": "github",
49
+ "url": "https://github.com/sponsors/Goldziher"
50
+ },
51
+ "license": "MIT",
52
+ "author": {
53
+ "name": "Na'aman Hirschfeld",
54
+ "email": "nhirschfeld@gmail.com",
55
+ "url": "https://github.com/Goldziher"
56
+ },
57
+ "maintainers": [
58
+ {
59
+ "name": "Na'aman Hirschfeld",
60
+ "email": "nhirschfeld@gmail.com",
61
+ "url": "https://github.com/Goldziher"
62
+ }
63
+ ],
64
+ "bin": {
65
+ "ai-rulez": "./bin/ai-rulez.js"
66
+ },
67
+ "scripts": {},
68
+ "files": [
69
+ "bin/",
70
+ "install.js",
71
+ "../../README.md"
72
+ ],
73
+ "engines": {
74
+ "node": ">=14.0.0"
75
+ },
76
+ "os": [
77
+ "darwin",
78
+ "linux",
79
+ "win32"
80
+ ],
81
+ "cpu": [
82
+ "x64",
83
+ "arm64",
84
+ "ia32"
85
+ ]
86
+ }
package/README.md DELETED
@@ -1,261 +0,0 @@
1
- # ai-rulez ⚡
2
-
3
- > **Lightning-fast CLI tool (written in Go) for managing AI assistant rules**
4
-
5
- Generate configuration files for Claude, Cursor, Windsurf, and other AI assistants from a single, centralized configuration.
6
-
7
- ## 🚀 Features
8
-
9
- - ⚡ **Blazing Fast**: Written in Go for maximum performance
10
- - 🔧 **Multi-Assistant Support**: Generate configs for Claude (CLAUDE.md), Cursor (.cursorrules), Windsurf (.windsurfrules), and more
11
- - 📝 **Single Source of Truth**: Maintain all your AI rules in one YAML configuration
12
- - 🎯 **Smart Templates**: Built-in templates with custom template support
13
- - 🔍 **Validation**: Comprehensive configuration validation
14
- - 🔄 **Git Integration**: Perfect for pre-commit hooks and CI/CD
15
- - 📦 **Node.js Integration**: Easy installation via npm
16
-
17
- ## 📦 Installation
18
-
19
- ### npm (Recommended)
20
-
21
- ```bash
22
- # Global installation
23
- npm install -g ai-rulez
24
-
25
- # Local project installation
26
- npm install --save-dev ai-rulez
27
- ```
28
-
29
- The npm package automatically downloads and manages the Go binary for your platform.
30
-
31
- ### Other Installation Methods
32
-
33
- - **pip**: `pip install ai-rulez`
34
- - **Go**: `go install github.com/Goldziher/ai-rulez@latest`
35
- - **Homebrew**: `brew install goldziher/tap/ai-rulez` *(coming soon)*
36
- - **Direct Download**: Download from [GitHub Releases](https://github.com/Goldziher/ai-rulez/releases)
37
-
38
- ## 🎯 Quick Start
39
-
40
- 1. **Create a configuration file** (`ai-rulez.yaml`):
41
-
42
- ```yaml
43
- metadata:
44
- name: "My AI Rules"
45
- version: "1.0.0"
46
-
47
- rules:
48
- - name: "Code Style"
49
- priority: 10
50
- content: |
51
- - Use TypeScript strict mode
52
- - Prefer functional components
53
- - Use meaningful variable names
54
-
55
- - name: "Testing"
56
- priority: 5
57
- content: |
58
- - Write unit tests for all functions
59
- - Use describe/it pattern
60
- - Aim for 80% code coverage
61
-
62
- outputs:
63
- - file: "CLAUDE.md"
64
- template: "claude"
65
- - file: ".cursorrules"
66
- template: "cursor"
67
- - file: ".windsurfrules"
68
- template: "windsurf"
69
- ```
70
-
71
- 2. **Generate configuration files**:
72
-
73
- ```bash
74
- ai-rulez generate
75
- ```
76
-
77
- This creates `CLAUDE.md`, `.cursorrules`, and `.windsurfrules` with your rules properly formatted for each AI assistant.
78
-
79
- ## 🛠️ Commands
80
-
81
- ```bash
82
- # Generate all configuration files
83
- ai-rulez generate
84
-
85
- # Validate configuration
86
- ai-rulez validate
87
-
88
- # Generate recursively in subdirectories
89
- ai-rulez generate --recursive
90
-
91
- # Preview output without writing files
92
- ai-rulez generate --dry-run
93
-
94
- # Show help
95
- ai-rulez --help
96
- ```
97
-
98
- ## 🔄 Git Integration
99
-
100
- ### Pre-commit Hook
101
-
102
- Add to your `.pre-commit-config.yaml`:
103
-
104
- ```yaml
105
- repos:
106
- - repo: https://github.com/Goldziher/ai-rulez
107
- rev: v1.0.0
108
- hooks:
109
- - id: ai-rulez-generate
110
- ```
111
-
112
- ### Lefthook
113
-
114
- Add to your `lefthook.yml`:
115
-
116
- ```yaml
117
- pre-commit:
118
- commands:
119
- ai-rulez:
120
- run: ai-rulez generate
121
- files: git diff --cached --name-only
122
- glob: "*.{ai-rulez,ai_rulez}.{yml,yaml}"
123
- ```
124
-
125
- ### npm Scripts
126
-
127
- Add to your `package.json`:
128
-
129
- ```json
130
- {
131
- "scripts": {
132
- "ai-rulez": "ai-rulez generate",
133
- "ai-rulez:validate": "ai-rulez validate",
134
- "ai-rulez:watch": "ai-rulez generate --recursive"
135
- }
136
- }
137
- ```
138
-
139
- ## 📚 Configuration
140
-
141
- The tool looks for configuration files in this order:
142
- - `.ai-rulez.yaml`
143
- - `ai-rulez.yaml`
144
- - `.ai_rulez.yaml`
145
- - `ai_rulez.yaml`
146
-
147
- ### User Rules vs. Coding Rules
148
-
149
- When creating AI rules, distinguish between two types of instructions:
150
-
151
- - **Coding Rules**: Technical guidelines about code quality, architecture, testing, etc.
152
- - Examples: "Use TypeScript strict mode", "Write unit tests", "Follow REST conventions"
153
- - Should be in the main configuration file committed to version control
154
-
155
- - **User Rules**: Personal preferences about communication style and interaction
156
- - Examples: "Be concise in responses", "Use casual tone", "Address me as 'Chief'", "Always explain your reasoning"
157
- - Perfect for `.local.yaml` files (e.g., `ai-rulez.local.yaml`) as they're personal and shouldn't affect the whole team
158
- - Allow individual developers to customize AI behavior without impacting others
159
-
160
- **Example local config** (`ai-rulez.local.yaml`):
161
- ```yaml
162
- rules:
163
- - name: "Communication Style"
164
- content: "Be concise and direct. Address me as 'Boss'. Always ask for clarification before making assumptions."
165
- - name: "Response Format"
166
- content: "Provide code examples for every suggestion. Use bullet points for lists."
167
- ```
168
-
169
- ### Configuration Schema
170
-
171
- ```yaml
172
- metadata:
173
- name: string # Required: Project name
174
- version: string # Required: Version
175
- description: string # Optional: Description
176
-
177
- rules:
178
- - name: string # Required: Rule name
179
- priority: number # Required: Priority (1-10)
180
- content: string # Required: Rule content
181
-
182
- sections: # Optional: Organize rules into sections
183
- - title: string # Required: Section title
184
- priority: number # Required: Section priority
185
- content: string # Required: Section content
186
-
187
- outputs: # Required: At least one output
188
- - file: string # Required: Output filename
189
- template: string # Required: Template name or path
190
-
191
- includes: # Optional: Include other config files
192
- - path/to/other.yaml
193
- ```
194
-
195
- ## 🎨 Templates
196
-
197
- Built-in templates:
198
- - `claude` - CLAUDE.md format
199
- - `cursor` - .cursorrules format
200
- - `windsurf` - .windsurfrules format
201
- - `default` - Generic format
202
-
203
- Custom templates use Go template syntax with access to `.Rules`, `.Sections`, `.Metadata`, etc.
204
-
205
- ## 🔧 Advanced Usage
206
-
207
- ### Environment Variables
208
-
209
- - `AI_RULEZ_CONFIG` - Override config file path
210
- - `AI_RULEZ_DEBUG` - Enable debug output
211
-
212
- ### Node.js API
213
-
214
- ```javascript
215
- const { execSync } = require('child_process');
216
-
217
- // Run ai-rulez programmatically
218
- try {
219
- const output = execSync('ai-rulez generate --dry-run', { encoding: 'utf8' });
220
- console.log(output);
221
- } catch (error) {
222
- console.error('ai-rulez failed:', error.message);
223
- }
224
- ```
225
-
226
- ### npm Scripts Integration
227
-
228
- ```json
229
- {
230
- "scripts": {
231
- "precommit": "ai-rulez generate",
232
- "lint": "eslint . && ai-rulez validate",
233
- "build": "npm run ai-rulez && npm run compile"
234
- },
235
- "husky": {
236
- "hooks": {
237
- "pre-commit": "ai-rulez generate"
238
- }
239
- }
240
- }
241
- ```
242
-
243
- ## 🤝 Contributing
244
-
245
- Contributions are welcome! Please see our [Contributing Guide](https://github.com/Goldziher/ai-rulez/blob/main/CONTRIBUTING.md).
246
-
247
- ## 📄 License
248
-
249
- MIT License - see [LICENSE](https://github.com/Goldziher/ai-rulez/blob/main/LICENSE)
250
-
251
- ## 🔗 Links
252
-
253
- - [GitHub Repository](https://github.com/Goldziher/ai-rulez)
254
- - [Documentation](https://github.com/Goldziher/ai-rulez#readme)
255
- - [Issues](https://github.com/Goldziher/ai-rulez/issues)
256
- - [Releases](https://github.com/Goldziher/ai-rulez/releases)
257
- - [PyPI Package](https://pypi.org/project/ai-rulez/)
258
-
259
- ---
260
-
261
- **Note**: This npm package is a wrapper around the Go binary. The actual tool is written in Go for maximum performance and cross-platform compatibility.