aioengine 0.1.3 → 0.1.4
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 +74 -3
- package/package.json +3 -2
- package/src/index.js +204 -0
package/README.md
CHANGED
|
@@ -25,6 +25,12 @@ npx aioengine scope "update landing page headline"
|
|
|
25
25
|
npx aioengine review
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
In CI or pull request workflows:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx aioengine ci --task "update landing page headline"
|
|
32
|
+
```
|
|
33
|
+
|
|
28
34
|
## Commands
|
|
29
35
|
|
|
30
36
|
```bash
|
|
@@ -32,6 +38,7 @@ npx aioengine init
|
|
|
32
38
|
npx aioengine check
|
|
33
39
|
npx aioengine scope "add init command"
|
|
34
40
|
npx aioengine review
|
|
41
|
+
npx aioengine ci --task "add init command"
|
|
35
42
|
npx aioengine rules
|
|
36
43
|
```
|
|
37
44
|
|
|
@@ -39,7 +46,7 @@ npx aioengine rules
|
|
|
39
46
|
|
|
40
47
|
AI coding tools can move fast, but review becomes the bottleneck.
|
|
41
48
|
|
|
42
|
-
A simple prompt can lead to unexpected changes in sensitive files like auth, billing, database migrations, environment config, deployment settings, or
|
|
49
|
+
A simple prompt can lead to unexpected changes in sensitive files like auth, billing, database migrations, environment config, deployment settings, dependency files, or CI workflows.
|
|
43
50
|
|
|
44
51
|
aioengine helps answer:
|
|
45
52
|
|
|
@@ -47,12 +54,14 @@ aioengine helps answer:
|
|
|
47
54
|
- Did AI change files outside the task?
|
|
48
55
|
- Did AI add or modify dependencies?
|
|
49
56
|
- Does this repo have AI coding rules?
|
|
50
|
-
- What should I review before committing?
|
|
57
|
+
- What should I review before committing or merging?
|
|
51
58
|
|
|
52
59
|
## `aioengine init`
|
|
53
60
|
|
|
54
61
|
Sets up aioengine in your repo.
|
|
55
62
|
|
|
63
|
+
Creates missing files only. aioengine will not overwrite an existing `CLAUDE.md`.
|
|
64
|
+
|
|
56
65
|
Creates:
|
|
57
66
|
|
|
58
67
|
```txt
|
|
@@ -61,6 +70,12 @@ CLAUDE.md
|
|
|
61
70
|
.cursor/rules/aioengine.mdc
|
|
62
71
|
```
|
|
63
72
|
|
|
73
|
+
If `CLAUDE.md` already exists, aioengine leaves it untouched and saves suggested rules to:
|
|
74
|
+
|
|
75
|
+
```txt
|
|
76
|
+
.aioengine/suggested-claude-rules.md
|
|
77
|
+
```
|
|
78
|
+
|
|
64
79
|
Run:
|
|
65
80
|
|
|
66
81
|
```bash
|
|
@@ -122,6 +137,52 @@ aioengine will flag changes to files that often deserve extra review, such as:
|
|
|
122
137
|
- dependency files
|
|
123
138
|
- GitHub workflow files
|
|
124
139
|
|
|
140
|
+
## `aioengine ci`
|
|
141
|
+
|
|
142
|
+
Runs aioengine checks in CI or pull request workflows.
|
|
143
|
+
|
|
144
|
+
Run:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npx aioengine ci --task "update landing page headline"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
In GitHub Actions, aioengine will try to detect changed files from the pull request context. If a task is available from the PR title, event payload, or `AIOENGINE_TASK`, it can flag possible scope drift.
|
|
151
|
+
|
|
152
|
+
By default:
|
|
153
|
+
|
|
154
|
+
- possible scope drift fails the CI check
|
|
155
|
+
- risky files warn but do not fail the CI check
|
|
156
|
+
|
|
157
|
+
Example GitHub Actions step:
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
- name: Run aioengine
|
|
161
|
+
run: npx aioengine ci
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
For more reliable PR diffs, use checkout with full history:
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
- uses: actions/checkout@v4
|
|
168
|
+
with:
|
|
169
|
+
fetch-depth: 0
|
|
170
|
+
|
|
171
|
+
- name: Run aioengine
|
|
172
|
+
run: npx aioengine ci
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
You can also pass a task manually:
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
- uses: actions/checkout@v4
|
|
179
|
+
with:
|
|
180
|
+
fetch-depth: 0
|
|
181
|
+
|
|
182
|
+
- name: Run aioengine
|
|
183
|
+
run: npx aioengine ci --task "update landing page headline"
|
|
184
|
+
```
|
|
185
|
+
|
|
125
186
|
## `aioengine rules`
|
|
126
187
|
|
|
127
188
|
Generates starter AI coding rules for Claude Code and Cursor.
|
|
@@ -132,11 +193,20 @@ Run:
|
|
|
132
193
|
npx aioengine rules
|
|
133
194
|
```
|
|
134
195
|
|
|
135
|
-
|
|
196
|
+
Creates missing files only. aioengine will not overwrite an existing `CLAUDE.md`.
|
|
197
|
+
|
|
198
|
+
If `CLAUDE.md` already exists, suggested Claude rules are saved to:
|
|
199
|
+
|
|
200
|
+
```txt
|
|
201
|
+
.aioengine/suggested-claude-rules.md
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
This command creates or skips:
|
|
136
205
|
|
|
137
206
|
```txt
|
|
138
207
|
CLAUDE.md
|
|
139
208
|
.cursor/rules/aioengine.mdc
|
|
209
|
+
.aioengine/suggested-claude-rules.md
|
|
140
210
|
```
|
|
141
211
|
|
|
142
212
|
## Example workflow
|
|
@@ -149,6 +219,7 @@ npx aioengine check
|
|
|
149
219
|
|
|
150
220
|
npx aioengine scope "update landing page headline"
|
|
151
221
|
npx aioengine review
|
|
222
|
+
npx aioengine ci --task "update landing page headline"
|
|
152
223
|
```
|
|
153
224
|
|
|
154
225
|
## Current status
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aioengine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "AI change control for developers using AI coding tools.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"review": "node ./src/index.js review",
|
|
10
10
|
"rules": "node ./src/index.js rules",
|
|
11
11
|
"scope": "node ./src/index.js scope",
|
|
12
|
-
"init": "node ./src/index.js init"
|
|
12
|
+
"init": "node ./src/index.js init",
|
|
13
|
+
"ci": "node ./src/index.js ci"
|
|
13
14
|
},
|
|
14
15
|
"keywords": [
|
|
15
16
|
"ai",
|
package/src/index.js
CHANGED
|
@@ -35,6 +35,12 @@ program
|
|
|
35
35
|
runReview();
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
program
|
|
39
|
+
.command("ci")
|
|
40
|
+
.description("Run aioengine review checks in CI and pull request workflows.")
|
|
41
|
+
.option("--task <task>", "Task description to compare changed files against")
|
|
42
|
+
.action((options) => runCi(options));
|
|
43
|
+
|
|
38
44
|
program
|
|
39
45
|
.command("scope")
|
|
40
46
|
.description("Check whether changed files match the requested task.")
|
|
@@ -275,6 +281,112 @@ function runReview() {
|
|
|
275
281
|
}
|
|
276
282
|
}
|
|
277
283
|
|
|
284
|
+
function runCi(options = {}) {
|
|
285
|
+
printHeader("aioengine CI");
|
|
286
|
+
|
|
287
|
+
const root = getProjectRoot();
|
|
288
|
+
|
|
289
|
+
if (!isInsideGitRepo()) {
|
|
290
|
+
console.log(
|
|
291
|
+
`${pc.red("✗")} No Git repo detected. aioengine ci must run inside a Git repository.`
|
|
292
|
+
);
|
|
293
|
+
process.exitCode = 1;
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const task = options.task || getCiTask();
|
|
298
|
+
const files = getCiChangedFiles(root);
|
|
299
|
+
|
|
300
|
+
console.log(`${pc.dim("Project:")} ${root}`);
|
|
301
|
+
|
|
302
|
+
if (isGitHubActions()) {
|
|
303
|
+
console.log(`${pc.dim("Environment:")} GitHub Actions`);
|
|
304
|
+
} else {
|
|
305
|
+
console.log(`${pc.dim("Environment:")} Local / unknown CI`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (task) {
|
|
309
|
+
console.log(`${pc.dim("Task:")} ${task}`);
|
|
310
|
+
} else {
|
|
311
|
+
console.log(
|
|
312
|
+
`${pc.yellow("!")} No task detected. Scope checks will be less precise.`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (files.length === 0) {
|
|
317
|
+
console.log(pc.green("\nNo changed files found."));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const profile = task ? inferTaskProfile(task) : null;
|
|
322
|
+
const riskyFiles = files.filter(isRiskyFile);
|
|
323
|
+
const outOfScopeFiles = profile
|
|
324
|
+
? files.filter((file) => isProbablyOutOfScope(file, profile))
|
|
325
|
+
: [];
|
|
326
|
+
|
|
327
|
+
if (profile) {
|
|
328
|
+
console.log(`${pc.dim("Detected task type:")} ${profile.label}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
console.log(`${pc.dim("Changed files:")} ${files.length}\n`);
|
|
332
|
+
|
|
333
|
+
for (const file of files) {
|
|
334
|
+
const risky = riskyFiles.includes(file);
|
|
335
|
+
const outOfScope = outOfScopeFiles.includes(file);
|
|
336
|
+
|
|
337
|
+
if (outOfScope) {
|
|
338
|
+
console.log(` ${pc.red("✗")} ${file} ${pc.red("— possible scope drift")}`);
|
|
339
|
+
} else if (risky) {
|
|
340
|
+
console.log(` ${pc.yellow("!")} ${file} ${pc.yellow("— review carefully")}`);
|
|
341
|
+
} else {
|
|
342
|
+
console.log(` ${pc.green("✓")} ${file}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const hasScopeDrift = outOfScopeFiles.length > 0;
|
|
347
|
+
const hasRiskyFiles = riskyFiles.length > 0;
|
|
348
|
+
|
|
349
|
+
if (hasScopeDrift) {
|
|
350
|
+
console.log(pc.yellow("\nCI review recommended"));
|
|
351
|
+
|
|
352
|
+
printSection(
|
|
353
|
+
"Possible scope drift",
|
|
354
|
+
outOfScopeFiles.map(
|
|
355
|
+
(file) => `Possible out-of-scope file changed: ${file}`
|
|
356
|
+
),
|
|
357
|
+
"warning"
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
if (hasRiskyFiles) {
|
|
361
|
+
printSection(
|
|
362
|
+
"Risky files",
|
|
363
|
+
riskyFiles.map((file) => `High-risk file changed: ${file}`),
|
|
364
|
+
"warning"
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
console.log(
|
|
369
|
+
pc.dim(
|
|
370
|
+
"\nRecommendation: Review these changes before merging. aioengine is failing this CI check because possible scope drift was detected."
|
|
371
|
+
)
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
process.exitCode = 1;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (hasRiskyFiles) {
|
|
379
|
+
console.log(
|
|
380
|
+
pc.yellow(
|
|
381
|
+
"\nRisky files were changed. aioengine is allowing this check to pass, but these files should receive extra human review."
|
|
382
|
+
)
|
|
383
|
+
);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
console.log(pc.green("\nNo obvious AI change-control issues detected."));
|
|
388
|
+
}
|
|
389
|
+
|
|
278
390
|
function runScope(task) {
|
|
279
391
|
printHeader("aioengine Scope");
|
|
280
392
|
|
|
@@ -932,6 +1044,98 @@ For UI-only tasks, avoid backend, API, database, and config changes.
|
|
|
932
1044
|
`;
|
|
933
1045
|
}
|
|
934
1046
|
|
|
1047
|
+
function isGitHubActions() {
|
|
1048
|
+
return process.env.GITHUB_ACTIONS === "true";
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function getCiTask() {
|
|
1052
|
+
const explicitTask = process.env.AIOENGINE_TASK;
|
|
1053
|
+
|
|
1054
|
+
if (explicitTask) {
|
|
1055
|
+
return explicitTask;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
1059
|
+
|
|
1060
|
+
if (!eventPath || !fs.existsSync(eventPath)) {
|
|
1061
|
+
return "";
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
try {
|
|
1065
|
+
const event = JSON.parse(fs.readFileSync(eventPath, "utf8"));
|
|
1066
|
+
|
|
1067
|
+
return (
|
|
1068
|
+
event.pull_request?.title ||
|
|
1069
|
+
event.issue?.title ||
|
|
1070
|
+
event.head_commit?.message ||
|
|
1071
|
+
""
|
|
1072
|
+
);
|
|
1073
|
+
} catch {
|
|
1074
|
+
return "";
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function getCiChangedFiles(root) {
|
|
1079
|
+
if (isGitHubActions()) {
|
|
1080
|
+
const files = getGitHubActionsChangedFiles(root);
|
|
1081
|
+
|
|
1082
|
+
if (files.length > 0) {
|
|
1083
|
+
return files;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
return getChangedFiles(root);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function getGitHubActionsChangedFiles(root) {
|
|
1091
|
+
const baseRef = process.env.GITHUB_BASE_REF;
|
|
1092
|
+
const beforeSha = process.env.GITHUB_EVENT_BEFORE;
|
|
1093
|
+
const currentSha = process.env.GITHUB_SHA || "HEAD";
|
|
1094
|
+
|
|
1095
|
+
try {
|
|
1096
|
+
if (baseRef) {
|
|
1097
|
+
try {
|
|
1098
|
+
execSync(`git fetch origin ${baseRef} --depth=1`, {
|
|
1099
|
+
cwd: root,
|
|
1100
|
+
stdio: "ignore",
|
|
1101
|
+
});
|
|
1102
|
+
} catch {
|
|
1103
|
+
// The workflow may already have enough history.
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
return uniqueFiles(
|
|
1107
|
+
execSync(`git diff --name-only origin/${baseRef}...HEAD`, {
|
|
1108
|
+
cwd: root,
|
|
1109
|
+
encoding: "utf8",
|
|
1110
|
+
})
|
|
1111
|
+
.split("\n")
|
|
1112
|
+
.map((file) => file.trim())
|
|
1113
|
+
.filter(Boolean)
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (beforeSha && currentSha) {
|
|
1118
|
+
return uniqueFiles(
|
|
1119
|
+
execSync(`git diff --name-only ${beforeSha} ${currentSha}`, {
|
|
1120
|
+
cwd: root,
|
|
1121
|
+
encoding: "utf8",
|
|
1122
|
+
})
|
|
1123
|
+
.split("\n")
|
|
1124
|
+
.map((file) => file.trim())
|
|
1125
|
+
.filter(Boolean)
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
} catch {
|
|
1129
|
+
return [];
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return [];
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function uniqueFiles(files) {
|
|
1136
|
+
return [...new Set(files)].sort();
|
|
1137
|
+
}
|
|
1138
|
+
|
|
935
1139
|
function getCliVersion() {
|
|
936
1140
|
try {
|
|
937
1141
|
const currentFile = fileURLToPath(import.meta.url);
|