ai-rulez 1.0.0-rc9 → 1.1.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/README.md +206 -28
- package/install.js +271 -37
- package/package.json +51 -8
package/README.md
CHANGED
|
@@ -1,61 +1,239 @@
|
|
|
1
|
-
# ai-rulez
|
|
1
|
+
# ai-rulez ⚡
|
|
2
2
|
|
|
3
|
-
CLI tool for managing AI assistant rules
|
|
3
|
+
> **Lightning-fast CLI tool (written in Go) for managing AI assistant rules**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Generate configuration files for Claude, Cursor, Windsurf, and other AI assistants from a single, centralized configuration.
|
|
6
6
|
|
|
7
|
-
##
|
|
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)
|
|
8
20
|
|
|
9
21
|
```bash
|
|
22
|
+
# Global installation
|
|
10
23
|
npm install -g ai-rulez
|
|
24
|
+
|
|
25
|
+
# Local project installation
|
|
26
|
+
npm install --save-dev ai-rulez
|
|
11
27
|
```
|
|
12
28
|
|
|
13
|
-
The package
|
|
29
|
+
The npm package automatically downloads and manages the Go binary for your platform.
|
|
14
30
|
|
|
15
|
-
|
|
31
|
+
### Other Installation Methods
|
|
16
32
|
|
|
17
|
-
|
|
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`):
|
|
18
41
|
|
|
19
42
|
```yaml
|
|
20
43
|
metadata:
|
|
21
|
-
name:
|
|
22
|
-
version: 1.0.0
|
|
44
|
+
name: "My AI Rules"
|
|
45
|
+
version: "1.0.0"
|
|
23
46
|
|
|
24
47
|
rules:
|
|
25
|
-
- name:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
29
61
|
|
|
30
62
|
outputs:
|
|
31
|
-
- file: .
|
|
32
|
-
|
|
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
|
|
33
75
|
```
|
|
34
76
|
|
|
35
|
-
|
|
77
|
+
This creates `CLAUDE.md`, `.cursorrules`, and `.windsurfrules` with your rules properly formatted for each AI assistant.
|
|
78
|
+
|
|
79
|
+
## 🛠️ Commands
|
|
36
80
|
|
|
37
81
|
```bash
|
|
38
|
-
# Generate files
|
|
82
|
+
# Generate all configuration files
|
|
39
83
|
ai-rulez generate
|
|
40
84
|
|
|
41
85
|
# Validate configuration
|
|
42
86
|
ai-rulez validate
|
|
43
87
|
|
|
44
|
-
#
|
|
45
|
-
ai-rulez
|
|
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
|
|
46
96
|
```
|
|
47
97
|
|
|
48
|
-
##
|
|
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
|
+
### Configuration Schema
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
metadata:
|
|
151
|
+
name: string # Required: Project name
|
|
152
|
+
version: string # Required: Version
|
|
153
|
+
description: string # Optional: Description
|
|
154
|
+
|
|
155
|
+
rules:
|
|
156
|
+
- name: string # Required: Rule name
|
|
157
|
+
priority: number # Required: Priority (1-10)
|
|
158
|
+
content: string # Required: Rule content
|
|
159
|
+
|
|
160
|
+
sections: # Optional: Organize rules into sections
|
|
161
|
+
- title: string # Required: Section title
|
|
162
|
+
priority: number # Required: Section priority
|
|
163
|
+
content: string # Required: Section content
|
|
164
|
+
|
|
165
|
+
outputs: # Required: At least one output
|
|
166
|
+
- file: string # Required: Output filename
|
|
167
|
+
template: string # Required: Template name or path
|
|
168
|
+
|
|
169
|
+
includes: # Optional: Include other config files
|
|
170
|
+
- path/to/other.yaml
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## 🎨 Templates
|
|
174
|
+
|
|
175
|
+
Built-in templates:
|
|
176
|
+
- `claude` - CLAUDE.md format
|
|
177
|
+
- `cursor` - .cursorrules format
|
|
178
|
+
- `windsurf` - .windsurfrules format
|
|
179
|
+
- `default` - Generic format
|
|
180
|
+
|
|
181
|
+
Custom templates use Go template syntax with access to `.Rules`, `.Sections`, `.Metadata`, etc.
|
|
182
|
+
|
|
183
|
+
## 🔧 Advanced Usage
|
|
184
|
+
|
|
185
|
+
### Environment Variables
|
|
186
|
+
|
|
187
|
+
- `AI_RULEZ_CONFIG` - Override config file path
|
|
188
|
+
- `AI_RULEZ_DEBUG` - Enable debug output
|
|
189
|
+
|
|
190
|
+
### Node.js API
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
const { execSync } = require('child_process');
|
|
194
|
+
|
|
195
|
+
// Run ai-rulez programmatically
|
|
196
|
+
try {
|
|
197
|
+
const output = execSync('ai-rulez generate --dry-run', { encoding: 'utf8' });
|
|
198
|
+
console.log(output);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error('ai-rulez failed:', error.message);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### npm Scripts Integration
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"scripts": {
|
|
209
|
+
"precommit": "ai-rulez generate",
|
|
210
|
+
"lint": "eslint . && ai-rulez validate",
|
|
211
|
+
"build": "npm run ai-rulez && npm run compile"
|
|
212
|
+
},
|
|
213
|
+
"husky": {
|
|
214
|
+
"hooks": {
|
|
215
|
+
"pre-commit": "ai-rulez generate"
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## 🤝 Contributing
|
|
222
|
+
|
|
223
|
+
Contributions are welcome! Please see our [Contributing Guide](https://github.com/Goldziher/ai-rulez/blob/main/CONTRIBUTING.md).
|
|
224
|
+
|
|
225
|
+
## 📄 License
|
|
49
226
|
|
|
50
|
-
|
|
51
|
-
- macOS (Intel and Apple Silicon)
|
|
52
|
-
- Linux (x64, ARM64, and x86)
|
|
53
|
-
- Windows (x64 and x86)
|
|
227
|
+
MIT License - see [LICENSE](https://github.com/Goldziher/ai-rulez/blob/main/LICENSE)
|
|
54
228
|
|
|
55
|
-
##
|
|
229
|
+
## 🔗 Links
|
|
56
230
|
|
|
57
|
-
|
|
231
|
+
- [GitHub Repository](https://github.com/Goldziher/ai-rulez)
|
|
232
|
+
- [Documentation](https://github.com/Goldziher/ai-rulez#readme)
|
|
233
|
+
- [Issues](https://github.com/Goldziher/ai-rulez/issues)
|
|
234
|
+
- [Releases](https://github.com/Goldziher/ai-rulez/releases)
|
|
235
|
+
- [PyPI Package](https://pypi.org/project/ai-rulez/)
|
|
58
236
|
|
|
59
|
-
|
|
237
|
+
---
|
|
60
238
|
|
|
61
|
-
|
|
239
|
+
**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.
|
package/install.js
CHANGED
|
@@ -1,12 +1,46 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const https = require('https');
|
|
4
|
-
const
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { exec, spawn } = require('child_process');
|
|
5
7
|
const { promisify } = require('util');
|
|
6
8
|
|
|
7
9
|
const execAsync = promisify(exec);
|
|
8
10
|
|
|
9
11
|
const REPO_NAME = 'Goldziher/ai-rulez';
|
|
12
|
+
const DOWNLOAD_TIMEOUT = 30000; // 30 seconds
|
|
13
|
+
const MAX_RETRIES = 3;
|
|
14
|
+
const RETRY_DELAY = 2000; // 2 seconds
|
|
15
|
+
|
|
16
|
+
async function calculateSHA256(filePath) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const hash = crypto.createHash('sha256');
|
|
19
|
+
const stream = fs.createReadStream(filePath);
|
|
20
|
+
|
|
21
|
+
stream.on('data', (data) => hash.update(data));
|
|
22
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
23
|
+
stream.on('error', reject);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function getExpectedChecksum(checksumPath, filename) {
|
|
28
|
+
try {
|
|
29
|
+
const checksumContent = fs.readFileSync(checksumPath, 'utf8');
|
|
30
|
+
const lines = checksumContent.split('\n');
|
|
31
|
+
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const parts = line.trim().split(/\s+/);
|
|
34
|
+
if (parts.length >= 2 && parts[1] === filename) {
|
|
35
|
+
return parts[0];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.warn('Warning: Could not parse checksums file:', error.message);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
10
44
|
|
|
11
45
|
function getPlatform() {
|
|
12
46
|
const platform = process.platform;
|
|
@@ -28,13 +62,17 @@ function getPlatform() {
|
|
|
28
62
|
const mappedPlatform = platformMap[platform];
|
|
29
63
|
const mappedArch = archMap[arch];
|
|
30
64
|
|
|
31
|
-
if (!mappedPlatform
|
|
32
|
-
throw new Error(`Unsupported
|
|
65
|
+
if (!mappedPlatform) {
|
|
66
|
+
throw new Error(`Unsupported operating system: ${platform}. Supported platforms: darwin (macOS), linux, win32 (Windows)`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!mappedArch) {
|
|
70
|
+
throw new Error(`Unsupported architecture: ${arch}. Supported architectures: x64, arm64, ia32`);
|
|
33
71
|
}
|
|
34
72
|
|
|
35
|
-
|
|
73
|
+
|
|
36
74
|
if (mappedPlatform === 'windows' && mappedArch === 'arm64') {
|
|
37
|
-
throw new Error('Windows ARM64 is not supported');
|
|
75
|
+
throw new Error('Windows ARM64 is not currently supported. Please use x64 or ia32 version.');
|
|
38
76
|
}
|
|
39
77
|
|
|
40
78
|
return {
|
|
@@ -47,81 +85,264 @@ function getBinaryName(platform) {
|
|
|
47
85
|
return platform === 'windows' ? 'ai-rulez.exe' : 'ai-rulez';
|
|
48
86
|
}
|
|
49
87
|
|
|
50
|
-
async function downloadBinary(url, dest) {
|
|
88
|
+
async function downloadBinary(url, dest, retryCount = 0) {
|
|
51
89
|
return new Promise((resolve, reject) => {
|
|
52
90
|
const file = fs.createWriteStream(dest);
|
|
91
|
+
const protocol = url.startsWith('https') ? https : http;
|
|
53
92
|
|
|
54
|
-
|
|
93
|
+
const request = protocol.get(url, { timeout: DOWNLOAD_TIMEOUT }, (response) => {
|
|
55
94
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
95
|
+
file.close();
|
|
96
|
+
try { fs.unlinkSync(dest); } catch {} // Clean up partial file
|
|
97
|
+
downloadBinary(response.headers.location, dest, retryCount)
|
|
98
|
+
.then(resolve)
|
|
99
|
+
.catch(reject);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (response.statusCode !== 200) {
|
|
104
|
+
file.close();
|
|
105
|
+
try { fs.unlinkSync(dest); } catch {} // Clean up partial file
|
|
106
|
+
const error = new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`);
|
|
107
|
+
|
|
108
|
+
if (retryCount < MAX_RETRIES) {
|
|
109
|
+
console.log(`Download failed, retrying in ${RETRY_DELAY/1000}s... (${retryCount + 1}/${MAX_RETRIES})`);
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
downloadBinary(url, dest, retryCount + 1)
|
|
112
|
+
.then(resolve)
|
|
113
|
+
.catch(reject);
|
|
114
|
+
}, RETRY_DELAY);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
reject(error);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let downloadedBytes = 0;
|
|
123
|
+
response.on('data', (chunk) => {
|
|
124
|
+
downloadedBytes += chunk.length;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
response.pipe(file);
|
|
128
|
+
|
|
129
|
+
file.on('finish', () => {
|
|
130
|
+
file.close();
|
|
131
|
+
if (downloadedBytes === 0) {
|
|
132
|
+
try { fs.unlinkSync(dest); } catch {}
|
|
133
|
+
reject(new Error('Downloaded file is empty'));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
console.log(`Downloaded ${downloadedBytes} bytes`);
|
|
137
|
+
resolve();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
file.on('error', (err) => {
|
|
141
|
+
file.close();
|
|
142
|
+
try { fs.unlinkSync(dest); } catch {}
|
|
143
|
+
reject(err);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
request.on('timeout', () => {
|
|
148
|
+
request.destroy();
|
|
149
|
+
file.close();
|
|
150
|
+
try { fs.unlinkSync(dest); } catch {}
|
|
151
|
+
|
|
152
|
+
if (retryCount < MAX_RETRIES) {
|
|
153
|
+
console.log(`Download timeout, retrying in ${RETRY_DELAY/1000}s... (${retryCount + 1}/${MAX_RETRIES})`);
|
|
154
|
+
setTimeout(() => {
|
|
155
|
+
downloadBinary(url, dest, retryCount + 1)
|
|
156
|
+
.then(resolve)
|
|
157
|
+
.catch(reject);
|
|
158
|
+
}, RETRY_DELAY);
|
|
159
|
+
return;
|
|
72
160
|
}
|
|
73
|
-
|
|
161
|
+
|
|
162
|
+
reject(new Error('Download timeout after multiple retries'));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
request.on('error', (err) => {
|
|
166
|
+
file.close();
|
|
167
|
+
try { fs.unlinkSync(dest); } catch {}
|
|
168
|
+
|
|
169
|
+
if (retryCount < MAX_RETRIES) {
|
|
170
|
+
console.log(`Download error, retrying in ${RETRY_DELAY/1000}s... (${retryCount + 1}/${MAX_RETRIES})`);
|
|
171
|
+
setTimeout(() => {
|
|
172
|
+
downloadBinary(url, dest, retryCount + 1)
|
|
173
|
+
.then(resolve)
|
|
174
|
+
.catch(reject);
|
|
175
|
+
}, RETRY_DELAY);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
reject(err);
|
|
180
|
+
});
|
|
74
181
|
});
|
|
75
182
|
}
|
|
76
183
|
|
|
77
184
|
async function extractArchive(archivePath, extractDir, platform) {
|
|
78
185
|
if (platform === 'windows') {
|
|
79
|
-
// Use PowerShell
|
|
80
|
-
|
|
186
|
+
// Use safer PowerShell execution with proper escaping
|
|
187
|
+
const escapedArchivePath = archivePath.replace(/'/g, "''");
|
|
188
|
+
const escapedExtractDir = extractDir.replace(/'/g, "''");
|
|
189
|
+
|
|
190
|
+
const powershellCommand = [
|
|
191
|
+
'powershell.exe',
|
|
192
|
+
'-NoProfile',
|
|
193
|
+
'-ExecutionPolicy', 'Bypass',
|
|
194
|
+
'-Command',
|
|
195
|
+
`Expand-Archive -LiteralPath '${escapedArchivePath}' -DestinationPath '${escapedExtractDir}' -Force`
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
await new Promise((resolve, reject) => {
|
|
199
|
+
const child = spawn(powershellCommand[0], powershellCommand.slice(1), {
|
|
200
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
201
|
+
windowsHide: true
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
let stderr = '';
|
|
205
|
+
child.stderr.on('data', (data) => {
|
|
206
|
+
stderr += data.toString();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
child.on('close', (code) => {
|
|
210
|
+
if (code === 0) {
|
|
211
|
+
resolve();
|
|
212
|
+
} else {
|
|
213
|
+
reject(new Error(`PowerShell extraction failed with code ${code}: ${stderr}`));
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
child.on('error', reject);
|
|
218
|
+
});
|
|
81
219
|
} else {
|
|
82
|
-
// Use
|
|
83
|
-
await
|
|
220
|
+
// Use spawn instead of exec for better security and error handling
|
|
221
|
+
await new Promise((resolve, reject) => {
|
|
222
|
+
const child = spawn('tar', ['-xzf', archivePath, '-C', extractDir], {
|
|
223
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
let stderr = '';
|
|
227
|
+
child.stderr.on('data', (data) => {
|
|
228
|
+
stderr += data.toString();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
child.on('close', (code) => {
|
|
232
|
+
if (code === 0) {
|
|
233
|
+
resolve();
|
|
234
|
+
} else {
|
|
235
|
+
reject(new Error(`tar extraction failed with code ${code}: ${stderr}`));
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
child.on('error', reject);
|
|
240
|
+
});
|
|
84
241
|
}
|
|
85
242
|
}
|
|
86
243
|
|
|
87
244
|
async function install() {
|
|
88
245
|
try {
|
|
246
|
+
// Check Node.js version compatibility
|
|
247
|
+
const nodeVersion = process.version;
|
|
248
|
+
const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]);
|
|
249
|
+
if (majorVersion < 20) {
|
|
250
|
+
console.error(`Error: Node.js ${nodeVersion} is not supported. Please upgrade to Node.js 20 or later.`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
|
|
89
254
|
const { os, arch } = getPlatform();
|
|
90
255
|
const binaryName = getBinaryName(os);
|
|
91
256
|
|
|
92
|
-
|
|
257
|
+
|
|
93
258
|
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
|
|
94
259
|
const version = packageJson.version;
|
|
95
260
|
|
|
96
|
-
|
|
261
|
+
|
|
97
262
|
const archiveExt = os === 'windows' ? 'zip' : 'tar.gz';
|
|
98
263
|
const archiveName = `ai-rulez_${version}_${os}_${arch}.${archiveExt}`;
|
|
99
264
|
const downloadUrl = `https://github.com/${REPO_NAME}/releases/download/v${version}/${archiveName}`;
|
|
265
|
+
const checksumUrl = `https://github.com/${REPO_NAME}/releases/download/v${version}/checksums.txt`;
|
|
100
266
|
|
|
101
267
|
console.log(`Downloading ai-rulez ${version} for ${os}/${arch}...`);
|
|
102
268
|
console.log(`URL: ${downloadUrl}`);
|
|
103
269
|
|
|
104
|
-
|
|
270
|
+
|
|
105
271
|
const binDir = path.join(__dirname, 'bin');
|
|
106
272
|
if (!fs.existsSync(binDir)) {
|
|
107
273
|
fs.mkdirSync(binDir, { recursive: true });
|
|
108
274
|
}
|
|
109
275
|
|
|
110
|
-
|
|
276
|
+
|
|
111
277
|
const archivePath = path.join(__dirname, archiveName);
|
|
278
|
+
|
|
279
|
+
// Download checksums first for verification
|
|
280
|
+
console.log('Downloading checksums...');
|
|
281
|
+
const checksumPath = path.join(__dirname, 'checksums.txt');
|
|
282
|
+
try {
|
|
283
|
+
await downloadBinary(checksumUrl, checksumPath);
|
|
284
|
+
} catch (checksumError) {
|
|
285
|
+
console.warn('Warning: Could not download checksums, skipping verification');
|
|
286
|
+
}
|
|
287
|
+
|
|
112
288
|
await downloadBinary(downloadUrl, archivePath);
|
|
113
289
|
|
|
114
|
-
//
|
|
290
|
+
// Verify checksum if available
|
|
291
|
+
if (fs.existsSync(checksumPath)) {
|
|
292
|
+
console.log('Verifying checksum...');
|
|
293
|
+
const expectedHash = await getExpectedChecksum(checksumPath, archiveName);
|
|
294
|
+
if (expectedHash) {
|
|
295
|
+
const actualHash = await calculateSHA256(archivePath);
|
|
296
|
+
if (actualHash !== expectedHash) {
|
|
297
|
+
throw new Error(`Checksum verification failed. Expected: ${expectedHash}, Got: ${actualHash}`);
|
|
298
|
+
}
|
|
299
|
+
console.log('✓ Checksum verified');
|
|
300
|
+
}
|
|
301
|
+
fs.unlinkSync(checksumPath);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
115
305
|
console.log('Extracting binary...');
|
|
116
306
|
await extractArchive(archivePath, binDir, os);
|
|
117
307
|
|
|
118
|
-
|
|
308
|
+
|
|
309
|
+
const binaryPath = path.join(binDir, binaryName);
|
|
310
|
+
if (!fs.existsSync(binaryPath)) {
|
|
311
|
+
throw new Error(`Binary not found after extraction: ${binaryPath}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
119
315
|
if (os !== 'windows') {
|
|
120
|
-
const binaryPath = path.join(binDir, binaryName);
|
|
121
316
|
fs.chmodSync(binaryPath, 0o755);
|
|
122
317
|
}
|
|
123
318
|
|
|
124
|
-
//
|
|
319
|
+
// Verify binary is executable
|
|
320
|
+
try {
|
|
321
|
+
await new Promise((resolve, reject) => {
|
|
322
|
+
const testCommand = os === 'windows' ? [binaryPath, '--version'] : [binaryPath, '--version'];
|
|
323
|
+
const child = spawn(testCommand[0], testCommand.slice(1), {
|
|
324
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
325
|
+
timeout: 5000
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
child.on('close', (code) => {
|
|
329
|
+
// Any exit code is fine, we just want to verify it can execute
|
|
330
|
+
resolve();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
child.on('error', (err) => {
|
|
334
|
+
if (err.code === 'ENOENT') {
|
|
335
|
+
reject(new Error('Downloaded binary is not executable'));
|
|
336
|
+
} else {
|
|
337
|
+
resolve(); // Other errors are OK for version check
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
} catch (verifyError) {
|
|
342
|
+
console.warn('Warning: Could not verify binary execution:', verifyError.message);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
|
|
125
346
|
fs.unlinkSync(archivePath);
|
|
126
347
|
|
|
127
348
|
console.log(`✅ ai-rulez ${version} installed successfully for ${os}/${arch}!`);
|
|
@@ -134,7 +355,20 @@ async function install() {
|
|
|
134
355
|
}
|
|
135
356
|
}
|
|
136
357
|
|
|
137
|
-
|
|
358
|
+
|
|
359
|
+
// Export functions for testing
|
|
360
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
361
|
+
module.exports = {
|
|
362
|
+
getPlatform,
|
|
363
|
+
getBinaryName,
|
|
364
|
+
downloadBinary,
|
|
365
|
+
extractArchive,
|
|
366
|
+
calculateSHA256,
|
|
367
|
+
getExpectedChecksum,
|
|
368
|
+
install
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
138
372
|
if (require.main === module) {
|
|
139
373
|
install();
|
|
140
374
|
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-rulez",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI tool for managing AI assistant rules - generate configuration files for Claude, Cursor, Windsurf and more",
|
|
5
|
-
"keywords": [
|
|
3
|
+
"version": "1.1.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
|
+
],
|
|
6
35
|
"repository": {
|
|
7
36
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/Goldziher/ai-rulez"
|
|
37
|
+
"url": "https://github.com/Goldziher/ai-rulez.git"
|
|
9
38
|
},
|
|
10
|
-
"homepage": "https://github.com/Goldziher/ai-rulez",
|
|
39
|
+
"homepage": "https://github.com/Goldziher/ai-rulez#readme",
|
|
11
40
|
"bugs": {
|
|
12
41
|
"url": "https://github.com/Goldziher/ai-rulez/issues"
|
|
13
42
|
},
|
|
14
43
|
"license": "MIT",
|
|
15
|
-
"author":
|
|
44
|
+
"author": {
|
|
45
|
+
"name": "Na'aman Hirschfeld",
|
|
46
|
+
"email": "nhirschfeld@gmail.com",
|
|
47
|
+
"url": "https://github.com/Goldziher"
|
|
48
|
+
},
|
|
16
49
|
"bin": {
|
|
17
50
|
"ai-rulez": "./bin/ai-rulez"
|
|
18
51
|
},
|
|
@@ -21,10 +54,20 @@
|
|
|
21
54
|
},
|
|
22
55
|
"files": [
|
|
23
56
|
"bin",
|
|
24
|
-
"install.js",
|
|
57
|
+
"install.js",
|
|
25
58
|
"README.md"
|
|
26
59
|
],
|
|
27
60
|
"engines": {
|
|
28
61
|
"node": ">=14.0.0"
|
|
29
|
-
}
|
|
62
|
+
},
|
|
63
|
+
"os": [
|
|
64
|
+
"darwin",
|
|
65
|
+
"linux",
|
|
66
|
+
"win32"
|
|
67
|
+
],
|
|
68
|
+
"cpu": [
|
|
69
|
+
"x64",
|
|
70
|
+
"arm64",
|
|
71
|
+
"ia32"
|
|
72
|
+
]
|
|
30
73
|
}
|