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.
- package/bin/ai-rulez.js +66 -0
- package/install.js +420 -389
- package/package.json +85 -72
- package/README.md +0 -261
package/bin/ai-rulez.js
ADDED
|
@@ -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(
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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
|
+
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
|
-
|
|
415
|
-
}
|
|
445
|
+
install();
|
|
446
|
+
}
|
package/package.json
CHANGED
|
@@ -1,73 +1,86 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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.
|