aioengine 0.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 +146 -0
- package/bin/aioengine.js +3 -0
- package/package.json +41 -0
- package/src/index.js +890 -0
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# aioengine
|
|
2
|
+
|
|
3
|
+
AI change control for developers using Claude Code, Cursor, Codex, Copilot, and MCP tools.
|
|
4
|
+
|
|
5
|
+
aioengine helps you review AI-generated code before you trust it. It scans your repo for missing guardrails, checks changed files for risky edits, and flags when AI may have wandered outside the requested task.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
aioengine init
|
|
11
|
+
aioengine check
|
|
12
|
+
aioengine scope "add init command"
|
|
13
|
+
aioengine review
|
|
14
|
+
aioengine rules
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Why aioengine exists
|
|
18
|
+
|
|
19
|
+
AI coding tools can move fast, but review becomes the bottleneck.
|
|
20
|
+
|
|
21
|
+
A simple prompt can lead to unexpected changes in sensitive files like auth, billing, database migrations, environment config, deployment settings, or dependency files.
|
|
22
|
+
|
|
23
|
+
aioengine helps answer:
|
|
24
|
+
|
|
25
|
+
- Did AI touch sensitive files?
|
|
26
|
+
- Did AI change files outside the task?
|
|
27
|
+
- Did AI add or modify dependencies?
|
|
28
|
+
- Does this repo have AI coding rules?
|
|
29
|
+
- What should I review before committing?
|
|
30
|
+
|
|
31
|
+
## `aioengine init`
|
|
32
|
+
|
|
33
|
+
Sets up aioengine in your repo.
|
|
34
|
+
|
|
35
|
+
Creates:
|
|
36
|
+
|
|
37
|
+
```txt
|
|
38
|
+
.aioengine/config.json
|
|
39
|
+
CLAUDE.md
|
|
40
|
+
.cursor/rules/aioengine.mdc
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Run:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
aioengine init
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## `aioengine check`
|
|
50
|
+
|
|
51
|
+
Scans your repo for AI coding setup risks.
|
|
52
|
+
|
|
53
|
+
Checks for:
|
|
54
|
+
|
|
55
|
+
- Git repo
|
|
56
|
+
- `package.json`
|
|
57
|
+
- `.aioengine/config.json`
|
|
58
|
+
- `.gitignore`
|
|
59
|
+
- env files
|
|
60
|
+
- Claude rules
|
|
61
|
+
- Cursor rules
|
|
62
|
+
- MCP config
|
|
63
|
+
- GitHub Actions
|
|
64
|
+
- tests
|
|
65
|
+
|
|
66
|
+
Run:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
aioengine check
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## `aioengine scope`
|
|
73
|
+
|
|
74
|
+
Checks whether changed files match the task you gave your AI coding tool.
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
aioengine scope "update landing page headline"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
If the task sounds like a UI change but AI modified billing, database, env, dependency, or deployment files, aioengine will flag possible scope drift.
|
|
83
|
+
|
|
84
|
+
## `aioengine review`
|
|
85
|
+
|
|
86
|
+
Reviews current uncommitted changes for risky files.
|
|
87
|
+
|
|
88
|
+
Run:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
aioengine review
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
aioengine will flag changes to files that often deserve extra review, such as:
|
|
95
|
+
|
|
96
|
+
- env files
|
|
97
|
+
- auth files
|
|
98
|
+
- billing files
|
|
99
|
+
- database files
|
|
100
|
+
- deployment config
|
|
101
|
+
- dependency files
|
|
102
|
+
- GitHub workflow files
|
|
103
|
+
|
|
104
|
+
## `aioengine rules`
|
|
105
|
+
|
|
106
|
+
Generates starter AI coding rules for Claude Code and Cursor.
|
|
107
|
+
|
|
108
|
+
Run:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
aioengine rules
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This creates or skips:
|
|
115
|
+
|
|
116
|
+
```txt
|
|
117
|
+
CLAUDE.md
|
|
118
|
+
.cursor/rules/aioengine.mdc
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Example workflow
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
aioengine init
|
|
125
|
+
aioengine check
|
|
126
|
+
|
|
127
|
+
# Ask Claude, Cursor, Codex, or another AI coding tool to make a change.
|
|
128
|
+
|
|
129
|
+
aioengine scope "update landing page headline"
|
|
130
|
+
aioengine review
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Current status
|
|
134
|
+
|
|
135
|
+
aioengine is in early development.
|
|
136
|
+
|
|
137
|
+
The first goal is simple: help AI-assisted developers catch risky or out-of-scope changes before committing code.
|
|
138
|
+
|
|
139
|
+
Future goals include:
|
|
140
|
+
|
|
141
|
+
- better risk detection
|
|
142
|
+
- prettier local reports
|
|
143
|
+
- GitHub PR checks
|
|
144
|
+
- saved reports
|
|
145
|
+
- team rules
|
|
146
|
+
- paid CI features
|
package/bin/aioengine.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aioengine",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI change control for developers using AI coding tools.",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "node ./src/index.js",
|
|
8
|
+
"check": "node ./src/index.js check",
|
|
9
|
+
"review": "node ./src/index.js review",
|
|
10
|
+
"rules": "node ./src/index.js rules",
|
|
11
|
+
"scope": "node ./src/index.js scope",
|
|
12
|
+
"init": "node ./src/index.js init"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"ai",
|
|
16
|
+
"ai-coding",
|
|
17
|
+
"cli",
|
|
18
|
+
"code-review",
|
|
19
|
+
"cursor",
|
|
20
|
+
"claude",
|
|
21
|
+
"developer-tools"
|
|
22
|
+
],
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "UNLICENSED",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"commander": "^15.0.0",
|
|
28
|
+
"picocolors": "^1.1.1"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src",
|
|
32
|
+
"README.md",
|
|
33
|
+
"bin"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"bin": {
|
|
39
|
+
"aioengine": "bin/aioengine.js"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name("aioengine")
|
|
13
|
+
.description("AI change control for developers using AI coding tools.")
|
|
14
|
+
.version("0.1.0");
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command("init")
|
|
18
|
+
.description("Set up aioengine for AI change control in this repo.")
|
|
19
|
+
.action(() => {
|
|
20
|
+
runInit();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.command("check")
|
|
25
|
+
.description("Scan your repo for AI coding setup risks.")
|
|
26
|
+
.action(() => {
|
|
27
|
+
runCheck();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command("review")
|
|
32
|
+
.description("Review current Git changes for risky AI-generated edits.")
|
|
33
|
+
.action(() => {
|
|
34
|
+
runReview();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.command("scope")
|
|
39
|
+
.description("Check whether changed files match the requested task.")
|
|
40
|
+
.argument("[task...]", "The task you asked the AI coding tool to do")
|
|
41
|
+
.action((taskParts = []) => {
|
|
42
|
+
const task = Array.isArray(taskParts) ? taskParts.join(" ") : "";
|
|
43
|
+
runScope(task);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
program
|
|
47
|
+
.command("rules")
|
|
48
|
+
.description("Generate starter AI coding rules for this repo.")
|
|
49
|
+
.action(() => {
|
|
50
|
+
runRules();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
program.parse();
|
|
54
|
+
|
|
55
|
+
function runInit() {
|
|
56
|
+
printHeader("aioengine Init");
|
|
57
|
+
|
|
58
|
+
const root = getProjectRoot();
|
|
59
|
+
const created = [];
|
|
60
|
+
const skipped = [];
|
|
61
|
+
|
|
62
|
+
const aioengineDir = ".aioengine";
|
|
63
|
+
const configPath = path.join(aioengineDir, "config.json");
|
|
64
|
+
const claudePath = "CLAUDE.md";
|
|
65
|
+
const cursorDir = ".cursor/rules";
|
|
66
|
+
const cursorPath = path.join(cursorDir, "aioengine.mdc");
|
|
67
|
+
|
|
68
|
+
if (!isInsideGitRepo()) {
|
|
69
|
+
console.log(
|
|
70
|
+
pc.yellow(
|
|
71
|
+
"Warning: aioengine works best inside a Git repo. Run this from your project folder."
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
console.log("");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!exists(aioengineDir, root)) {
|
|
78
|
+
fs.mkdirSync(path.join(root, aioengineDir), { recursive: true });
|
|
79
|
+
created.push(aioengineDir);
|
|
80
|
+
} else {
|
|
81
|
+
skipped.push(aioengineDir);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!exists(configPath, root)) {
|
|
85
|
+
fs.writeFileSync(
|
|
86
|
+
path.join(root, configPath),
|
|
87
|
+
JSON.stringify(getDefaultConfig(), null, 2)
|
|
88
|
+
);
|
|
89
|
+
created.push(configPath);
|
|
90
|
+
} else {
|
|
91
|
+
skipped.push(configPath);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!exists(claudePath, root)) {
|
|
95
|
+
fs.writeFileSync(path.join(root, claudePath), getClaudeRules());
|
|
96
|
+
created.push(claudePath);
|
|
97
|
+
} else {
|
|
98
|
+
skipped.push(claudePath);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!exists(cursorDir, root)) {
|
|
102
|
+
fs.mkdirSync(path.join(root, cursorDir), { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!exists(cursorPath, root)) {
|
|
106
|
+
fs.writeFileSync(path.join(root, cursorPath), getCursorRules());
|
|
107
|
+
created.push(cursorPath);
|
|
108
|
+
} else {
|
|
109
|
+
skipped.push(cursorPath);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
printSection("Created", created, "green");
|
|
113
|
+
printSection("Skipped", skipped, "yellow");
|
|
114
|
+
|
|
115
|
+
console.log(pc.bold("Next steps:"));
|
|
116
|
+
console.log(` 1. Run ${pc.cyan("aioengine check")}`);
|
|
117
|
+
console.log(` 2. Make or review AI-generated changes`);
|
|
118
|
+
console.log(` 3. Run ${pc.cyan('aioengine scope "describe the task"')}`);
|
|
119
|
+
console.log(` 4. Run ${pc.cyan("aioengine review")} before committing`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function runCheck() {
|
|
123
|
+
const root = getProjectRoot();
|
|
124
|
+
|
|
125
|
+
const critical = [];
|
|
126
|
+
const warnings = [];
|
|
127
|
+
const passed = [];
|
|
128
|
+
|
|
129
|
+
printHeader("aioengine Check");
|
|
130
|
+
|
|
131
|
+
const hasGit = isInsideGitRepo();
|
|
132
|
+
const hasPackageJson = exists("package.json", root);
|
|
133
|
+
const hasGitignore = exists(".gitignore", root);
|
|
134
|
+
const hasClaude = exists("CLAUDE.md", root);
|
|
135
|
+
const hasCursorRules =
|
|
136
|
+
exists(".cursor/rules", root) || exists(".cursorrules", root);
|
|
137
|
+
const hasAioengineConfig = exists(".aioengine/config.json", root);
|
|
138
|
+
const envFiles = findFiles(root, [
|
|
139
|
+
".env",
|
|
140
|
+
".env.local",
|
|
141
|
+
".env.production",
|
|
142
|
+
".env.development",
|
|
143
|
+
]);
|
|
144
|
+
const mcpFiles = findFiles(root, [".mcp.json", "mcp.json"]);
|
|
145
|
+
const hasGithubActions = exists(".github/workflows", root);
|
|
146
|
+
const hasTests =
|
|
147
|
+
exists("__tests__", root) ||
|
|
148
|
+
exists("tests", root) ||
|
|
149
|
+
exists("test", root) ||
|
|
150
|
+
packageScriptIncludes(root, "test");
|
|
151
|
+
|
|
152
|
+
if (hasGit) passed.push("Git repo detected");
|
|
153
|
+
else warnings.push("No Git repo detected. aioengine works best inside a Git repo.");
|
|
154
|
+
|
|
155
|
+
if (hasPackageJson) passed.push("package.json detected");
|
|
156
|
+
else warnings.push("No package.json detected. Some checks may be limited.");
|
|
157
|
+
|
|
158
|
+
if (hasAioengineConfig) passed.push(".aioengine/config.json detected");
|
|
159
|
+
else warnings.push("No .aioengine/config.json found. Run aioengine init to create one.");
|
|
160
|
+
|
|
161
|
+
if (hasGitignore) {
|
|
162
|
+
passed.push(".gitignore detected");
|
|
163
|
+
|
|
164
|
+
const gitignore = read(".gitignore", root);
|
|
165
|
+
if (!gitignore.includes(".env")) {
|
|
166
|
+
critical.push(".gitignore does not appear to include .env patterns.");
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
critical.push(
|
|
170
|
+
"Missing .gitignore. Env files and generated files may be accidentally committed."
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (envFiles.length > 0) {
|
|
175
|
+
critical.push(
|
|
176
|
+
`Env files detected at repo root: ${envFiles.join(
|
|
177
|
+
", "
|
|
178
|
+
)}. AI coding tools may be able to read sensitive local config.`
|
|
179
|
+
);
|
|
180
|
+
} else {
|
|
181
|
+
passed.push("No common env files detected at repo root");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (hasClaude) passed.push("CLAUDE.md detected");
|
|
185
|
+
else warnings.push("No CLAUDE.md found. Claude Code may not have repo-specific boundaries.");
|
|
186
|
+
|
|
187
|
+
if (hasCursorRules) passed.push("Cursor rules detected");
|
|
188
|
+
else warnings.push("No Cursor rules detected. Cursor may not have project-specific boundaries.");
|
|
189
|
+
|
|
190
|
+
if (mcpFiles.length > 0) {
|
|
191
|
+
warnings.push(
|
|
192
|
+
`MCP config detected: ${mcpFiles.join(", ")}. Review tool access carefully.`
|
|
193
|
+
);
|
|
194
|
+
} else {
|
|
195
|
+
passed.push("No root MCP config detected");
|
|
196
|
+
}
|
|
197
|
+
if (hasGithubActions) passed.push("GitHub Actions detected");
|
|
198
|
+
else warnings.push("No GitHub Actions folder detected. PR checks may not be configured yet.");
|
|
199
|
+
|
|
200
|
+
if (hasTests) passed.push("Tests detected");
|
|
201
|
+
else warnings.push("No obvious tests detected. AI-generated changes may be harder to verify.");
|
|
202
|
+
|
|
203
|
+
const score = calculateScore(critical.length, warnings.length, passed.length);
|
|
204
|
+
|
|
205
|
+
console.log(`${pc.bold("Project:")} ${root}`);
|
|
206
|
+
console.log(`${pc.bold("AI coding setup score:")} ${formatScore(score)}\n`);
|
|
207
|
+
|
|
208
|
+
printSection("Critical", critical, "red");
|
|
209
|
+
printSection("Warnings", warnings, "yellow");
|
|
210
|
+
printSection("Passed", passed, "green");
|
|
211
|
+
|
|
212
|
+
console.log(pc.bold("Recommended next step:"));
|
|
213
|
+
|
|
214
|
+
if (!hasAioengineConfig || !hasClaude || !hasCursorRules) {
|
|
215
|
+
console.log(` Run ${pc.cyan("aioengine init")} to set up AI change control.`);
|
|
216
|
+
} else {
|
|
217
|
+
console.log(` Run ${pc.cyan("aioengine review")} before committing AI-generated changes.`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function runReview() {
|
|
222
|
+
printHeader("aioengine Review");
|
|
223
|
+
|
|
224
|
+
if (!isInsideGitRepo()) {
|
|
225
|
+
console.log(pc.red("This command must be run inside a Git repo."));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const root = getProjectRoot();
|
|
230
|
+
const files = getChangedFiles(root);
|
|
231
|
+
|
|
232
|
+
if (files.length === 0) {
|
|
233
|
+
console.log(pc.green("No uncommitted changes found."));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const riskyFiles = files.filter(isRiskyFile);
|
|
238
|
+
const reviewItems = [];
|
|
239
|
+
|
|
240
|
+
if (files.length > 12) {
|
|
241
|
+
reviewItems.push(`Large change set detected: ${files.length} files changed.`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (riskyFiles.length > 0) {
|
|
245
|
+
reviewItems.push(`High-risk files changed: ${riskyFiles.join(", ")}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const packageChanged = files.some((file) =>
|
|
249
|
+
["package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock"].includes(file)
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
if (packageChanged) {
|
|
253
|
+
reviewItems.push(
|
|
254
|
+
"Package/dependency files changed. Review for unexpected dependency additions."
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
console.log(`${pc.bold("Project:")} ${root}`);
|
|
259
|
+
console.log(`${pc.bold("Changed files:")} ${files.length}\n`);
|
|
260
|
+
|
|
261
|
+
files.forEach((file) => {
|
|
262
|
+
const marker = isRiskyFile(file) ? pc.red("✗") : pc.green("✓");
|
|
263
|
+
console.log(` ${marker} ${file}`);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
console.log("");
|
|
267
|
+
|
|
268
|
+
if (reviewItems.length === 0) {
|
|
269
|
+
console.log(pc.green("No obvious high-risk changes detected."));
|
|
270
|
+
} else {
|
|
271
|
+
printSection("Review recommended", reviewItems, "yellow");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function runScope(task) {
|
|
276
|
+
printHeader("aioengine Scope");
|
|
277
|
+
|
|
278
|
+
if (!isInsideGitRepo()) {
|
|
279
|
+
console.log(pc.red("This command must be run inside a Git repo."));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const root = getProjectRoot();
|
|
284
|
+
const files = getChangedFiles(root);
|
|
285
|
+
|
|
286
|
+
if (files.length === 0) {
|
|
287
|
+
console.log(pc.green("No uncommitted changes found."));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const cleanTask = task.trim();
|
|
292
|
+
|
|
293
|
+
if (!cleanTask) {
|
|
294
|
+
console.log(pc.yellow("No task description provided."));
|
|
295
|
+
console.log("");
|
|
296
|
+
console.log("Try:");
|
|
297
|
+
console.log(` ${pc.cyan('aioengine scope "fix pricing page layout"')}`);
|
|
298
|
+
console.log("");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const profile = inferTaskProfile(cleanTask);
|
|
302
|
+
const outOfScopeFiles = files.filter((file) =>
|
|
303
|
+
isProbablyOutOfScope(file, profile)
|
|
304
|
+
);
|
|
305
|
+
const riskyFiles = files.filter(isRiskyFile);
|
|
306
|
+
|
|
307
|
+
const reviewItems = [];
|
|
308
|
+
|
|
309
|
+
if (cleanTask) {
|
|
310
|
+
console.log(`${pc.bold("Task:")} ${cleanTask}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
console.log(`${pc.bold("Detected task type:")} ${profile.label}`);
|
|
314
|
+
console.log(`${pc.bold("Changed files:")} ${files.length}\n`);
|
|
315
|
+
|
|
316
|
+
files.forEach((file) => {
|
|
317
|
+
const outOfScope = outOfScopeFiles.includes(file);
|
|
318
|
+
const risky = riskyFiles.includes(file);
|
|
319
|
+
|
|
320
|
+
let marker = pc.green("✓");
|
|
321
|
+
let note = pc.dim("in scope");
|
|
322
|
+
|
|
323
|
+
if (outOfScope) {
|
|
324
|
+
marker = pc.red("✗");
|
|
325
|
+
note = pc.red("possible scope drift");
|
|
326
|
+
} else if (risky) {
|
|
327
|
+
marker = pc.yellow("!");
|
|
328
|
+
note = pc.yellow("review carefully");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
console.log(` ${marker} ${file} ${pc.dim("—")} ${note}`);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (outOfScopeFiles.length > 0) {
|
|
335
|
+
reviewItems.push(
|
|
336
|
+
`Possible out-of-scope files changed: ${outOfScopeFiles.join(", ")}`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (riskyFiles.length > 0) {
|
|
341
|
+
reviewItems.push(`High-risk files changed: ${riskyFiles.join(", ")}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (files.length > 12) {
|
|
345
|
+
reviewItems.push(
|
|
346
|
+
`Large change set detected: ${files.length} files changed. AI may have touched more than needed.`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
console.log("");
|
|
351
|
+
|
|
352
|
+
if (reviewItems.length === 0) {
|
|
353
|
+
console.log(pc.green("No obvious scope drift detected."));
|
|
354
|
+
console.log("");
|
|
355
|
+
console.log(
|
|
356
|
+
pc.dim("Still review the diff normally before committing AI-generated code.")
|
|
357
|
+
);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
printSection("Scope review recommended", reviewItems, "yellow");
|
|
362
|
+
|
|
363
|
+
console.log(pc.bold("Recommendation:"));
|
|
364
|
+
|
|
365
|
+
if (outOfScopeFiles.length > 0) {
|
|
366
|
+
console.log(
|
|
367
|
+
` ${pc.red(
|
|
368
|
+
"Do not commit yet."
|
|
369
|
+
)} Review the out-of-scope files and revert anything unrelated to the task.`
|
|
370
|
+
);
|
|
371
|
+
} else {
|
|
372
|
+
console.log(
|
|
373
|
+
` ${pc.yellow(
|
|
374
|
+
"Review carefully."
|
|
375
|
+
)} These changes may be valid, but they touch sensitive areas.`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function runRules() {
|
|
380
|
+
printHeader("aioengine Rules");
|
|
381
|
+
|
|
382
|
+
const root = getProjectRoot();
|
|
383
|
+
|
|
384
|
+
const claudePath = "CLAUDE.md";
|
|
385
|
+
const cursorDir = ".cursor/rules";
|
|
386
|
+
const cursorPath = path.join(cursorDir, "aioengine.mdc");
|
|
387
|
+
|
|
388
|
+
const created = [];
|
|
389
|
+
const skipped = [];
|
|
390
|
+
|
|
391
|
+
if (!exists(claudePath, root)) {
|
|
392
|
+
fs.writeFileSync(path.join(root, claudePath), getClaudeRules());
|
|
393
|
+
created.push(claudePath);
|
|
394
|
+
} else {
|
|
395
|
+
skipped.push(claudePath);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (!exists(cursorDir, root)) {
|
|
399
|
+
fs.mkdirSync(path.join(root, cursorDir), { recursive: true });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!exists(cursorPath, root)) {
|
|
403
|
+
fs.writeFileSync(path.join(root, cursorPath), getCursorRules());
|
|
404
|
+
created.push(cursorPath);
|
|
405
|
+
} else {
|
|
406
|
+
skipped.push(cursorPath);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
printSection("Created", created, "green");
|
|
410
|
+
printSection("Skipped", skipped, "yellow");
|
|
411
|
+
|
|
412
|
+
console.log(pc.bold("Next step:"));
|
|
413
|
+
console.log(` Run ${pc.cyan("aioengine check")} again.`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function getChangedFiles(root) {
|
|
417
|
+
try {
|
|
418
|
+
const changed = execSync("git diff --name-only", {
|
|
419
|
+
encoding: "utf8",
|
|
420
|
+
cwd: root,
|
|
421
|
+
})
|
|
422
|
+
.trim()
|
|
423
|
+
.split("\n")
|
|
424
|
+
.filter(Boolean);
|
|
425
|
+
|
|
426
|
+
const untracked = execSync("git ls-files --others --exclude-standard", {
|
|
427
|
+
encoding: "utf8",
|
|
428
|
+
cwd: root,
|
|
429
|
+
})
|
|
430
|
+
.trim()
|
|
431
|
+
.split("\n")
|
|
432
|
+
.filter(Boolean);
|
|
433
|
+
|
|
434
|
+
return Array.from(new Set([...changed, ...untracked]));
|
|
435
|
+
} catch {
|
|
436
|
+
return [];
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function inferTaskProfile(task) {
|
|
441
|
+
const lower = task.toLowerCase();
|
|
442
|
+
|
|
443
|
+
const profiles = [
|
|
444
|
+
{
|
|
445
|
+
id: "cli",
|
|
446
|
+
label: "CLI / tooling task",
|
|
447
|
+
keywords: [
|
|
448
|
+
"cli",
|
|
449
|
+
"command",
|
|
450
|
+
"terminal",
|
|
451
|
+
"init",
|
|
452
|
+
"check",
|
|
453
|
+
"review",
|
|
454
|
+
"scope",
|
|
455
|
+
"rules",
|
|
456
|
+
"package",
|
|
457
|
+
"script",
|
|
458
|
+
"npm",
|
|
459
|
+
"bin",
|
|
460
|
+
"commander",
|
|
461
|
+
"aioengine",
|
|
462
|
+
],
|
|
463
|
+
allowed: [
|
|
464
|
+
"packages/cli/",
|
|
465
|
+
"package.json",
|
|
466
|
+
"package-lock.json",
|
|
467
|
+
"src/index.js",
|
|
468
|
+
"readme",
|
|
469
|
+
".aioengine/",
|
|
470
|
+
"CLAUDE.md",
|
|
471
|
+
".cursor/",
|
|
472
|
+
],
|
|
473
|
+
sensitive: [
|
|
474
|
+
".env",
|
|
475
|
+
"auth",
|
|
476
|
+
"stripe",
|
|
477
|
+
"billing",
|
|
478
|
+
"payment",
|
|
479
|
+
"supabase",
|
|
480
|
+
"migration",
|
|
481
|
+
"middleware",
|
|
482
|
+
".github/workflows",
|
|
483
|
+
],
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
id: "ui",
|
|
487
|
+
label: "UI / frontend task",
|
|
488
|
+
keywords: [
|
|
489
|
+
"ui",
|
|
490
|
+
"layout",
|
|
491
|
+
"style",
|
|
492
|
+
"design",
|
|
493
|
+
"button",
|
|
494
|
+
"card",
|
|
495
|
+
"page",
|
|
496
|
+
"copy",
|
|
497
|
+
"text",
|
|
498
|
+
"color",
|
|
499
|
+
"colour",
|
|
500
|
+
"spacing",
|
|
501
|
+
"responsive",
|
|
502
|
+
"navbar",
|
|
503
|
+
"footer",
|
|
504
|
+
"hero",
|
|
505
|
+
"landing",
|
|
506
|
+
"pricing",
|
|
507
|
+
"headline",
|
|
508
|
+
],
|
|
509
|
+
allowed: [
|
|
510
|
+
"app/",
|
|
511
|
+
"pages/",
|
|
512
|
+
"components/",
|
|
513
|
+
"styles/",
|
|
514
|
+
"public/",
|
|
515
|
+
"src/app/",
|
|
516
|
+
"src/components/",
|
|
517
|
+
"src/styles/",
|
|
518
|
+
".css",
|
|
519
|
+
".tsx",
|
|
520
|
+
".jsx",
|
|
521
|
+
],
|
|
522
|
+
sensitive: [
|
|
523
|
+
"api/",
|
|
524
|
+
"auth",
|
|
525
|
+
"session",
|
|
526
|
+
"stripe",
|
|
527
|
+
"billing",
|
|
528
|
+
"payment",
|
|
529
|
+
"supabase",
|
|
530
|
+
"migration",
|
|
531
|
+
"schema",
|
|
532
|
+
"rls",
|
|
533
|
+
".env",
|
|
534
|
+
"package.json",
|
|
535
|
+
"package-lock.json",
|
|
536
|
+
"middleware",
|
|
537
|
+
".github/workflows",
|
|
538
|
+
"next.config",
|
|
539
|
+
"vercel",
|
|
540
|
+
],
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
id: "docs",
|
|
544
|
+
label: "Docs / copy task",
|
|
545
|
+
keywords: [
|
|
546
|
+
"docs",
|
|
547
|
+
"readme",
|
|
548
|
+
"copy",
|
|
549
|
+
"text",
|
|
550
|
+
"wording",
|
|
551
|
+
"content",
|
|
552
|
+
"landing page copy",
|
|
553
|
+
"headline",
|
|
554
|
+
"description",
|
|
555
|
+
],
|
|
556
|
+
allowed: [
|
|
557
|
+
".md",
|
|
558
|
+
"readme",
|
|
559
|
+
"app/",
|
|
560
|
+
"src/app/",
|
|
561
|
+
"components/",
|
|
562
|
+
"src/components/",
|
|
563
|
+
],
|
|
564
|
+
sensitive: [
|
|
565
|
+
"api/",
|
|
566
|
+
"auth",
|
|
567
|
+
"stripe",
|
|
568
|
+
"billing",
|
|
569
|
+
"payment",
|
|
570
|
+
"supabase",
|
|
571
|
+
"migration",
|
|
572
|
+
".env",
|
|
573
|
+
"package.json",
|
|
574
|
+
"middleware",
|
|
575
|
+
],
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
id: "backend",
|
|
579
|
+
label: "Backend / API task",
|
|
580
|
+
keywords: [
|
|
581
|
+
"api",
|
|
582
|
+
"route",
|
|
583
|
+
"server",
|
|
584
|
+
"backend",
|
|
585
|
+
"database",
|
|
586
|
+
"supabase",
|
|
587
|
+
"webhook",
|
|
588
|
+
"stripe",
|
|
589
|
+
"auth",
|
|
590
|
+
"login",
|
|
591
|
+
"billing",
|
|
592
|
+
],
|
|
593
|
+
allowed: [
|
|
594
|
+
"api/",
|
|
595
|
+
"server",
|
|
596
|
+
"lib/",
|
|
597
|
+
"src/lib/",
|
|
598
|
+
"supabase",
|
|
599
|
+
"schema",
|
|
600
|
+
"migration",
|
|
601
|
+
"middleware",
|
|
602
|
+
"package.json",
|
|
603
|
+
],
|
|
604
|
+
sensitive: [".env", "billing", "payment", "stripe", "auth", "rls", "migration"],
|
|
605
|
+
},
|
|
606
|
+
];
|
|
607
|
+
|
|
608
|
+
const matched = profiles.find((profile) =>
|
|
609
|
+
profile.keywords.some((keyword) => lower.includes(keyword))
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
return (
|
|
613
|
+
matched ?? {
|
|
614
|
+
id: "unknown",
|
|
615
|
+
label: "Unknown / general task",
|
|
616
|
+
keywords: [],
|
|
617
|
+
allowed: [],
|
|
618
|
+
sensitive: [
|
|
619
|
+
".env",
|
|
620
|
+
"auth",
|
|
621
|
+
"session",
|
|
622
|
+
"stripe",
|
|
623
|
+
"billing",
|
|
624
|
+
"payment",
|
|
625
|
+
"supabase",
|
|
626
|
+
"migration",
|
|
627
|
+
"schema",
|
|
628
|
+
"rls",
|
|
629
|
+
"middleware",
|
|
630
|
+
"package.json",
|
|
631
|
+
"package-lock.json",
|
|
632
|
+
".github/workflows",
|
|
633
|
+
],
|
|
634
|
+
}
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function isProbablyOutOfScope(file, profile) {
|
|
639
|
+
const lower = file.toLowerCase();
|
|
640
|
+
|
|
641
|
+
if (profile.id === "unknown") {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const touchesSensitiveArea = profile.sensitive.some((pattern) =>
|
|
646
|
+
lower.includes(pattern)
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
if (!touchesSensitiveArea) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const explicitlyAllowed = profile.allowed.some((pattern) =>
|
|
654
|
+
lower.includes(pattern)
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
return !explicitlyAllowed;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function isRiskyFile(file) {
|
|
661
|
+
const riskyPatterns = [
|
|
662
|
+
".env",
|
|
663
|
+
"auth",
|
|
664
|
+
"session",
|
|
665
|
+
"middleware",
|
|
666
|
+
"stripe",
|
|
667
|
+
"billing",
|
|
668
|
+
"payment",
|
|
669
|
+
"supabase",
|
|
670
|
+
"migration",
|
|
671
|
+
"schema",
|
|
672
|
+
"rls",
|
|
673
|
+
"vercel",
|
|
674
|
+
"netlify",
|
|
675
|
+
"docker",
|
|
676
|
+
"package.json",
|
|
677
|
+
"package-lock.json",
|
|
678
|
+
"pnpm-lock.yaml",
|
|
679
|
+
"yarn.lock",
|
|
680
|
+
".github/workflows",
|
|
681
|
+
];
|
|
682
|
+
|
|
683
|
+
const lower = file.toLowerCase();
|
|
684
|
+
return riskyPatterns.some((pattern) => lower.includes(pattern));
|
|
685
|
+
}
|
|
686
|
+
function getProjectRoot() {
|
|
687
|
+
try {
|
|
688
|
+
return execSync("git rev-parse --show-toplevel", {
|
|
689
|
+
encoding: "utf8",
|
|
690
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
691
|
+
}).trim();
|
|
692
|
+
} catch {
|
|
693
|
+
return process.cwd();
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function isInsideGitRepo() {
|
|
698
|
+
try {
|
|
699
|
+
const result = execSync("git rev-parse --is-inside-work-tree", {
|
|
700
|
+
encoding: "utf8",
|
|
701
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
702
|
+
})
|
|
703
|
+
.trim()
|
|
704
|
+
.toLowerCase();
|
|
705
|
+
|
|
706
|
+
return result === "true";
|
|
707
|
+
} catch {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function exists(relativePath, root = process.cwd()) {
|
|
713
|
+
return fs.existsSync(path.join(root, relativePath));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function read(relativePath, root = process.cwd()) {
|
|
717
|
+
try {
|
|
718
|
+
return fs.readFileSync(path.join(root, relativePath), "utf8");
|
|
719
|
+
} catch {
|
|
720
|
+
return "";
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function findFiles(root, files) {
|
|
725
|
+
return files.filter((file) => exists(file, root));
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function packageScriptIncludes(root, scriptName) {
|
|
729
|
+
const packageJson = read("package.json", root);
|
|
730
|
+
|
|
731
|
+
if (!packageJson) {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
const parsed = JSON.parse(packageJson);
|
|
737
|
+
const script = parsed.scripts?.[scriptName];
|
|
738
|
+
|
|
739
|
+
if (!script) {
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (scriptName === "test") {
|
|
744
|
+
const normalized = script.toLowerCase().replace(/\s+/g, " ").trim();
|
|
745
|
+
|
|
746
|
+
const fakeTestScripts = [
|
|
747
|
+
'echo "error: no test specified" && exit 1',
|
|
748
|
+
"echo 'error: no test specified' && exit 1",
|
|
749
|
+
"echo error: no test specified && exit 1",
|
|
750
|
+
];
|
|
751
|
+
|
|
752
|
+
if (fakeTestScripts.includes(normalized)) {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return true;
|
|
758
|
+
} catch {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function calculateScore(criticalCount, warningCount, passedCount) {
|
|
764
|
+
const raw =
|
|
765
|
+
100 - criticalCount * 18 - warningCount * 7 + Math.min(passedCount * 2, 10);
|
|
766
|
+
|
|
767
|
+
return Math.max(0, Math.min(100, raw));
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function formatScore(score) {
|
|
771
|
+
if (score >= 80) return pc.green(`${score}/100`);
|
|
772
|
+
if (score >= 60) return pc.yellow(`${score}/100`);
|
|
773
|
+
return pc.red(`${score}/100`);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function printHeader(title) {
|
|
777
|
+
console.log("");
|
|
778
|
+
console.log(pc.bold(pc.cyan(title)));
|
|
779
|
+
console.log(pc.dim("AI change control for developers"));
|
|
780
|
+
console.log("");
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function printSection(title, items, color) {
|
|
784
|
+
if (items.length === 0) return;
|
|
785
|
+
|
|
786
|
+
const colorFn =
|
|
787
|
+
color === "red" ? pc.red : color === "yellow" ? pc.yellow : pc.green;
|
|
788
|
+
|
|
789
|
+
const icon = color === "red" ? "✗" : color === "yellow" ? "!" : "✓";
|
|
790
|
+
|
|
791
|
+
console.log(pc.bold(colorFn(title)));
|
|
792
|
+
|
|
793
|
+
items.forEach((item) => {
|
|
794
|
+
console.log(` ${colorFn(icon)} ${item}`);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
console.log("");
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function getDefaultConfig() {
|
|
801
|
+
return {
|
|
802
|
+
version: "0.1.0",
|
|
803
|
+
projectType: "auto",
|
|
804
|
+
highRiskPatterns: [
|
|
805
|
+
".env",
|
|
806
|
+
"auth",
|
|
807
|
+
"session",
|
|
808
|
+
"middleware",
|
|
809
|
+
"stripe",
|
|
810
|
+
"billing",
|
|
811
|
+
"payment",
|
|
812
|
+
"supabase",
|
|
813
|
+
"migration",
|
|
814
|
+
"schema",
|
|
815
|
+
"rls",
|
|
816
|
+
"vercel",
|
|
817
|
+
"netlify",
|
|
818
|
+
"docker",
|
|
819
|
+
"package.json",
|
|
820
|
+
"package-lock.json",
|
|
821
|
+
"pnpm-lock.yaml",
|
|
822
|
+
"yarn.lock",
|
|
823
|
+
".github/workflows",
|
|
824
|
+
],
|
|
825
|
+
reviewRules: {
|
|
826
|
+
warnOnLargeChanges: true,
|
|
827
|
+
largeChangeFileCount: 12,
|
|
828
|
+
requireReviewForSensitiveFiles: true,
|
|
829
|
+
warnOnDependencyChanges: true,
|
|
830
|
+
},
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function getClaudeRules() {
|
|
835
|
+
return `# AI Coding Rules
|
|
836
|
+
|
|
837
|
+
This project uses AI-assisted development. Follow these rules carefully.
|
|
838
|
+
|
|
839
|
+
## Before changing code
|
|
840
|
+
|
|
841
|
+
- Understand the requested task before editing files.
|
|
842
|
+
- Keep changes narrow and directly related to the task.
|
|
843
|
+
- Do not modify auth, billing, database, env, deployment, or security files unless explicitly asked.
|
|
844
|
+
- Do not add new dependencies unless clearly necessary.
|
|
845
|
+
- Do not delete files without explaining why.
|
|
846
|
+
|
|
847
|
+
## High-risk areas
|
|
848
|
+
|
|
849
|
+
Changes to these areas require extra human review:
|
|
850
|
+
|
|
851
|
+
- Authentication and session logic
|
|
852
|
+
- Billing and payment code
|
|
853
|
+
- Database migrations and RLS policies
|
|
854
|
+
- Environment variables and config files
|
|
855
|
+
- Deployment settings
|
|
856
|
+
- GitHub Actions and CI
|
|
857
|
+
- Middleware and API routes
|
|
858
|
+
- Package/dependency files
|
|
859
|
+
|
|
860
|
+
## Testing
|
|
861
|
+
|
|
862
|
+
When making code changes:
|
|
863
|
+
|
|
864
|
+
- Run relevant tests when possible.
|
|
865
|
+
- Mention any tests that were not run.
|
|
866
|
+
- Keep the final summary specific and honest.
|
|
867
|
+
|
|
868
|
+
## Scope
|
|
869
|
+
|
|
870
|
+
If the task is UI-only, do not edit backend, database, billing, auth, or deployment files.
|
|
871
|
+
`;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function getCursorRules() {
|
|
875
|
+
return `---
|
|
876
|
+
description: AI coding safety rules generated by aioengine
|
|
877
|
+
alwaysApply: true
|
|
878
|
+
---
|
|
879
|
+
|
|
880
|
+
Keep changes narrow and task-focused.
|
|
881
|
+
|
|
882
|
+
Do not modify auth, billing, database, environment, deployment, dependency, or security files unless explicitly requested.
|
|
883
|
+
|
|
884
|
+
Flag high-risk changes for human review.
|
|
885
|
+
|
|
886
|
+
Do not add dependencies without a clear reason.
|
|
887
|
+
|
|
888
|
+
For UI-only tasks, avoid backend, API, database, and config changes.
|
|
889
|
+
`;
|
|
890
|
+
}
|