autoworkflow 1.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/README.md +206 -0
- package/bin/cli.js +299 -0
- package/lib/index.js +9 -0
- package/lib/install.js +251 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# autoworkflow
|
|
2
|
+
|
|
3
|
+
Zero-config code quality enforcement. Just install and commit.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/autoworkflow)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install autoworkflow --save-dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**That's it.** Enforcement is now active on all commits.
|
|
15
|
+
|
|
16
|
+
## What Happens
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
npm install autoworkflow
|
|
20
|
+
│
|
|
21
|
+
▼
|
|
22
|
+
┌──────────────────────────────────┐
|
|
23
|
+
│ Auto-setup (postinstall): │
|
|
24
|
+
│ ✓ Creates .husky/pre-commit │
|
|
25
|
+
│ ✓ Creates .husky/commit-msg │
|
|
26
|
+
│ ✓ Creates enforce.config.json │
|
|
27
|
+
│ ✓ Adds prepare script │
|
|
28
|
+
└──────────────────────────────────┘
|
|
29
|
+
│
|
|
30
|
+
▼
|
|
31
|
+
Every "git commit" runs checks automatically
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Default Checks
|
|
35
|
+
|
|
36
|
+
| Check | Blocking | Auto-Fix |
|
|
37
|
+
|-------|----------|----------|
|
|
38
|
+
| No TODO/FIXME comments | ✅ | - |
|
|
39
|
+
| No console.log | ✅ | - |
|
|
40
|
+
| TypeScript errors | ✅ | - |
|
|
41
|
+
| ESLint errors | ✅ | ✅ |
|
|
42
|
+
| Circular dependencies | ✅ | - |
|
|
43
|
+
| Commit message format | ✅ | - |
|
|
44
|
+
|
|
45
|
+
## How It Works
|
|
46
|
+
|
|
47
|
+
### Pre-Commit Hook
|
|
48
|
+
Every time you run `git commit`, autoworkflow:
|
|
49
|
+
1. Runs all enabled checks
|
|
50
|
+
2. If ESLint fails, auto-fixes and retries (up to 5 attempts)
|
|
51
|
+
3. Blocks commit if any blocking check fails
|
|
52
|
+
4. Allows commit if all checks pass
|
|
53
|
+
|
|
54
|
+
### Commit Message Validation
|
|
55
|
+
Enforces [Conventional Commits](https://www.conventionalcommits.org/):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# ✅ Valid
|
|
59
|
+
git commit -m "feat: add user authentication"
|
|
60
|
+
git commit -m "fix(api): resolve timeout issue"
|
|
61
|
+
git commit -m "docs: update installation guide"
|
|
62
|
+
|
|
63
|
+
# ❌ Invalid (blocked)
|
|
64
|
+
git commit -m "fixed stuff"
|
|
65
|
+
git commit -m "WIP"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
Edit `enforce.config.json` in your project root:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"rules": {
|
|
75
|
+
"no-todo-comments": {
|
|
76
|
+
"enabled": true,
|
|
77
|
+
"blocking": true,
|
|
78
|
+
"patterns": ["TODO", "FIXME", "XXX", "HACK"]
|
|
79
|
+
},
|
|
80
|
+
"no-console-logs": {
|
|
81
|
+
"enabled": true,
|
|
82
|
+
"blocking": true
|
|
83
|
+
},
|
|
84
|
+
"typescript": {
|
|
85
|
+
"enabled": true,
|
|
86
|
+
"blocking": true,
|
|
87
|
+
"command": "npx tsc --noEmit"
|
|
88
|
+
},
|
|
89
|
+
"eslint": {
|
|
90
|
+
"enabled": true,
|
|
91
|
+
"blocking": true,
|
|
92
|
+
"autoFix": true,
|
|
93
|
+
"command": "npx eslint . --max-warnings 0",
|
|
94
|
+
"fixCommand": "npx eslint . --fix"
|
|
95
|
+
},
|
|
96
|
+
"circular-deps": {
|
|
97
|
+
"enabled": true,
|
|
98
|
+
"blocking": true,
|
|
99
|
+
"paths": ["src/"]
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"fixLoop": {
|
|
103
|
+
"enabled": true,
|
|
104
|
+
"maxAttempts": 5,
|
|
105
|
+
"autoFixRules": ["eslint"]
|
|
106
|
+
},
|
|
107
|
+
"commitMessage": {
|
|
108
|
+
"enabled": true,
|
|
109
|
+
"pattern": "^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\\(.+\\))?: .{1,72}$"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Rule Options
|
|
115
|
+
|
|
116
|
+
| Option | Type | Description |
|
|
117
|
+
|--------|------|-------------|
|
|
118
|
+
| `enabled` | boolean | Enable/disable the rule |
|
|
119
|
+
| `blocking` | boolean | If true, fails the commit on violation |
|
|
120
|
+
| `autoFix` | boolean | Enable auto-fix for this rule |
|
|
121
|
+
| `command` | string | Custom check command |
|
|
122
|
+
| `fixCommand` | string | Custom fix command |
|
|
123
|
+
|
|
124
|
+
### Disable a Rule
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"rules": {
|
|
129
|
+
"no-console-logs": { "enabled": false }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Non-Blocking Warning
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"rules": {
|
|
139
|
+
"circular-deps": { "enabled": true, "blocking": false }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## CLI Commands
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Run all checks manually
|
|
148
|
+
npx autoworkflow run
|
|
149
|
+
|
|
150
|
+
# List all rules and their status
|
|
151
|
+
npx autoworkflow list
|
|
152
|
+
|
|
153
|
+
# Validate a commit message
|
|
154
|
+
npx autoworkflow commit-msg .git/COMMIT_EDITMSG
|
|
155
|
+
|
|
156
|
+
# Show help
|
|
157
|
+
npx autoworkflow --help
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Fix Loop
|
|
161
|
+
|
|
162
|
+
When ESLint (or other auto-fixable rules) fail:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
[1/5] ESLint... ❌
|
|
166
|
+
src/app.ts: 'unused' is defined but never used
|
|
167
|
+
|
|
168
|
+
🔧 Attempting auto-fix...
|
|
169
|
+
✅ Fixed: ESLint
|
|
170
|
+
|
|
171
|
+
🔄 Fix Loop - Attempt 2/5
|
|
172
|
+
|
|
173
|
+
[1/5] ESLint... ✅
|
|
174
|
+
|
|
175
|
+
✅ ALL CHECKS PASSED
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Skip Enforcement (Emergency)
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
git commit --no-verify -m "emergency: hotfix for production"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Use sparingly. All skipped commits are your responsibility.
|
|
185
|
+
|
|
186
|
+
## Requirements
|
|
187
|
+
|
|
188
|
+
- Node.js >= 18.0.0
|
|
189
|
+
- Git repository (initialized with `git init`)
|
|
190
|
+
|
|
191
|
+
### Optional Peer Dependencies
|
|
192
|
+
|
|
193
|
+
- TypeScript >= 5.0.0 (for TypeScript checks)
|
|
194
|
+
- ESLint >= 8.0.0 (for ESLint checks)
|
|
195
|
+
|
|
196
|
+
## Uninstall
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
npm uninstall autoworkflow
|
|
200
|
+
rm -rf .husky
|
|
201
|
+
rm enforce.config.json
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* autoworkflow - CLI
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx autoworkflow run Run all checks (with fix loop)
|
|
7
|
+
* npx autoworkflow list List all rules
|
|
8
|
+
* npx autoworkflow check <id> Run specific rule
|
|
9
|
+
* npx autoworkflow commit-msg Validate commit message
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
// Colors
|
|
17
|
+
const c = {
|
|
18
|
+
reset: '\x1b[0m',
|
|
19
|
+
red: '\x1b[31m',
|
|
20
|
+
green: '\x1b[32m',
|
|
21
|
+
yellow: '\x1b[33m',
|
|
22
|
+
blue: '\x1b[34m',
|
|
23
|
+
cyan: '\x1b[36m',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const log = (color, msg) => console.log(`${color}${msg}${c.reset}`);
|
|
27
|
+
|
|
28
|
+
// Find config file
|
|
29
|
+
function findConfig() {
|
|
30
|
+
const configPath = path.join(process.cwd(), 'enforce.config.json');
|
|
31
|
+
if (fs.existsSync(configPath)) {
|
|
32
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
33
|
+
}
|
|
34
|
+
// Return defaults
|
|
35
|
+
return {
|
|
36
|
+
rules: {
|
|
37
|
+
'no-todo-comments': { enabled: true, blocking: true },
|
|
38
|
+
'no-console-logs': { enabled: true, blocking: true },
|
|
39
|
+
'typescript': { enabled: true, blocking: true },
|
|
40
|
+
'eslint': { enabled: true, blocking: true, autoFix: true },
|
|
41
|
+
'circular-deps': { enabled: true, blocking: true },
|
|
42
|
+
},
|
|
43
|
+
fixLoop: { enabled: true, maxAttempts: 5, autoFixRules: ['eslint'] },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Rule implementations
|
|
48
|
+
const rules = {
|
|
49
|
+
'no-todo-comments': {
|
|
50
|
+
name: 'No TODO/FIXME Comments',
|
|
51
|
+
check: (config) => {
|
|
52
|
+
try {
|
|
53
|
+
const patterns = config.patterns || ['TODO', 'FIXME', 'XXX', 'HACK'];
|
|
54
|
+
const output = execSync(
|
|
55
|
+
`grep -rn "${patterns.join('\\|')}" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" . 2>/dev/null | grep -v node_modules | grep -v ".git" || true`,
|
|
56
|
+
{ encoding: 'utf-8' }
|
|
57
|
+
).trim();
|
|
58
|
+
const matches = output ? output.split('\n').filter(Boolean) : [];
|
|
59
|
+
return { passed: matches.length === 0, details: matches.slice(0, 5) };
|
|
60
|
+
} catch {
|
|
61
|
+
return { passed: true, details: [] };
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
'no-console-logs': {
|
|
67
|
+
name: 'No Console Logs',
|
|
68
|
+
check: (config) => {
|
|
69
|
+
try {
|
|
70
|
+
const output = execSync(
|
|
71
|
+
'grep -rn "console\\.\\(log\\|debug\\|info\\)" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" . 2>/dev/null | grep -v node_modules | grep -v ".git" | grep -v "eslint-disable" || true',
|
|
72
|
+
{ encoding: 'utf-8' }
|
|
73
|
+
).trim();
|
|
74
|
+
const matches = output ? output.split('\n').filter(Boolean) : [];
|
|
75
|
+
return { passed: matches.length === 0, details: matches.slice(0, 5) };
|
|
76
|
+
} catch {
|
|
77
|
+
return { passed: true, details: [] };
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
'typescript': {
|
|
83
|
+
name: 'TypeScript',
|
|
84
|
+
check: (config) => {
|
|
85
|
+
try {
|
|
86
|
+
const cmd = config.command || 'npx tsc --noEmit';
|
|
87
|
+
execSync(cmd, { encoding: 'utf-8', stdio: 'pipe' });
|
|
88
|
+
return { passed: true, details: [] };
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const output = error.stdout || error.message || '';
|
|
91
|
+
const errors = output.split('\n').filter(l => l.includes('error')).slice(0, 5);
|
|
92
|
+
return { passed: false, details: errors };
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
'eslint': {
|
|
98
|
+
name: 'ESLint',
|
|
99
|
+
check: (config) => {
|
|
100
|
+
try {
|
|
101
|
+
const cmd = config.command || 'npx eslint . --max-warnings 0';
|
|
102
|
+
execSync(cmd, { encoding: 'utf-8', stdio: 'pipe' });
|
|
103
|
+
return { passed: true, details: [] };
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const output = error.stdout || error.message || '';
|
|
106
|
+
const errors = output.split('\n').filter(l => l.includes('error') || l.includes('warning')).slice(0, 5);
|
|
107
|
+
return { passed: false, details: errors };
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
fix: (config) => {
|
|
111
|
+
try {
|
|
112
|
+
const cmd = config.fixCommand || 'npx eslint . --fix';
|
|
113
|
+
execSync(cmd, { encoding: 'utf-8', stdio: 'pipe' });
|
|
114
|
+
return true;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
'circular-deps': {
|
|
122
|
+
name: 'Circular Dependencies',
|
|
123
|
+
check: (config) => {
|
|
124
|
+
try {
|
|
125
|
+
const paths = config.paths || ['src/'];
|
|
126
|
+
const output = execSync(
|
|
127
|
+
`npx madge --circular --extensions ts,tsx,js,jsx ${paths.join(' ')} 2>&1`,
|
|
128
|
+
{ encoding: 'utf-8' }
|
|
129
|
+
);
|
|
130
|
+
const hasCircular = output.includes('Found');
|
|
131
|
+
return { passed: !hasCircular, details: hasCircular ? output.split('\n').slice(0, 5) : [] };
|
|
132
|
+
} catch {
|
|
133
|
+
return { passed: true, details: [] };
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Run all checks with fix loop
|
|
140
|
+
function runChecks(config) {
|
|
141
|
+
const fixLoop = config.fixLoop || { enabled: true, maxAttempts: 5 };
|
|
142
|
+
let attempt = 0;
|
|
143
|
+
let results = [];
|
|
144
|
+
const fixedRules = [];
|
|
145
|
+
|
|
146
|
+
log(c.blue, '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
147
|
+
log(c.blue, '🔒 autoworkflow');
|
|
148
|
+
log(c.blue, '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
149
|
+
console.log('');
|
|
150
|
+
|
|
151
|
+
while (attempt < fixLoop.maxAttempts) {
|
|
152
|
+
attempt++;
|
|
153
|
+
|
|
154
|
+
if (attempt > 1) {
|
|
155
|
+
log(c.cyan, `\n🔄 Fix Loop - Attempt ${attempt}/${fixLoop.maxAttempts}\n`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
results = [];
|
|
159
|
+
let index = 0;
|
|
160
|
+
const ruleIds = Object.keys(config.rules).filter(id => config.rules[id]?.enabled);
|
|
161
|
+
|
|
162
|
+
for (const ruleId of ruleIds) {
|
|
163
|
+
const rule = rules[ruleId];
|
|
164
|
+
const ruleConfig = config.rules[ruleId];
|
|
165
|
+
if (!rule) continue;
|
|
166
|
+
|
|
167
|
+
index++;
|
|
168
|
+
const result = rule.check(ruleConfig);
|
|
169
|
+
const status = result.passed ? c.green + '✅' : (ruleConfig.blocking ? c.red + '❌' : c.yellow + '⚠️');
|
|
170
|
+
|
|
171
|
+
console.log(`${c.yellow}[${index}/${ruleIds.length}]${c.reset} ${rule.name}... ${status}${c.reset}`);
|
|
172
|
+
|
|
173
|
+
if (!result.passed && result.details.length > 0) {
|
|
174
|
+
result.details.forEach(d => console.log(` ${d}`));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
results.push({ ruleId, ...result, blocking: ruleConfig.blocking, fixable: !!rule.fix && ruleConfig.autoFix });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check if we need to fix
|
|
181
|
+
const failedFixable = results.filter(
|
|
182
|
+
r => !r.passed && r.fixable && (fixLoop.autoFixRules || []).includes(r.ruleId)
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (failedFixable.length === 0) break;
|
|
186
|
+
|
|
187
|
+
// Attempt fixes
|
|
188
|
+
log(c.cyan, '\n🔧 Attempting auto-fix...');
|
|
189
|
+
for (const result of failedFixable) {
|
|
190
|
+
const rule = rules[result.ruleId];
|
|
191
|
+
const ruleConfig = config.rules[result.ruleId];
|
|
192
|
+
if (rule?.fix?.(ruleConfig)) {
|
|
193
|
+
log(c.green, ` ✅ Fixed: ${rule.name}`);
|
|
194
|
+
if (!fixedRules.includes(result.ruleId)) fixedRules.push(result.ruleId);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Summary
|
|
200
|
+
const failed = results.filter(r => !r.passed && r.blocking).length;
|
|
201
|
+
|
|
202
|
+
console.log('');
|
|
203
|
+
log(c.blue, '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
204
|
+
|
|
205
|
+
if (failed > 0) {
|
|
206
|
+
log(c.red, '⛔ COMMIT BLOCKED');
|
|
207
|
+
log(c.red, ` ${failed} check(s) failed. Fix issues and try again.`);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
} else {
|
|
210
|
+
log(c.green, '✅ ALL CHECKS PASSED');
|
|
211
|
+
if (fixedRules.length > 0) {
|
|
212
|
+
log(c.green, ` Auto-fixed: ${fixedRules.join(', ')}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
console.log('');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Validate commit message
|
|
219
|
+
function validateCommitMessage(msgFile) {
|
|
220
|
+
const config = findConfig();
|
|
221
|
+
const commitConfig = config.commitMessage || {};
|
|
222
|
+
|
|
223
|
+
if (!commitConfig.enabled) return;
|
|
224
|
+
|
|
225
|
+
const message = fs.readFileSync(msgFile, 'utf-8').trim().split('\n')[0];
|
|
226
|
+
const pattern = commitConfig.pattern || '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\\(.+\\))?: .{1,72}$';
|
|
227
|
+
|
|
228
|
+
if (!new RegExp(pattern).test(message)) {
|
|
229
|
+
log(c.red, '');
|
|
230
|
+
log(c.red, '⛔ Invalid commit message format!');
|
|
231
|
+
log(c.red, '');
|
|
232
|
+
log(c.yellow, 'Expected: type(scope): description');
|
|
233
|
+
log(c.yellow, 'Examples:');
|
|
234
|
+
console.log(' feat: add user authentication');
|
|
235
|
+
console.log(' fix(auth): resolve login bug');
|
|
236
|
+
console.log(' docs: update readme');
|
|
237
|
+
log(c.red, '');
|
|
238
|
+
log(c.red, `Your message: "${message}"`);
|
|
239
|
+
log(c.red, '');
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// List rules
|
|
245
|
+
function listRules(config) {
|
|
246
|
+
log(c.blue, 'Available rules:');
|
|
247
|
+
console.log('');
|
|
248
|
+
|
|
249
|
+
for (const [id, ruleConfig] of Object.entries(config.rules)) {
|
|
250
|
+
const rule = rules[id];
|
|
251
|
+
const status = ruleConfig.enabled ? c.green + '✅' : c.red + '❌';
|
|
252
|
+
const blocking = ruleConfig.blocking ? '🔒' : '⚠️';
|
|
253
|
+
const name = rule?.name || id;
|
|
254
|
+
console.log(` ${status}${c.reset} ${blocking} ${name} (${id})`);
|
|
255
|
+
}
|
|
256
|
+
console.log('');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Main
|
|
260
|
+
function main() {
|
|
261
|
+
const args = process.argv.slice(2);
|
|
262
|
+
const command = args[0] || 'run';
|
|
263
|
+
const config = findConfig();
|
|
264
|
+
|
|
265
|
+
switch (command) {
|
|
266
|
+
case 'run':
|
|
267
|
+
runChecks(config);
|
|
268
|
+
break;
|
|
269
|
+
|
|
270
|
+
case 'commit-msg':
|
|
271
|
+
const msgFile = args[1];
|
|
272
|
+
if (msgFile) validateCommitMessage(msgFile);
|
|
273
|
+
break;
|
|
274
|
+
|
|
275
|
+
case 'list':
|
|
276
|
+
listRules(config);
|
|
277
|
+
break;
|
|
278
|
+
|
|
279
|
+
case '--help':
|
|
280
|
+
case '-h':
|
|
281
|
+
console.log('');
|
|
282
|
+
log(c.blue, 'autoworkflow');
|
|
283
|
+
console.log('');
|
|
284
|
+
console.log('Usage:');
|
|
285
|
+
console.log(' npx autoworkflow run Run all checks');
|
|
286
|
+
console.log(' npx autoworkflow list List all rules');
|
|
287
|
+
console.log(' npx autoworkflow commit-msg Validate commit message');
|
|
288
|
+
console.log(' npx autoworkflow --help Show this help');
|
|
289
|
+
console.log('');
|
|
290
|
+
console.log('Configuration: enforce.config.json');
|
|
291
|
+
console.log('');
|
|
292
|
+
break;
|
|
293
|
+
|
|
294
|
+
default:
|
|
295
|
+
runChecks(config);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
main();
|
package/lib/index.js
ADDED
package/lib/install.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* autoworkflow - Auto-Install Script
|
|
4
|
+
*
|
|
5
|
+
* This runs automatically when the package is installed.
|
|
6
|
+
* Sets up Git hooks and creates default config.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
// Colors for output
|
|
14
|
+
const c = {
|
|
15
|
+
reset: '\x1b[0m',
|
|
16
|
+
green: '\x1b[32m',
|
|
17
|
+
yellow: '\x1b[33m',
|
|
18
|
+
blue: '\x1b[34m',
|
|
19
|
+
red: '\x1b[31m',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function log(color, message) {
|
|
23
|
+
console.log(`${color}${message}${c.reset}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function findProjectRoot() {
|
|
27
|
+
// Start from where npm install was run (not node_modules)
|
|
28
|
+
let dir = process.env.INIT_CWD || process.cwd();
|
|
29
|
+
|
|
30
|
+
// Walk up to find package.json (project root)
|
|
31
|
+
while (dir !== path.dirname(dir)) {
|
|
32
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
33
|
+
// Make sure it's not our own package.json
|
|
34
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
|
|
35
|
+
if (pkg.name !== 'autoworkflow') {
|
|
36
|
+
return dir;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
dir = path.dirname(dir);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return process.env.INIT_CWD || process.cwd();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isGitRepo(dir) {
|
|
46
|
+
return fs.existsSync(path.join(dir, '.git'));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ensureHuskyDir(projectRoot) {
|
|
50
|
+
const huskyDir = path.join(projectRoot, '.husky');
|
|
51
|
+
if (!fs.existsSync(huskyDir)) {
|
|
52
|
+
fs.mkdirSync(huskyDir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Create husky internal dir
|
|
56
|
+
const huskyInternalDir = path.join(huskyDir, '_');
|
|
57
|
+
if (!fs.existsSync(huskyInternalDir)) {
|
|
58
|
+
fs.mkdirSync(huskyInternalDir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return huskyDir;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function createPreCommitHook(huskyDir, projectRoot) {
|
|
65
|
+
const hookPath = path.join(huskyDir, 'pre-commit');
|
|
66
|
+
|
|
67
|
+
// Check if hook already exists
|
|
68
|
+
if (fs.existsSync(hookPath)) {
|
|
69
|
+
log(c.yellow, ' ⚠ Pre-commit hook already exists, skipping...');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const hookContent = `#!/bin/bash
|
|
74
|
+
# ============================================
|
|
75
|
+
# autoworkflow - Pre-Commit Hook
|
|
76
|
+
# ============================================
|
|
77
|
+
# Auto-generated. Modify enforce.config.json to customize.
|
|
78
|
+
# ============================================
|
|
79
|
+
|
|
80
|
+
npx autoworkflow run
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
fs.writeFileSync(hookPath, hookContent);
|
|
84
|
+
fs.chmodSync(hookPath, '755');
|
|
85
|
+
log(c.green, ' ✓ Created pre-commit hook');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createCommitMsgHook(huskyDir) {
|
|
89
|
+
const hookPath = path.join(huskyDir, 'commit-msg');
|
|
90
|
+
|
|
91
|
+
if (fs.existsSync(hookPath)) {
|
|
92
|
+
log(c.yellow, ' ⚠ Commit-msg hook already exists, skipping...');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const hookContent = `#!/bin/bash
|
|
97
|
+
# ============================================
|
|
98
|
+
# autoworkflow - Commit Message Hook
|
|
99
|
+
# ============================================
|
|
100
|
+
|
|
101
|
+
npx autoworkflow commit-msg "$1"
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
fs.writeFileSync(hookPath, hookContent);
|
|
105
|
+
fs.chmodSync(hookPath, '755');
|
|
106
|
+
log(c.green, ' ✓ Created commit-msg hook');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createDefaultConfig(projectRoot) {
|
|
110
|
+
const configPath = path.join(projectRoot, 'enforce.config.json');
|
|
111
|
+
|
|
112
|
+
if (fs.existsSync(configPath)) {
|
|
113
|
+
log(c.yellow, ' ⚠ Config file already exists, skipping...');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const defaultConfig = {
|
|
118
|
+
version: '1.0.0',
|
|
119
|
+
rules: {
|
|
120
|
+
'no-todo-comments': {
|
|
121
|
+
enabled: true,
|
|
122
|
+
blocking: true,
|
|
123
|
+
patterns: ['TODO', 'FIXME', 'XXX', 'HACK']
|
|
124
|
+
},
|
|
125
|
+
'no-console-logs': {
|
|
126
|
+
enabled: true,
|
|
127
|
+
blocking: true,
|
|
128
|
+
allow: ['warn', 'error']
|
|
129
|
+
},
|
|
130
|
+
'typescript': {
|
|
131
|
+
enabled: true,
|
|
132
|
+
blocking: true,
|
|
133
|
+
command: 'npx tsc --noEmit'
|
|
134
|
+
},
|
|
135
|
+
'eslint': {
|
|
136
|
+
enabled: true,
|
|
137
|
+
blocking: true,
|
|
138
|
+
autoFix: true,
|
|
139
|
+
command: 'npx eslint . --max-warnings 0',
|
|
140
|
+
fixCommand: 'npx eslint . --fix'
|
|
141
|
+
},
|
|
142
|
+
'circular-deps': {
|
|
143
|
+
enabled: true,
|
|
144
|
+
blocking: true,
|
|
145
|
+
paths: ['src/']
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
fixLoop: {
|
|
149
|
+
enabled: true,
|
|
150
|
+
maxAttempts: 5,
|
|
151
|
+
autoFixRules: ['eslint']
|
|
152
|
+
},
|
|
153
|
+
commitMessage: {
|
|
154
|
+
enabled: true,
|
|
155
|
+
pattern: '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\\(.+\\))?: .{1,72}$'
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
|
|
160
|
+
log(c.green, ' ✓ Created enforce.config.json');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function initializeHusky(projectRoot) {
|
|
164
|
+
try {
|
|
165
|
+
// Initialize husky
|
|
166
|
+
execSync('npx husky', { cwd: projectRoot, stdio: 'ignore' });
|
|
167
|
+
log(c.green, ' ✓ Initialized husky');
|
|
168
|
+
} catch (error) {
|
|
169
|
+
// Husky might already be initialized, that's okay
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function updatePackageJson(projectRoot) {
|
|
174
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
175
|
+
|
|
176
|
+
if (!fs.existsSync(pkgPath)) {
|
|
177
|
+
log(c.yellow, ' ⚠ No package.json found');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
182
|
+
|
|
183
|
+
// Add prepare script for husky if not exists
|
|
184
|
+
if (!pkg.scripts) {
|
|
185
|
+
pkg.scripts = {};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!pkg.scripts.prepare) {
|
|
189
|
+
pkg.scripts.prepare = 'husky';
|
|
190
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
191
|
+
log(c.green, ' ✓ Added prepare script to package.json');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function main() {
|
|
196
|
+
console.log('');
|
|
197
|
+
log(c.blue, '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
198
|
+
log(c.blue, '🔧 autoworkflow - Setting up...');
|
|
199
|
+
log(c.blue, '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
200
|
+
console.log('');
|
|
201
|
+
|
|
202
|
+
const projectRoot = findProjectRoot();
|
|
203
|
+
|
|
204
|
+
// Skip if we're in the package itself
|
|
205
|
+
if (projectRoot.includes('node_modules')) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
log(c.blue, `Project root: ${projectRoot}`);
|
|
210
|
+
console.log('');
|
|
211
|
+
|
|
212
|
+
// Check if it's a git repo
|
|
213
|
+
if (!isGitRepo(projectRoot)) {
|
|
214
|
+
log(c.yellow, '⚠ Not a git repository. Initialize git first:');
|
|
215
|
+
log(c.yellow, ' git init');
|
|
216
|
+
console.log('');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Setup husky
|
|
221
|
+
const huskyDir = ensureHuskyDir(projectRoot);
|
|
222
|
+
initializeHusky(projectRoot);
|
|
223
|
+
|
|
224
|
+
// Create hooks
|
|
225
|
+
createPreCommitHook(huskyDir, projectRoot);
|
|
226
|
+
createCommitMsgHook(huskyDir);
|
|
227
|
+
|
|
228
|
+
// Create config
|
|
229
|
+
createDefaultConfig(projectRoot);
|
|
230
|
+
|
|
231
|
+
// Update package.json
|
|
232
|
+
updatePackageJson(projectRoot);
|
|
233
|
+
|
|
234
|
+
console.log('');
|
|
235
|
+
log(c.green, '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
236
|
+
log(c.green, '✅ Setup complete!');
|
|
237
|
+
log(c.green, '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
238
|
+
console.log('');
|
|
239
|
+
log(c.blue, 'Enforcement is now active. Just commit as usual:');
|
|
240
|
+
console.log(' git add .');
|
|
241
|
+
console.log(' git commit -m "feat: your feature"');
|
|
242
|
+
console.log('');
|
|
243
|
+
log(c.blue, 'Configuration: enforce.config.json');
|
|
244
|
+
log(c.blue, 'CLI commands: npx autoworkflow --help');
|
|
245
|
+
console.log('');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Only run if this is the actual install (not during npm pack)
|
|
249
|
+
if (!process.env.npm_config_ignore_scripts) {
|
|
250
|
+
main();
|
|
251
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "autoworkflow",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Automatic code quality enforcement with fix loops, configurable rules, and Git hook integration",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"git-hooks",
|
|
7
|
+
"pre-commit",
|
|
8
|
+
"eslint",
|
|
9
|
+
"typescript",
|
|
10
|
+
"code-quality",
|
|
11
|
+
"enforcement",
|
|
12
|
+
"husky",
|
|
13
|
+
"lint-staged",
|
|
14
|
+
"commitlint",
|
|
15
|
+
"autoworkflow"
|
|
16
|
+
],
|
|
17
|
+
"author": "autoworkflow",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/autoworkflow/autoworkflow"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/autoworkflow/autoworkflow#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/autoworkflow/autoworkflow/issues"
|
|
26
|
+
},
|
|
27
|
+
"main": "lib/index.js",
|
|
28
|
+
"bin": {
|
|
29
|
+
"autoworkflow": "bin/cli.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"bin",
|
|
33
|
+
"lib",
|
|
34
|
+
"templates"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"postinstall": "node lib/install.js"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"husky": "^9.1.7",
|
|
41
|
+
"madge": "^8.0.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"typescript": ">=5.0.0",
|
|
45
|
+
"eslint": ">=8.0.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"typescript": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"eslint": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=18.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|