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 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
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "../src/index.js";
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
+ }