aislop 0.5.1 → 0.6.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 +4 -4
- package/dist/cli.js +1136 -132
- package/dist/index.js +89 -14
- package/dist/{json-D_i2_5_-.js → json-DcE9soYJ.js} +1 -1
- package/dist/{version-CIlgPf8Q.js → version-C2lM_2fE.js} +1 -1
- package/package.json +4 -4
- package/scripts/postinstall-tools.mjs +2 -2
package/dist/cli.js
CHANGED
|
@@ -1026,7 +1026,49 @@ const MEANINGFUL_JSDOC_TAGS = new Set([
|
|
|
1026
1026
|
"todo",
|
|
1027
1027
|
"link",
|
|
1028
1028
|
"license",
|
|
1029
|
-
"preserve"
|
|
1029
|
+
"preserve",
|
|
1030
|
+
"swagger",
|
|
1031
|
+
"openapi",
|
|
1032
|
+
"route",
|
|
1033
|
+
"group",
|
|
1034
|
+
"summary",
|
|
1035
|
+
"description",
|
|
1036
|
+
"operationid",
|
|
1037
|
+
"response",
|
|
1038
|
+
"responses",
|
|
1039
|
+
"request",
|
|
1040
|
+
"requestbody",
|
|
1041
|
+
"security",
|
|
1042
|
+
"tag",
|
|
1043
|
+
"tags",
|
|
1044
|
+
"path",
|
|
1045
|
+
"body",
|
|
1046
|
+
"query",
|
|
1047
|
+
"queryparam",
|
|
1048
|
+
"header",
|
|
1049
|
+
"headers",
|
|
1050
|
+
"produces",
|
|
1051
|
+
"accept",
|
|
1052
|
+
"middleware",
|
|
1053
|
+
"api",
|
|
1054
|
+
"apiname",
|
|
1055
|
+
"apidefine",
|
|
1056
|
+
"apigroup",
|
|
1057
|
+
"apiparam",
|
|
1058
|
+
"apiquery",
|
|
1059
|
+
"apibody",
|
|
1060
|
+
"apiheader",
|
|
1061
|
+
"apisuccess",
|
|
1062
|
+
"apierror",
|
|
1063
|
+
"apiexample",
|
|
1064
|
+
"apiversion",
|
|
1065
|
+
"apidescription",
|
|
1066
|
+
"apipermission",
|
|
1067
|
+
"apiuse",
|
|
1068
|
+
"apiignore",
|
|
1069
|
+
"apiprivate",
|
|
1070
|
+
"namespace",
|
|
1071
|
+
"category"
|
|
1030
1072
|
]);
|
|
1031
1073
|
const SUPPORTED_EXTS = new Set([
|
|
1032
1074
|
".ts",
|
|
@@ -4657,7 +4699,7 @@ const discoverProject = async (directory) => {
|
|
|
4657
4699
|
//#region src/utils/git.ts
|
|
4658
4700
|
const MAX_BUFFER = 50 * 1024 * 1024;
|
|
4659
4701
|
const getChangedFiles = (cwd, base) => {
|
|
4660
|
-
const
|
|
4702
|
+
const diff = spawnSync("git", [
|
|
4661
4703
|
"diff",
|
|
4662
4704
|
"--name-only",
|
|
4663
4705
|
"--diff-filter=ACMR",
|
|
@@ -4667,8 +4709,22 @@ const getChangedFiles = (cwd, base) => {
|
|
|
4667
4709
|
encoding: "utf-8",
|
|
4668
4710
|
maxBuffer: MAX_BUFFER
|
|
4669
4711
|
});
|
|
4670
|
-
if (
|
|
4671
|
-
|
|
4712
|
+
if (diff.error || diff.status !== 0) return [];
|
|
4713
|
+
const untracked = spawnSync("git", [
|
|
4714
|
+
"ls-files",
|
|
4715
|
+
"--others",
|
|
4716
|
+
"--exclude-standard"
|
|
4717
|
+
], {
|
|
4718
|
+
cwd,
|
|
4719
|
+
encoding: "utf-8",
|
|
4720
|
+
maxBuffer: MAX_BUFFER
|
|
4721
|
+
});
|
|
4722
|
+
const names = /* @__PURE__ */ new Set();
|
|
4723
|
+
for (const line of diff.stdout.split("\n")) if (line.length > 0) names.add(line);
|
|
4724
|
+
if (!untracked.error && untracked.status === 0) {
|
|
4725
|
+
for (const line of untracked.stdout.split("\n")) if (line.length > 0) names.add(line);
|
|
4726
|
+
}
|
|
4727
|
+
return Array.from(names).map((f) => path.resolve(cwd, f));
|
|
4672
4728
|
};
|
|
4673
4729
|
const getStagedFiles = (cwd) => {
|
|
4674
4730
|
const result = spawnSync("git", [
|
|
@@ -4691,7 +4747,7 @@ const getStagedFiles = (cwd) => {
|
|
|
4691
4747
|
* Application version — injected at build time by tsdown from package.json.
|
|
4692
4748
|
* The fallback should always match the "version" field in package.json.
|
|
4693
4749
|
*/
|
|
4694
|
-
const APP_VERSION = "0.
|
|
4750
|
+
const APP_VERSION = "0.6.0";
|
|
4695
4751
|
|
|
4696
4752
|
//#endregion
|
|
4697
4753
|
//#region src/utils/telemetry.ts
|
|
@@ -6305,7 +6361,13 @@ const fixDependencyAudit = async (context, onProgress) => {
|
|
|
6305
6361
|
await tryNpmOverrides(context.rootDirectory, onProgress);
|
|
6306
6362
|
return;
|
|
6307
6363
|
}
|
|
6308
|
-
await tryPnpmOverrides(context.rootDirectory, onProgress);
|
|
6364
|
+
if (await tryPnpmOverrides(context.rootDirectory, onProgress)) return;
|
|
6365
|
+
if (fs.existsSync(path.join(context.rootDirectory, "package-lock.json"))) {
|
|
6366
|
+
await runNpmAuditFix(context.rootDirectory, onProgress);
|
|
6367
|
+
await tryNpmOverrides(context.rootDirectory, onProgress);
|
|
6368
|
+
return;
|
|
6369
|
+
}
|
|
6370
|
+
onProgress?.("Dependency audit fixes · skipping (pnpm audit unavailable and no package-lock.json for npm fallback)");
|
|
6309
6371
|
};
|
|
6310
6372
|
const runNpmAuditFix = async (rootDir, onProgress) => {
|
|
6311
6373
|
onProgress?.("Dependency audit fixes · running npm audit fix (can take a few minutes)");
|
|
@@ -6337,11 +6399,11 @@ const fetchLatestVersion = async (rootDir, pkgName, pm) => {
|
|
|
6337
6399
|
return null;
|
|
6338
6400
|
}
|
|
6339
6401
|
};
|
|
6340
|
-
const
|
|
6402
|
+
const collectOverrides = async (rootDir, vulnerabilities, pm) => {
|
|
6341
6403
|
const overrides = {};
|
|
6342
6404
|
for (const [pkgName, vuln] of Object.entries(vulnerabilities)) {
|
|
6343
6405
|
if (vuln.fixAvailable !== false || !vuln.range) continue;
|
|
6344
|
-
const latest = await fetchLatestVersion(rootDir, pkgName,
|
|
6406
|
+
const latest = await fetchLatestVersion(rootDir, pkgName, pm);
|
|
6345
6407
|
if (latest) overrides[pkgName] = latest;
|
|
6346
6408
|
}
|
|
6347
6409
|
return overrides;
|
|
@@ -6355,7 +6417,7 @@ const tryNpmOverrides = async (rootDir, onProgress) => {
|
|
|
6355
6417
|
if (!auditResult.stdout) return;
|
|
6356
6418
|
const vulnerabilities = JSON.parse(auditResult.stdout).vulnerabilities;
|
|
6357
6419
|
if (!vulnerabilities) return;
|
|
6358
|
-
const overrides = await
|
|
6420
|
+
const overrides = await collectOverrides(rootDir, vulnerabilities, "npm");
|
|
6359
6421
|
if (Object.keys(overrides).length === 0) return;
|
|
6360
6422
|
const pkgPath = path.join(rootDir, "package.json");
|
|
6361
6423
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
@@ -6391,23 +6453,31 @@ const collectPnpmOverrides = (advisories) => {
|
|
|
6391
6453
|
}
|
|
6392
6454
|
return overrides;
|
|
6393
6455
|
};
|
|
6456
|
+
const isPnpmAuditRetired = (stdout, stderr) => {
|
|
6457
|
+
const haystack = `${stdout}\n${stderr}`.toLowerCase();
|
|
6458
|
+
return haystack.includes("410") || haystack.includes("gone") || haystack.includes("retired") || haystack.includes("endpoint") || haystack.includes("err_pnpm_audit") || haystack.includes("audit endpoint");
|
|
6459
|
+
};
|
|
6394
6460
|
const tryPnpmOverrides = async (rootDir, onProgress) => {
|
|
6395
6461
|
onProgress?.("Dependency audit fixes · running pnpm audit");
|
|
6396
6462
|
const auditResult = await runSubprocess("pnpm", ["audit", "--json"], {
|
|
6397
6463
|
cwd: rootDir,
|
|
6398
6464
|
timeout: AUDIT_TIMEOUT
|
|
6399
6465
|
});
|
|
6400
|
-
if (!auditResult.stdout)
|
|
6466
|
+
if (!auditResult.stdout) {
|
|
6467
|
+
if (isPnpmAuditRetired(auditResult.stdout ?? "", auditResult.stderr ?? "")) return false;
|
|
6468
|
+
return auditResult.exitCode === 0;
|
|
6469
|
+
}
|
|
6401
6470
|
let parsed;
|
|
6402
6471
|
try {
|
|
6403
6472
|
parsed = JSON.parse(auditResult.stdout);
|
|
6404
6473
|
} catch {
|
|
6405
|
-
return;
|
|
6474
|
+
if (auditResult.exitCode !== 0 || isPnpmAuditRetired(auditResult.stdout, auditResult.stderr ?? "")) return false;
|
|
6475
|
+
return true;
|
|
6406
6476
|
}
|
|
6407
6477
|
const advisories = parsed.advisories;
|
|
6408
|
-
if (!advisories || Object.keys(advisories).length === 0) return;
|
|
6478
|
+
if (!advisories || Object.keys(advisories).length === 0) return true;
|
|
6409
6479
|
const overrides = collectPnpmOverrides(advisories);
|
|
6410
|
-
if (Object.keys(overrides).length === 0) return;
|
|
6480
|
+
if (Object.keys(overrides).length === 0) return true;
|
|
6411
6481
|
const pkgPath = path.join(rootDir, "package.json");
|
|
6412
6482
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
6413
6483
|
const pnpmBlock = pkg.pnpm ?? {};
|
|
@@ -6425,6 +6495,7 @@ const tryPnpmOverrides = async (rootDir, onProgress) => {
|
|
|
6425
6495
|
cwd: rootDir,
|
|
6426
6496
|
timeout: INSTALL_TIMEOUT
|
|
6427
6497
|
});
|
|
6498
|
+
return true;
|
|
6428
6499
|
};
|
|
6429
6500
|
const fixExpoDependencies = async (context, onProgress) => {
|
|
6430
6501
|
await removeDisallowedExpoPackages(context.rootDirectory, onProgress);
|
|
@@ -6450,6 +6521,10 @@ const fixExpoDependencies = async (context, onProgress) => {
|
|
|
6450
6521
|
});
|
|
6451
6522
|
if (checkResult.exitCode !== 0) throw new Error(checkResult.stderr || checkResult.stdout || "expo dependency check failed");
|
|
6452
6523
|
};
|
|
6524
|
+
/**
|
|
6525
|
+
* Run expo-doctor to detect packages that should not be installed directly,
|
|
6526
|
+
* then uninstall them. No hardcoded list — expo-doctor is the source of truth.
|
|
6527
|
+
*/
|
|
6453
6528
|
const removeDisallowedExpoPackages = async (rootDir, onProgress) => {
|
|
6454
6529
|
try {
|
|
6455
6530
|
onProgress?.("Expo dependency alignment · running expo-doctor");
|
|
@@ -6757,30 +6832,43 @@ const buildFeedback = (diagnostics, score, rootDirectory, baseline) => {
|
|
|
6757
6832
|
};
|
|
6758
6833
|
|
|
6759
6834
|
//#endregion
|
|
6760
|
-
//#region src/hooks/
|
|
6761
|
-
const
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6835
|
+
//#region src/hooks/io/scan-lock.ts
|
|
6836
|
+
const LOCK_DIR = ".aislop";
|
|
6837
|
+
const LOCK_FILE = "hook.lock";
|
|
6838
|
+
const STALE_MS = 3e4;
|
|
6839
|
+
const lockPath = (cwd) => path.join(cwd, LOCK_DIR, LOCK_FILE);
|
|
6840
|
+
const readLock = (target) => {
|
|
6841
|
+
try {
|
|
6842
|
+
const raw = fs.readFileSync(target, "utf-8");
|
|
6843
|
+
const parsed = JSON.parse(raw);
|
|
6844
|
+
if (typeof parsed.pid !== "number" || typeof parsed.ts !== "number") return null;
|
|
6845
|
+
return parsed;
|
|
6846
|
+
} catch {
|
|
6847
|
+
return null;
|
|
6767
6848
|
}
|
|
6768
|
-
return Array.from(files);
|
|
6769
6849
|
};
|
|
6770
|
-
const
|
|
6771
|
-
|
|
6850
|
+
const acquireHookLock = (cwd) => {
|
|
6851
|
+
const target = lockPath(cwd);
|
|
6852
|
+
const existing = readLock(target);
|
|
6853
|
+
if (existing && Date.now() - existing.ts < STALE_MS) return null;
|
|
6772
6854
|
try {
|
|
6773
|
-
|
|
6855
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
6856
|
+
fs.writeFileSync(target, JSON.stringify({
|
|
6857
|
+
pid: process.pid,
|
|
6858
|
+
ts: Date.now()
|
|
6859
|
+
}));
|
|
6774
6860
|
} catch {
|
|
6775
|
-
return
|
|
6861
|
+
return null;
|
|
6776
6862
|
}
|
|
6863
|
+
return () => {
|
|
6864
|
+
try {
|
|
6865
|
+
if (readLock(target)?.pid === process.pid) fs.unlinkSync(target);
|
|
6866
|
+
} catch {}
|
|
6867
|
+
};
|
|
6777
6868
|
};
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
6782
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
6783
|
-
};
|
|
6869
|
+
|
|
6870
|
+
//#endregion
|
|
6871
|
+
//#region src/hooks/io/scoped-scan.ts
|
|
6784
6872
|
const existingAbsolutePaths = (cwd, files) => files.map((f) => path.isAbsolute(f) ? f : path.join(cwd, f)).filter((p) => {
|
|
6785
6873
|
try {
|
|
6786
6874
|
return fs.statSync(p).isFile();
|
|
@@ -6788,6 +6876,11 @@ const existingAbsolutePaths = (cwd, files) => files.map((f) => path.isAbsolute(f
|
|
|
6788
6876
|
return false;
|
|
6789
6877
|
}
|
|
6790
6878
|
});
|
|
6879
|
+
const resolveHookFiles = (cwd, files) => {
|
|
6880
|
+
const direct = existingAbsolutePaths(cwd, files);
|
|
6881
|
+
if (direct.length > 0) return direct;
|
|
6882
|
+
return existingAbsolutePaths(cwd, getChangedFiles(cwd));
|
|
6883
|
+
};
|
|
6791
6884
|
const runScopedScan = async (cwd, filePaths) => {
|
|
6792
6885
|
const project = await discoverProject(cwd);
|
|
6793
6886
|
const config = loadConfig(cwd);
|
|
@@ -6822,6 +6915,146 @@ const runScopedScan = async (cwd, filePaths) => {
|
|
|
6822
6915
|
rootDirectory: project.rootDirectory
|
|
6823
6916
|
};
|
|
6824
6917
|
};
|
|
6918
|
+
|
|
6919
|
+
//#endregion
|
|
6920
|
+
//#region src/hooks/io/atomic-write.ts
|
|
6921
|
+
const atomicWrite = (targetPath, content) => {
|
|
6922
|
+
const dir = path.dirname(targetPath);
|
|
6923
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
6924
|
+
const rand = Math.random().toString(36).slice(2, 10);
|
|
6925
|
+
const tmp = path.join(dir, `.aislop-tmp-${process.pid}-${rand}`);
|
|
6926
|
+
fs.writeFileSync(tmp, content, "utf-8");
|
|
6927
|
+
fs.renameSync(tmp, targetPath);
|
|
6928
|
+
};
|
|
6929
|
+
const readIfExists = (targetPath) => {
|
|
6930
|
+
try {
|
|
6931
|
+
return fs.readFileSync(targetPath, "utf-8");
|
|
6932
|
+
} catch {
|
|
6933
|
+
return null;
|
|
6934
|
+
}
|
|
6935
|
+
};
|
|
6936
|
+
|
|
6937
|
+
//#endregion
|
|
6938
|
+
//#region src/hooks/quality-gate/baseline.ts
|
|
6939
|
+
const BASELINE_REL = path.join(".aislop", "baseline.json");
|
|
6940
|
+
const baselinePath = (cwd) => path.join(cwd, BASELINE_REL);
|
|
6941
|
+
const readBaseline = (cwd) => {
|
|
6942
|
+
const raw = readIfExists(baselinePath(cwd));
|
|
6943
|
+
if (!raw) return null;
|
|
6944
|
+
try {
|
|
6945
|
+
const parsed = JSON.parse(raw);
|
|
6946
|
+
if (parsed.schema !== "aislop.baseline.v1") return null;
|
|
6947
|
+
return parsed;
|
|
6948
|
+
} catch {
|
|
6949
|
+
return null;
|
|
6950
|
+
}
|
|
6951
|
+
};
|
|
6952
|
+
const writeBaseline = (cwd, baseline) => {
|
|
6953
|
+
const target = baselinePath(cwd);
|
|
6954
|
+
atomicWrite(target, `${JSON.stringify(baseline, null, 2)}\n`);
|
|
6955
|
+
return target;
|
|
6956
|
+
};
|
|
6957
|
+
const captureBaseline = async (cwd) => {
|
|
6958
|
+
const project = await discoverProject(cwd);
|
|
6959
|
+
const config = loadConfig(cwd);
|
|
6960
|
+
const results = await runEngines({
|
|
6961
|
+
rootDirectory: project.rootDirectory,
|
|
6962
|
+
languages: project.languages,
|
|
6963
|
+
frameworks: project.frameworks,
|
|
6964
|
+
files: [],
|
|
6965
|
+
installedTools: project.installedTools,
|
|
6966
|
+
config: {
|
|
6967
|
+
quality: config.quality,
|
|
6968
|
+
security: {
|
|
6969
|
+
audit: false,
|
|
6970
|
+
auditTimeout: 0
|
|
6971
|
+
}
|
|
6972
|
+
}
|
|
6973
|
+
}, {
|
|
6974
|
+
format: config.engines.format,
|
|
6975
|
+
lint: config.engines.lint,
|
|
6976
|
+
"code-quality": config.engines["code-quality"],
|
|
6977
|
+
"ai-slop": config.engines["ai-slop"],
|
|
6978
|
+
architecture: config.engines.architecture,
|
|
6979
|
+
security: false
|
|
6980
|
+
});
|
|
6981
|
+
const diagnostics = results.flatMap((r) => r.diagnostics);
|
|
6982
|
+
const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
|
|
6983
|
+
const byEngine = {};
|
|
6984
|
+
for (const r of results) {
|
|
6985
|
+
const { score: engineScore } = calculateScore(diagnostics.filter((d) => r.diagnostics.includes(d)), config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
|
|
6986
|
+
byEngine[r.engine] = engineScore;
|
|
6987
|
+
}
|
|
6988
|
+
const target = writeBaseline(cwd, {
|
|
6989
|
+
schema: "aislop.baseline.v1",
|
|
6990
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6991
|
+
score,
|
|
6992
|
+
byEngine,
|
|
6993
|
+
fileCount: project.sourceFileCount
|
|
6994
|
+
});
|
|
6995
|
+
return {
|
|
6996
|
+
score,
|
|
6997
|
+
fileCount: project.sourceFileCount,
|
|
6998
|
+
path: target
|
|
6999
|
+
};
|
|
7000
|
+
};
|
|
7001
|
+
const appendSessionFiles = (cwd, files) => {
|
|
7002
|
+
if (files.length === 0) return;
|
|
7003
|
+
const target = path.join(cwd, ".aislop", "session.jsonl");
|
|
7004
|
+
try {
|
|
7005
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
7006
|
+
const line = `${JSON.stringify({
|
|
7007
|
+
ts: Date.now(),
|
|
7008
|
+
files
|
|
7009
|
+
})}\n`;
|
|
7010
|
+
fs.appendFileSync(target, line);
|
|
7011
|
+
} catch {}
|
|
7012
|
+
};
|
|
7013
|
+
const readSessionFiles = (cwd) => {
|
|
7014
|
+
const raw = readIfExists(path.join(cwd, ".aislop", "session.jsonl"));
|
|
7015
|
+
if (!raw) return [];
|
|
7016
|
+
const files = /* @__PURE__ */ new Set();
|
|
7017
|
+
for (const line of raw.split("\n")) {
|
|
7018
|
+
if (!line.trim()) continue;
|
|
7019
|
+
try {
|
|
7020
|
+
const entry = JSON.parse(line);
|
|
7021
|
+
for (const f of entry.files ?? []) files.add(f);
|
|
7022
|
+
} catch {}
|
|
7023
|
+
}
|
|
7024
|
+
return Array.from(files);
|
|
7025
|
+
};
|
|
7026
|
+
const clearSessionFiles = (cwd) => {
|
|
7027
|
+
const target = path.join(cwd, ".aislop", "session.jsonl");
|
|
7028
|
+
try {
|
|
7029
|
+
fs.unlinkSync(target);
|
|
7030
|
+
} catch {}
|
|
7031
|
+
};
|
|
7032
|
+
|
|
7033
|
+
//#endregion
|
|
7034
|
+
//#region src/hooks/adapters/claude.ts
|
|
7035
|
+
const extractFiles$2 = (stdin) => {
|
|
7036
|
+
const files = /* @__PURE__ */ new Set();
|
|
7037
|
+
const input = stdin.tool_input ?? {};
|
|
7038
|
+
if (typeof input.file_path === "string" && input.file_path.length > 0) files.add(input.file_path);
|
|
7039
|
+
if (Array.isArray(input.edits)) {
|
|
7040
|
+
for (const e of input.edits) if (e && typeof e.file_path === "string" && e.file_path.length > 0) files.add(e.file_path);
|
|
7041
|
+
}
|
|
7042
|
+
return Array.from(files);
|
|
7043
|
+
};
|
|
7044
|
+
const parseClaudeStdin = (raw) => {
|
|
7045
|
+
if (!raw.trim()) return {};
|
|
7046
|
+
try {
|
|
7047
|
+
return JSON.parse(raw);
|
|
7048
|
+
} catch {
|
|
7049
|
+
return {};
|
|
7050
|
+
}
|
|
7051
|
+
};
|
|
7052
|
+
const readStdin$2 = async () => {
|
|
7053
|
+
if (process.stdin.isTTY) return "";
|
|
7054
|
+
const chunks = [];
|
|
7055
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7056
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
7057
|
+
};
|
|
6825
7058
|
const renderClaudeOutput = (additional, block) => {
|
|
6826
7059
|
const out = { hookSpecificOutput: {
|
|
6827
7060
|
hookEventName: "PostToolUse",
|
|
@@ -6834,20 +7067,167 @@ const renderClaudeOutput = (additional, block) => {
|
|
|
6834
7067
|
return out;
|
|
6835
7068
|
};
|
|
6836
7069
|
const runClaudeHook = async (deps = {}) => {
|
|
6837
|
-
const getStdin = deps.stdin ?? readStdin;
|
|
7070
|
+
const getStdin = deps.stdin ?? readStdin$2;
|
|
6838
7071
|
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
6839
7072
|
const input = parseClaudeStdin(await getStdin());
|
|
6840
7073
|
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
6841
|
-
const
|
|
6842
|
-
if (
|
|
7074
|
+
const files = resolveHookFiles(cwd, extractFiles$2(input));
|
|
7075
|
+
if (files.length === 0) return 0;
|
|
7076
|
+
const release = acquireHookLock(cwd);
|
|
7077
|
+
if (!release) return 0;
|
|
6843
7078
|
try {
|
|
6844
|
-
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd,
|
|
6845
|
-
const
|
|
7079
|
+
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
|
|
7080
|
+
const baseline = readBaseline(cwd);
|
|
7081
|
+
appendSessionFiles(cwd, files);
|
|
7082
|
+
const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline?.score);
|
|
6846
7083
|
const envelope = renderClaudeOutput(JSON.stringify(feedback));
|
|
6847
7084
|
write(JSON.stringify(envelope));
|
|
6848
7085
|
return 0;
|
|
6849
7086
|
} catch {
|
|
6850
7087
|
return 0;
|
|
7088
|
+
} finally {
|
|
7089
|
+
release();
|
|
7090
|
+
}
|
|
7091
|
+
};
|
|
7092
|
+
const parseClaudeStopStdin = (raw) => {
|
|
7093
|
+
if (!raw.trim()) return {};
|
|
7094
|
+
try {
|
|
7095
|
+
return JSON.parse(raw);
|
|
7096
|
+
} catch {
|
|
7097
|
+
return {};
|
|
7098
|
+
}
|
|
7099
|
+
};
|
|
7100
|
+
const runClaudeStopHook = async (deps = {}) => {
|
|
7101
|
+
const getStdin = deps.stdin ?? readStdin$2;
|
|
7102
|
+
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
7103
|
+
const input = parseClaudeStopStdin(await getStdin());
|
|
7104
|
+
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
7105
|
+
if (input.stop_hook_active) return 0;
|
|
7106
|
+
const baseline = readBaseline(cwd);
|
|
7107
|
+
if (!baseline) return 0;
|
|
7108
|
+
const sessionFiles = readSessionFiles(cwd);
|
|
7109
|
+
if (sessionFiles.length === 0) return 0;
|
|
7110
|
+
const release = acquireHookLock(cwd);
|
|
7111
|
+
if (!release) return 0;
|
|
7112
|
+
try {
|
|
7113
|
+
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, sessionFiles);
|
|
7114
|
+
const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline.score);
|
|
7115
|
+
if (!feedback.regressed) {
|
|
7116
|
+
clearSessionFiles(cwd);
|
|
7117
|
+
return 0;
|
|
7118
|
+
}
|
|
7119
|
+
const envelope = renderClaudeOutput(JSON.stringify(feedback), { reason: `aislop: score dropped from ${baseline.score} to ${score}. Fix the ${feedback.counts.total} finding${feedback.counts.total === 1 ? "" : "s"} before finishing.` });
|
|
7120
|
+
write(JSON.stringify(envelope));
|
|
7121
|
+
return 0;
|
|
7122
|
+
} catch {
|
|
7123
|
+
return 0;
|
|
7124
|
+
} finally {
|
|
7125
|
+
release();
|
|
7126
|
+
}
|
|
7127
|
+
};
|
|
7128
|
+
|
|
7129
|
+
//#endregion
|
|
7130
|
+
//#region src/hooks/adapters/cursor.ts
|
|
7131
|
+
const extractFiles$1 = (stdin) => {
|
|
7132
|
+
const files = /* @__PURE__ */ new Set();
|
|
7133
|
+
if (typeof stdin.file_path === "string" && stdin.file_path.length > 0) files.add(stdin.file_path);
|
|
7134
|
+
if (Array.isArray(stdin.edits)) {
|
|
7135
|
+
for (const e of stdin.edits) if (e && typeof e.file_path === "string" && e.file_path.length > 0) files.add(e.file_path);
|
|
7136
|
+
}
|
|
7137
|
+
const input = stdin.tool_input ?? {};
|
|
7138
|
+
if (typeof input.file_path === "string" && input.file_path.length > 0) files.add(input.file_path);
|
|
7139
|
+
if (Array.isArray(input.edits)) {
|
|
7140
|
+
for (const e of input.edits) if (e && typeof e.file_path === "string" && e.file_path.length > 0) files.add(e.file_path);
|
|
7141
|
+
}
|
|
7142
|
+
return Array.from(files);
|
|
7143
|
+
};
|
|
7144
|
+
const parseCursorStdin = (raw) => {
|
|
7145
|
+
if (!raw.trim()) return {};
|
|
7146
|
+
try {
|
|
7147
|
+
return JSON.parse(raw);
|
|
7148
|
+
} catch {
|
|
7149
|
+
return {};
|
|
7150
|
+
}
|
|
7151
|
+
};
|
|
7152
|
+
const renderCursorOutput = (additional, event = "afterFileEdit") => ({ hookSpecificOutput: {
|
|
7153
|
+
hookEventName: event,
|
|
7154
|
+
additionalContext: additional
|
|
7155
|
+
} });
|
|
7156
|
+
const readStdin$1 = async () => {
|
|
7157
|
+
if (process.stdin.isTTY) return "";
|
|
7158
|
+
const chunks = [];
|
|
7159
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7160
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
7161
|
+
};
|
|
7162
|
+
const runCursorHook = async (deps = {}) => {
|
|
7163
|
+
const getStdin = deps.stdin ?? readStdin$1;
|
|
7164
|
+
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
7165
|
+
const writeErr = deps.writeErr ?? ((s) => process.stderr.write(s));
|
|
7166
|
+
const input = parseCursorStdin(await getStdin());
|
|
7167
|
+
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
7168
|
+
const files = resolveHookFiles(cwd, extractFiles$1(input));
|
|
7169
|
+
if (files.length === 0) return 0;
|
|
7170
|
+
const release = acquireHookLock(cwd);
|
|
7171
|
+
if (!release) return 0;
|
|
7172
|
+
try {
|
|
7173
|
+
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
|
|
7174
|
+
const feedback = buildFeedback(diagnostics, score, rootDirectory);
|
|
7175
|
+
const serialized = JSON.stringify(feedback);
|
|
7176
|
+
write(JSON.stringify(renderCursorOutput(serialized)));
|
|
7177
|
+
writeErr(`${serialized}\n`);
|
|
7178
|
+
return 0;
|
|
7179
|
+
} catch {
|
|
7180
|
+
return 0;
|
|
7181
|
+
} finally {
|
|
7182
|
+
release();
|
|
7183
|
+
}
|
|
7184
|
+
};
|
|
7185
|
+
|
|
7186
|
+
//#endregion
|
|
7187
|
+
//#region src/hooks/adapters/gemini.ts
|
|
7188
|
+
const extractFiles = (stdin) => {
|
|
7189
|
+
const files = /* @__PURE__ */ new Set();
|
|
7190
|
+
const input = stdin.tool_input ?? {};
|
|
7191
|
+
if (typeof input.file_path === "string" && input.file_path.length > 0) files.add(input.file_path);
|
|
7192
|
+
if (typeof input.path === "string" && input.path.length > 0) files.add(input.path);
|
|
7193
|
+
return Array.from(files);
|
|
7194
|
+
};
|
|
7195
|
+
const parseGeminiStdin = (raw) => {
|
|
7196
|
+
if (!raw.trim()) return {};
|
|
7197
|
+
try {
|
|
7198
|
+
return JSON.parse(raw);
|
|
7199
|
+
} catch {
|
|
7200
|
+
return {};
|
|
7201
|
+
}
|
|
7202
|
+
};
|
|
7203
|
+
const renderGeminiOutput = (additional) => ({ hookSpecificOutput: {
|
|
7204
|
+
hookEventName: "AfterTool",
|
|
7205
|
+
additionalContext: additional
|
|
7206
|
+
} });
|
|
7207
|
+
const readStdin = async () => {
|
|
7208
|
+
if (process.stdin.isTTY) return "";
|
|
7209
|
+
const chunks = [];
|
|
7210
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7211
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
7212
|
+
};
|
|
7213
|
+
const runGeminiHook = async (deps = {}) => {
|
|
7214
|
+
const getStdin = deps.stdin ?? readStdin;
|
|
7215
|
+
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
7216
|
+
const input = parseGeminiStdin(await getStdin());
|
|
7217
|
+
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
7218
|
+
const files = resolveHookFiles(cwd, extractFiles(input));
|
|
7219
|
+
if (files.length === 0) return 0;
|
|
7220
|
+
const release = acquireHookLock(cwd);
|
|
7221
|
+
if (!release) return 0;
|
|
7222
|
+
try {
|
|
7223
|
+
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
|
|
7224
|
+
const feedback = buildFeedback(diagnostics, score, rootDirectory);
|
|
7225
|
+
write(JSON.stringify(renderGeminiOutput(JSON.stringify(feedback))));
|
|
7226
|
+
return 0;
|
|
7227
|
+
} catch {
|
|
7228
|
+
return 0;
|
|
7229
|
+
} finally {
|
|
7230
|
+
release();
|
|
6851
7231
|
}
|
|
6852
7232
|
};
|
|
6853
7233
|
|
|
@@ -6855,7 +7235,7 @@ const runClaudeHook = async (deps = {}) => {
|
|
|
6855
7235
|
//#region src/hooks/assets.ts
|
|
6856
7236
|
const AISLOP_MD_BODY = `# aislop — agent instructions
|
|
6857
7237
|
|
|
6858
|
-
[aislop](https://github.com/
|
|
7238
|
+
[aislop](https://github.com/scanaislop/aislop) is your guardrail for shipping clean, maintainable, production-grade code. It catches the patterns AI coding tools leave behind: dead imports, redundant code, duplicate logic, unsafe any-typed casts, narrative comments, swallowed errors, oversized functions, risky security constructs — and grades the project out of 100 so regressions are visible. Treat its findings as blocking, not advisory: the goal is code a human maintainer would happily inherit.
|
|
6859
7239
|
|
|
6860
7240
|
## On every edit
|
|
6861
7241
|
|
|
@@ -6881,21 +7261,129 @@ A PostToolUse hook runs \`aislop hook claude\` after every Edit, Write, or Multi
|
|
|
6881
7261
|
`;
|
|
6882
7262
|
|
|
6883
7263
|
//#endregion
|
|
6884
|
-
//#region src/hooks/io/
|
|
6885
|
-
const
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
7264
|
+
//#region src/hooks/io/sentinel.ts
|
|
7265
|
+
const sentinelHash = (content) => `sha256:${crypto.createHash("sha256").update(content).digest("hex").slice(0, 32)}`;
|
|
7266
|
+
const BEGIN_RE = /<!--\s*aislop:begin\s+v(\d+)(?:\s+hash=([^\s>]+))?\s*-->/;
|
|
7267
|
+
const END_RE = /<!--\s*aislop:end\s+v\d+\s*-->/;
|
|
7268
|
+
const renderFence = (body, hash) => [
|
|
7269
|
+
`<!-- aislop:begin v1 hash=${hash} -->`,
|
|
7270
|
+
body.trimEnd(),
|
|
7271
|
+
"<!-- aislop:end v1 -->"
|
|
7272
|
+
].join("\n");
|
|
7273
|
+
const upsertMarkdownFence = (existing, body, hash) => {
|
|
7274
|
+
const fenced = renderFence(body, hash);
|
|
7275
|
+
if (existing == null || existing.length === 0) return {
|
|
7276
|
+
nextContent: `${fenced}\n`,
|
|
7277
|
+
replaced: false
|
|
7278
|
+
};
|
|
7279
|
+
const begin = existing.match(BEGIN_RE);
|
|
7280
|
+
const end = existing.match(END_RE);
|
|
7281
|
+
if (begin && end && (end.index ?? 0) > (begin.index ?? 0)) return {
|
|
7282
|
+
nextContent: `${existing.slice(0, begin.index)}${fenced}${existing.slice((end.index ?? 0) + end[0].length)}`,
|
|
7283
|
+
replaced: true
|
|
7284
|
+
};
|
|
7285
|
+
return {
|
|
7286
|
+
nextContent: `${existing}${existing.endsWith("\n") ? "" : "\n"}\n${fenced}\n`,
|
|
7287
|
+
replaced: false
|
|
7288
|
+
};
|
|
6892
7289
|
};
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
7290
|
+
|
|
7291
|
+
//#endregion
|
|
7292
|
+
//#region src/hooks/install/types.ts
|
|
7293
|
+
const emptyResult = () => ({
|
|
7294
|
+
wrote: [],
|
|
7295
|
+
skipped: [],
|
|
7296
|
+
planned: []
|
|
7297
|
+
});
|
|
7298
|
+
const applyContent = (result, opts, target, nextContent, summary) => {
|
|
7299
|
+
if (readIfExists(target) === nextContent) {
|
|
7300
|
+
result.skipped.push(target);
|
|
7301
|
+
return;
|
|
7302
|
+
}
|
|
7303
|
+
if (opts.dryRun) {
|
|
7304
|
+
result.planned.push({
|
|
7305
|
+
path: target,
|
|
7306
|
+
summary
|
|
7307
|
+
});
|
|
7308
|
+
return;
|
|
7309
|
+
}
|
|
7310
|
+
atomicWrite(target, nextContent);
|
|
7311
|
+
result.wrote.push(target);
|
|
7312
|
+
};
|
|
7313
|
+
const applyRemoval = (result, opts, target, nextContent) => {
|
|
7314
|
+
const existing = readIfExists(target);
|
|
7315
|
+
if (existing == null) {
|
|
7316
|
+
result.skipped.push(target);
|
|
7317
|
+
return;
|
|
7318
|
+
}
|
|
7319
|
+
if (existing === (nextContent ?? "")) {
|
|
7320
|
+
result.skipped.push(target);
|
|
7321
|
+
return;
|
|
7322
|
+
}
|
|
7323
|
+
if (opts.dryRun) {
|
|
7324
|
+
result.removed.push(target);
|
|
7325
|
+
return;
|
|
7326
|
+
}
|
|
7327
|
+
if (nextContent == null) try {
|
|
7328
|
+
fs.unlinkSync(target);
|
|
7329
|
+
} catch {}
|
|
7330
|
+
else atomicWrite(target, nextContent);
|
|
7331
|
+
result.removed.push(target);
|
|
7332
|
+
};
|
|
7333
|
+
|
|
7334
|
+
//#endregion
|
|
7335
|
+
//#region src/hooks/install/rules-only.ts
|
|
7336
|
+
const installRulesOnly = (opts, paths, summary) => {
|
|
7337
|
+
const result = emptyResult();
|
|
7338
|
+
const next = upsertMarkdownFence(readIfExists(paths.rules), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
|
|
7339
|
+
applyContent(result, opts, paths.rules, next, summary);
|
|
7340
|
+
if (paths.host && paths.marker) {
|
|
7341
|
+
const host = readIfExists(paths.host) ?? "";
|
|
7342
|
+
if (!host.includes(paths.marker)) {
|
|
7343
|
+
const joiner = host.endsWith("\n") || host.length === 0 ? "" : "\n";
|
|
7344
|
+
const prefix = host.length === 0 ? "" : `${host}${joiner}\n`;
|
|
7345
|
+
applyContent(result, opts, paths.host, `${prefix}${paths.marker}\n`, `append ${paths.marker} reference`);
|
|
7346
|
+
} else result.skipped.push(paths.host);
|
|
7347
|
+
}
|
|
7348
|
+
return result;
|
|
7349
|
+
};
|
|
7350
|
+
const uninstallRulesOnly = (opts, paths) => {
|
|
7351
|
+
const result = {
|
|
7352
|
+
removed: [],
|
|
7353
|
+
skipped: []
|
|
7354
|
+
};
|
|
7355
|
+
if (readIfExists(paths.rules) != null) applyRemoval(result, opts, paths.rules, null);
|
|
7356
|
+
else result.skipped.push(paths.rules);
|
|
7357
|
+
if (paths.host && paths.marker) {
|
|
7358
|
+
const host = readIfExists(paths.host);
|
|
7359
|
+
if (host != null && host.includes(paths.marker)) {
|
|
7360
|
+
const stripped = host.split("\n").filter((l) => l.trim() !== paths.marker).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
7361
|
+
applyRemoval(result, opts, paths.host, stripped.length === 0 ? null : `${stripped}\n`);
|
|
7362
|
+
} else result.skipped.push(paths.host);
|
|
6898
7363
|
}
|
|
7364
|
+
return result;
|
|
7365
|
+
};
|
|
7366
|
+
|
|
7367
|
+
//#endregion
|
|
7368
|
+
//#region src/hooks/install/antigravity.ts
|
|
7369
|
+
const resolveAntigravityPaths = (opts) => ({ rules: path.join(opts.cwd, ".agents", "rules", "antigravity-aislop-rules.md") });
|
|
7370
|
+
const installAntigravity = (opts) => {
|
|
7371
|
+
if (opts.scope !== "project") return {
|
|
7372
|
+
wrote: [],
|
|
7373
|
+
skipped: [],
|
|
7374
|
+
planned: [{
|
|
7375
|
+
path: ".agents/rules/antigravity-aislop-rules.md",
|
|
7376
|
+
summary: "Antigravity is project-scope only; pass --project"
|
|
7377
|
+
}]
|
|
7378
|
+
};
|
|
7379
|
+
return installRulesOnly(opts, resolveAntigravityPaths(opts), "write .agents/rules/antigravity-aislop-rules.md");
|
|
7380
|
+
};
|
|
7381
|
+
const uninstallAntigravity = (opts) => {
|
|
7382
|
+
if (opts.scope !== "project") return {
|
|
7383
|
+
removed: [],
|
|
7384
|
+
skipped: []
|
|
7385
|
+
};
|
|
7386
|
+
return uninstallRulesOnly(opts, resolveAntigravityPaths(opts));
|
|
6899
7387
|
};
|
|
6900
7388
|
|
|
6901
7389
|
//#endregion
|
|
@@ -6918,43 +7406,44 @@ const upsertHookGroup = (config, event, group) => {
|
|
|
6918
7406
|
};
|
|
6919
7407
|
return next;
|
|
6920
7408
|
};
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
const
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
`<!-- aislop:begin v1 hash=${hash} -->`,
|
|
6929
|
-
body.trimEnd(),
|
|
6930
|
-
"<!-- aislop:end v1 -->"
|
|
6931
|
-
].join("\n");
|
|
6932
|
-
const upsertMarkdownFence = (existing, body, hash) => {
|
|
6933
|
-
const fenced = renderFence(body, hash);
|
|
6934
|
-
if (existing == null || existing.length === 0) return {
|
|
6935
|
-
nextContent: `${fenced}\n`,
|
|
6936
|
-
replaced: false
|
|
6937
|
-
};
|
|
6938
|
-
const begin = existing.match(BEGIN_RE);
|
|
6939
|
-
const end = existing.match(END_RE);
|
|
6940
|
-
if (begin && end && (end.index ?? 0) > (begin.index ?? 0)) return {
|
|
6941
|
-
nextContent: `${existing.slice(0, begin.index)}${fenced}${existing.slice((end.index ?? 0) + end[0].length)}`,
|
|
6942
|
-
replaced: true
|
|
7409
|
+
const upsertFlatHook = (config, event, entry) => {
|
|
7410
|
+
const next = { ...config };
|
|
7411
|
+
const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
|
|
7412
|
+
const cleaned = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((e) => !isAislopManaged(e));
|
|
7413
|
+
next.hooks = {
|
|
7414
|
+
...hooks,
|
|
7415
|
+
[event]: [...cleaned, entry]
|
|
6943
7416
|
};
|
|
7417
|
+
return next;
|
|
7418
|
+
};
|
|
7419
|
+
const removeAislopEntries = (config, event) => {
|
|
7420
|
+
const next = { ...config };
|
|
7421
|
+
const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
|
|
7422
|
+
const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
7423
|
+
const cleaned = existing.filter((e) => !isAislopManaged(e) && !groupIsAislop(e));
|
|
7424
|
+
const removed = existing.length - cleaned.length;
|
|
7425
|
+
const nextHooks = { ...hooks };
|
|
7426
|
+
if (cleaned.length === 0) delete nextHooks[event];
|
|
7427
|
+
else nextHooks[event] = cleaned;
|
|
7428
|
+
if (Object.keys(nextHooks).length === 0) delete next.hooks;
|
|
7429
|
+
else next.hooks = nextHooks;
|
|
6944
7430
|
return {
|
|
6945
|
-
|
|
6946
|
-
|
|
7431
|
+
next,
|
|
7432
|
+
removed
|
|
6947
7433
|
};
|
|
6948
7434
|
};
|
|
6949
7435
|
|
|
6950
7436
|
//#endregion
|
|
6951
7437
|
//#region src/hooks/install/claude.ts
|
|
6952
|
-
const resolveClaudePaths = (
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
7438
|
+
const resolveClaudePaths = (opts) => {
|
|
7439
|
+
const root = opts.scope === "project" ? path.join(opts.cwd, ".claude") : path.join(opts.home, ".claude");
|
|
7440
|
+
return {
|
|
7441
|
+
settings: path.join(root, "settings.json"),
|
|
7442
|
+
aislopMd: path.join(root, "AISLOP.md"),
|
|
7443
|
+
claudeMd: path.join(root, "CLAUDE.md")
|
|
7444
|
+
};
|
|
7445
|
+
};
|
|
7446
|
+
const buildHookGroup$1 = () => {
|
|
6958
7447
|
const hashBody = JSON.stringify({
|
|
6959
7448
|
command: "aislop hook claude",
|
|
6960
7449
|
matcher: "Edit|Write|MultiEdit"
|
|
@@ -6972,73 +7461,559 @@ const buildHookGroup = () => {
|
|
|
6972
7461
|
}]
|
|
6973
7462
|
};
|
|
6974
7463
|
};
|
|
6975
|
-
const
|
|
6976
|
-
const
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
7464
|
+
const buildStopHookGroup = () => {
|
|
7465
|
+
const hashBody = JSON.stringify({ command: "aislop hook claude --stop" });
|
|
7466
|
+
return {
|
|
7467
|
+
matcher: "",
|
|
7468
|
+
hooks: [{
|
|
7469
|
+
type: "command",
|
|
7470
|
+
command: "aislop hook claude --stop",
|
|
7471
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
7472
|
+
v: 1,
|
|
7473
|
+
managed: true,
|
|
7474
|
+
hash: sentinelHash(hashBody)
|
|
7475
|
+
}
|
|
7476
|
+
}]
|
|
7477
|
+
};
|
|
7478
|
+
};
|
|
7479
|
+
const renderSettings$1 = (existingRaw, qualityGate) => {
|
|
7480
|
+
let obj = {};
|
|
7481
|
+
if (existingRaw) try {
|
|
7482
|
+
obj = JSON.parse(existingRaw);
|
|
6983
7483
|
} catch {
|
|
6984
|
-
|
|
6985
|
-
}
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
7484
|
+
obj = {};
|
|
7485
|
+
}
|
|
7486
|
+
let next = upsertHookGroup(obj, "PostToolUse", buildHookGroup$1());
|
|
7487
|
+
if (qualityGate) next = upsertHookGroup(next, "Stop", buildStopHookGroup());
|
|
7488
|
+
else next = removeAislopEntries(next, "Stop").next;
|
|
7489
|
+
return `${JSON.stringify(next, null, 2)}\n`;
|
|
7490
|
+
};
|
|
7491
|
+
const installClaude = (opts) => {
|
|
7492
|
+
const paths = resolveClaudePaths(opts);
|
|
7493
|
+
const result = emptyResult();
|
|
7494
|
+
const nextSettings = renderSettings$1(readIfExists(paths.settings), Boolean(opts.qualityGate));
|
|
7495
|
+
applyContent(result, opts, paths.settings, nextSettings, "register PostToolUse hook");
|
|
6992
7496
|
const mdHash = sentinelHash(AISLOP_MD_BODY);
|
|
6993
|
-
const
|
|
6994
|
-
|
|
6995
|
-
if (fenced.nextContent !== existingMd) {
|
|
6996
|
-
atomicWrite(paths.aislopMd, fenced.nextContent);
|
|
6997
|
-
wrote.push(paths.aislopMd);
|
|
6998
|
-
} else skipped.push(paths.aislopMd);
|
|
7497
|
+
const fenced = upsertMarkdownFence(readIfExists(paths.aislopMd), AISLOP_MD_BODY, mdHash);
|
|
7498
|
+
applyContent(result, opts, paths.aislopMd, fenced.nextContent, "write AISLOP.md rules");
|
|
6999
7499
|
const existingClaudeMd = readIfExists(paths.claudeMd) ?? "";
|
|
7000
7500
|
const marker = "@AISLOP.md";
|
|
7001
7501
|
if (!existingClaudeMd.includes(marker)) {
|
|
7002
7502
|
const joiner = existingClaudeMd.endsWith("\n") || existingClaudeMd.length === 0 ? "" : "\n";
|
|
7003
|
-
const
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7503
|
+
const prefix = existingClaudeMd.length === 0 ? "" : `${existingClaudeMd}${joiner}\n`;
|
|
7504
|
+
applyContent(result, opts, paths.claudeMd, `${prefix}${marker}\n`, "append @AISLOP.md reference");
|
|
7505
|
+
} else result.skipped.push(paths.claudeMd);
|
|
7506
|
+
return result;
|
|
7507
|
+
};
|
|
7508
|
+
const uninstallClaude = (opts) => {
|
|
7509
|
+
const paths = resolveClaudePaths({
|
|
7510
|
+
...opts,
|
|
7511
|
+
qualityGate: false
|
|
7512
|
+
});
|
|
7513
|
+
const result = {
|
|
7514
|
+
removed: [],
|
|
7515
|
+
skipped: []
|
|
7516
|
+
};
|
|
7517
|
+
const settingsRaw = readIfExists(paths.settings);
|
|
7518
|
+
if (settingsRaw) {
|
|
7519
|
+
let obj = {};
|
|
7520
|
+
try {
|
|
7521
|
+
obj = JSON.parse(settingsRaw);
|
|
7522
|
+
} catch {
|
|
7523
|
+
obj = {};
|
|
7524
|
+
}
|
|
7525
|
+
const stripped = removeAislopEntries(removeAislopEntries(obj, "PostToolUse").next, "Stop").next;
|
|
7526
|
+
const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
|
|
7527
|
+
const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks");
|
|
7528
|
+
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
|
|
7529
|
+
else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
7530
|
+
} else result.skipped.push(paths.settings);
|
|
7531
|
+
if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
|
|
7532
|
+
else result.skipped.push(paths.aislopMd);
|
|
7533
|
+
const claudeMd = readIfExists(paths.claudeMd);
|
|
7534
|
+
if (claudeMd != null && claudeMd.includes("@AISLOP.md")) {
|
|
7535
|
+
const stripped = claudeMd.split("\n").filter((line) => line.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
7536
|
+
applyRemoval(result, opts, paths.claudeMd, stripped.length === 0 ? null : `${stripped}\n`);
|
|
7537
|
+
} else result.skipped.push(paths.claudeMd);
|
|
7538
|
+
return result;
|
|
7539
|
+
};
|
|
7540
|
+
|
|
7541
|
+
//#endregion
|
|
7542
|
+
//#region src/hooks/install/cline.ts
|
|
7543
|
+
const resolveClinePaths = (opts) => ({ rules: path.join(opts.cwd, ".clinerules") });
|
|
7544
|
+
const resolveRooPaths = (opts) => ({ rules: path.join(opts.cwd, ".roo", "rules", "aislop.md") });
|
|
7545
|
+
const installCline = (opts) => {
|
|
7546
|
+
if (opts.scope !== "project") return {
|
|
7547
|
+
wrote: [],
|
|
7548
|
+
skipped: [],
|
|
7549
|
+
planned: [{
|
|
7550
|
+
path: ".clinerules",
|
|
7551
|
+
summary: "Cline is project-scope only; pass --project"
|
|
7552
|
+
}]
|
|
7553
|
+
};
|
|
7554
|
+
const cline = installRulesOnly(opts, resolveClinePaths(opts), "write .clinerules");
|
|
7555
|
+
const roo = installRulesOnly(opts, resolveRooPaths(opts), "write .roo/rules/aislop.md");
|
|
7007
7556
|
return {
|
|
7008
|
-
wrote,
|
|
7009
|
-
skipped
|
|
7557
|
+
wrote: [...cline.wrote, ...roo.wrote],
|
|
7558
|
+
skipped: [...cline.skipped, ...roo.skipped],
|
|
7559
|
+
planned: [...cline.planned, ...roo.planned]
|
|
7560
|
+
};
|
|
7561
|
+
};
|
|
7562
|
+
const uninstallCline = (opts) => {
|
|
7563
|
+
if (opts.scope !== "project") return {
|
|
7564
|
+
removed: [],
|
|
7565
|
+
skipped: []
|
|
7566
|
+
};
|
|
7567
|
+
const a = uninstallRulesOnly(opts, resolveClinePaths(opts));
|
|
7568
|
+
const b = uninstallRulesOnly(opts, resolveRooPaths(opts));
|
|
7569
|
+
return {
|
|
7570
|
+
removed: [...a.removed, ...b.removed],
|
|
7571
|
+
skipped: [...a.skipped, ...b.skipped]
|
|
7010
7572
|
};
|
|
7011
7573
|
};
|
|
7012
7574
|
|
|
7013
7575
|
//#endregion
|
|
7014
|
-
//#region src/
|
|
7015
|
-
const
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7576
|
+
//#region src/hooks/install/codex.ts
|
|
7577
|
+
const resolveCodexPaths = (opts) => ({ rules: opts.scope === "project" ? path.join(opts.cwd, "AGENTS.md") : path.join(opts.home, ".codex", "AGENTS.md") });
|
|
7578
|
+
const installCodex = (opts) => installRulesOnly(opts, resolveCodexPaths(opts), "write AGENTS.md rules for Codex");
|
|
7579
|
+
const uninstallCodex = (opts) => uninstallRulesOnly(opts, resolveCodexPaths(opts));
|
|
7580
|
+
|
|
7581
|
+
//#endregion
|
|
7582
|
+
//#region src/hooks/install/copilot.ts
|
|
7583
|
+
const resolveCopilotPaths = (opts) => ({ rules: path.join(opts.cwd, ".github", "copilot-instructions.md") });
|
|
7584
|
+
const installCopilot = (opts) => {
|
|
7585
|
+
if (opts.scope !== "project") return {
|
|
7586
|
+
wrote: [],
|
|
7587
|
+
skipped: [],
|
|
7588
|
+
planned: [{
|
|
7589
|
+
path: ".github/copilot-instructions.md",
|
|
7590
|
+
summary: "Copilot is project-scope only; pass --project"
|
|
7591
|
+
}]
|
|
7592
|
+
};
|
|
7593
|
+
return installRulesOnly(opts, resolveCopilotPaths(opts), "write .github/copilot-instructions.md");
|
|
7594
|
+
};
|
|
7595
|
+
const uninstallCopilot = (opts) => {
|
|
7596
|
+
if (opts.scope !== "project") return {
|
|
7597
|
+
removed: [],
|
|
7598
|
+
skipped: []
|
|
7599
|
+
};
|
|
7600
|
+
return uninstallRulesOnly(opts, resolveCopilotPaths(opts));
|
|
7601
|
+
};
|
|
7602
|
+
|
|
7603
|
+
//#endregion
|
|
7604
|
+
//#region src/hooks/install/cursor.ts
|
|
7605
|
+
const resolveCursorPaths = (opts) => {
|
|
7606
|
+
const root = opts.scope === "project" ? path.join(opts.cwd, ".cursor") : path.join(opts.home, ".cursor");
|
|
7607
|
+
return {
|
|
7608
|
+
hooks: path.join(root, "hooks.json"),
|
|
7609
|
+
rules: path.join(opts.cwd, ".cursor", "rules", "aislop.mdc")
|
|
7610
|
+
};
|
|
7611
|
+
};
|
|
7612
|
+
const buildHookEntry = () => {
|
|
7613
|
+
const hashBody = JSON.stringify({
|
|
7614
|
+
command: "aislop hook cursor",
|
|
7615
|
+
timeout: 5e3
|
|
7616
|
+
});
|
|
7617
|
+
return {
|
|
7618
|
+
command: "aislop hook cursor",
|
|
7619
|
+
type: "command",
|
|
7620
|
+
timeout: 5e3,
|
|
7621
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
7622
|
+
v: 1,
|
|
7623
|
+
managed: true,
|
|
7624
|
+
hash: sentinelHash(hashBody)
|
|
7625
|
+
}
|
|
7626
|
+
};
|
|
7627
|
+
};
|
|
7628
|
+
const renderHooksJson = (existingRaw) => {
|
|
7629
|
+
let obj = { version: 1 };
|
|
7630
|
+
if (existingRaw) try {
|
|
7631
|
+
obj = JSON.parse(existingRaw);
|
|
7632
|
+
} catch {
|
|
7633
|
+
obj = { version: 1 };
|
|
7020
7634
|
}
|
|
7021
|
-
if (
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7635
|
+
if (typeof obj.version !== "number") obj.version = 1;
|
|
7636
|
+
const next = upsertFlatHook(obj, "afterFileEdit", buildHookEntry());
|
|
7637
|
+
return `${JSON.stringify(next, null, 2)}\n`;
|
|
7638
|
+
};
|
|
7639
|
+
const installCursor = (opts) => {
|
|
7640
|
+
const paths = resolveCursorPaths(opts);
|
|
7641
|
+
const result = emptyResult();
|
|
7642
|
+
const nextHooks = renderHooksJson(readIfExists(paths.hooks));
|
|
7643
|
+
applyContent(result, opts, paths.hooks, nextHooks, "register afterFileEdit hook");
|
|
7644
|
+
if (opts.scope === "project") {
|
|
7645
|
+
const rules = upsertMarkdownFence(readIfExists(paths.rules), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
|
|
7646
|
+
applyContent(result, opts, paths.rules, rules, "write .cursor/rules/aislop.mdc");
|
|
7647
|
+
}
|
|
7648
|
+
return result;
|
|
7649
|
+
};
|
|
7650
|
+
const uninstallCursor = (opts) => {
|
|
7651
|
+
const paths = resolveCursorPaths(opts);
|
|
7652
|
+
const result = {
|
|
7653
|
+
removed: [],
|
|
7654
|
+
skipped: []
|
|
7655
|
+
};
|
|
7656
|
+
const raw = readIfExists(paths.hooks);
|
|
7657
|
+
if (raw) {
|
|
7658
|
+
let obj = {};
|
|
7659
|
+
try {
|
|
7660
|
+
obj = JSON.parse(raw);
|
|
7661
|
+
} catch {
|
|
7662
|
+
obj = {};
|
|
7663
|
+
}
|
|
7664
|
+
const stripped = removeAislopEntries(obj, "afterFileEdit").next;
|
|
7665
|
+
const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
|
|
7666
|
+
const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks" && k !== "version");
|
|
7667
|
+
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.hooks, null);
|
|
7668
|
+
else applyRemoval(result, opts, paths.hooks, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
7669
|
+
} else result.skipped.push(paths.hooks);
|
|
7670
|
+
if (opts.scope === "project") applyRemoval(result, opts, paths.rules, null);
|
|
7671
|
+
return result;
|
|
7672
|
+
};
|
|
7673
|
+
|
|
7674
|
+
//#endregion
|
|
7675
|
+
//#region src/hooks/install/gemini.ts
|
|
7676
|
+
const resolveGeminiPaths = (opts) => {
|
|
7677
|
+
const root = opts.scope === "project" ? path.join(opts.cwd, ".gemini") : path.join(opts.home, ".gemini");
|
|
7678
|
+
return {
|
|
7679
|
+
settings: path.join(root, "settings.json"),
|
|
7680
|
+
aislopMd: path.join(root, "AISLOP.md"),
|
|
7681
|
+
geminiMd: path.join(root, "GEMINI.md")
|
|
7682
|
+
};
|
|
7683
|
+
};
|
|
7684
|
+
const buildHookGroup = () => {
|
|
7685
|
+
const hashBody = JSON.stringify({
|
|
7686
|
+
command: "aislop hook gemini",
|
|
7687
|
+
matcher: "write_file|replace"
|
|
7688
|
+
});
|
|
7689
|
+
return {
|
|
7690
|
+
matcher: "write_file|replace",
|
|
7691
|
+
hooks: [{
|
|
7692
|
+
name: "aislop",
|
|
7693
|
+
type: "command",
|
|
7694
|
+
command: "aislop hook gemini",
|
|
7695
|
+
timeout: 5e3,
|
|
7696
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
7697
|
+
v: 1,
|
|
7698
|
+
managed: true,
|
|
7699
|
+
hash: sentinelHash(hashBody)
|
|
7700
|
+
}
|
|
7701
|
+
}]
|
|
7702
|
+
};
|
|
7703
|
+
};
|
|
7704
|
+
const renderSettings = (existingRaw) => {
|
|
7705
|
+
let obj = {};
|
|
7706
|
+
if (existingRaw) try {
|
|
7707
|
+
obj = JSON.parse(existingRaw);
|
|
7708
|
+
} catch {
|
|
7709
|
+
obj = {};
|
|
7710
|
+
}
|
|
7711
|
+
const next = upsertHookGroup(obj, "AfterTool", buildHookGroup());
|
|
7712
|
+
return `${JSON.stringify(next, null, 2)}\n`;
|
|
7713
|
+
};
|
|
7714
|
+
const installGemini = (opts) => {
|
|
7715
|
+
const paths = resolveGeminiPaths(opts);
|
|
7716
|
+
const result = emptyResult();
|
|
7717
|
+
const next = renderSettings(readIfExists(paths.settings));
|
|
7718
|
+
applyContent(result, opts, paths.settings, next, "register AfterTool hook");
|
|
7719
|
+
const fenced = upsertMarkdownFence(readIfExists(paths.aislopMd), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
|
|
7720
|
+
applyContent(result, opts, paths.aislopMd, fenced, "write AISLOP.md rules");
|
|
7721
|
+
const existingGeminiMd = readIfExists(paths.geminiMd) ?? "";
|
|
7722
|
+
const marker = "@AISLOP.md";
|
|
7723
|
+
if (!existingGeminiMd.includes(marker)) {
|
|
7724
|
+
const joiner = existingGeminiMd.endsWith("\n") || existingGeminiMd.length === 0 ? "" : "\n";
|
|
7725
|
+
const prefix = existingGeminiMd.length === 0 ? "" : `${existingGeminiMd}${joiner}\n`;
|
|
7726
|
+
applyContent(result, opts, paths.geminiMd, `${prefix}${marker}\n`, "append @AISLOP.md reference");
|
|
7727
|
+
} else result.skipped.push(paths.geminiMd);
|
|
7728
|
+
return result;
|
|
7729
|
+
};
|
|
7730
|
+
const uninstallGemini = (opts) => {
|
|
7731
|
+
const paths = resolveGeminiPaths(opts);
|
|
7732
|
+
const result = {
|
|
7733
|
+
removed: [],
|
|
7734
|
+
skipped: []
|
|
7735
|
+
};
|
|
7736
|
+
const raw = readIfExists(paths.settings);
|
|
7737
|
+
if (raw) {
|
|
7738
|
+
let obj = {};
|
|
7739
|
+
try {
|
|
7740
|
+
obj = JSON.parse(raw);
|
|
7741
|
+
} catch {
|
|
7742
|
+
obj = {};
|
|
7743
|
+
}
|
|
7744
|
+
const stripped = removeAislopEntries(obj, "AfterTool").next;
|
|
7745
|
+
const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
|
|
7746
|
+
const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks");
|
|
7747
|
+
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
|
|
7748
|
+
else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
7749
|
+
} else result.skipped.push(paths.settings);
|
|
7750
|
+
if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
|
|
7751
|
+
else result.skipped.push(paths.aislopMd);
|
|
7752
|
+
const geminiMd = readIfExists(paths.geminiMd);
|
|
7753
|
+
if (geminiMd != null && geminiMd.includes("@AISLOP.md")) {
|
|
7754
|
+
const stripped = geminiMd.split("\n").filter((l) => l.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
7755
|
+
applyRemoval(result, opts, paths.geminiMd, stripped.length === 0 ? null : `${stripped}\n`);
|
|
7756
|
+
} else result.skipped.push(paths.geminiMd);
|
|
7757
|
+
return result;
|
|
7758
|
+
};
|
|
7759
|
+
|
|
7760
|
+
//#endregion
|
|
7761
|
+
//#region src/hooks/install/kilocode.ts
|
|
7762
|
+
const resolveKilocodePaths = (opts) => ({ rules: path.join(opts.cwd, ".kilocode", "rules", "aislop-rules.md") });
|
|
7763
|
+
const installKilocode = (opts) => {
|
|
7764
|
+
if (opts.scope !== "project") return {
|
|
7765
|
+
wrote: [],
|
|
7766
|
+
skipped: [],
|
|
7767
|
+
planned: [{
|
|
7768
|
+
path: ".kilocode/rules/aislop-rules.md",
|
|
7769
|
+
summary: "Kilo Code is project-scope only; pass --project"
|
|
7770
|
+
}]
|
|
7771
|
+
};
|
|
7772
|
+
return installRulesOnly(opts, resolveKilocodePaths(opts), "write .kilocode/rules/aislop-rules.md");
|
|
7773
|
+
};
|
|
7774
|
+
const uninstallKilocode = (opts) => {
|
|
7775
|
+
if (opts.scope !== "project") return {
|
|
7776
|
+
removed: [],
|
|
7777
|
+
skipped: []
|
|
7778
|
+
};
|
|
7779
|
+
return uninstallRulesOnly(opts, resolveKilocodePaths(opts));
|
|
7780
|
+
};
|
|
7781
|
+
|
|
7782
|
+
//#endregion
|
|
7783
|
+
//#region src/hooks/install/windsurf.ts
|
|
7784
|
+
const resolveWindsurfPaths = (opts) => ({ rules: path.join(opts.cwd, ".windsurfrules") });
|
|
7785
|
+
const installWindsurf = (opts) => {
|
|
7786
|
+
if (opts.scope !== "project") return {
|
|
7787
|
+
wrote: [],
|
|
7788
|
+
skipped: [],
|
|
7789
|
+
planned: [{
|
|
7790
|
+
path: ".windsurfrules",
|
|
7791
|
+
summary: "Windsurf is project-scope only; pass --project"
|
|
7792
|
+
}]
|
|
7793
|
+
};
|
|
7794
|
+
return installRulesOnly(opts, resolveWindsurfPaths(opts), "write .windsurfrules");
|
|
7795
|
+
};
|
|
7796
|
+
const uninstallWindsurf = (opts) => {
|
|
7797
|
+
if (opts.scope !== "project") return {
|
|
7798
|
+
removed: [],
|
|
7799
|
+
skipped: []
|
|
7800
|
+
};
|
|
7801
|
+
return uninstallRulesOnly(opts, resolveWindsurfPaths(opts));
|
|
7802
|
+
};
|
|
7803
|
+
|
|
7804
|
+
//#endregion
|
|
7805
|
+
//#region src/hooks/install/registry.ts
|
|
7806
|
+
const ALL_AGENTS = [
|
|
7807
|
+
"claude",
|
|
7808
|
+
"cursor",
|
|
7809
|
+
"gemini",
|
|
7810
|
+
"codex",
|
|
7811
|
+
"windsurf",
|
|
7812
|
+
"cline",
|
|
7813
|
+
"kilocode",
|
|
7814
|
+
"antigravity",
|
|
7815
|
+
"copilot"
|
|
7816
|
+
];
|
|
7817
|
+
const AGENTS_PROJECT_ONLY = [
|
|
7818
|
+
"windsurf",
|
|
7819
|
+
"cline",
|
|
7820
|
+
"kilocode",
|
|
7821
|
+
"antigravity",
|
|
7822
|
+
"copilot"
|
|
7823
|
+
];
|
|
7824
|
+
const AGENTS_SUPPORTING_BOTH_SCOPES = [
|
|
7825
|
+
"claude",
|
|
7826
|
+
"cursor",
|
|
7827
|
+
"gemini",
|
|
7828
|
+
"codex"
|
|
7829
|
+
];
|
|
7830
|
+
const paths = {
|
|
7831
|
+
claude: (opts) => {
|
|
7832
|
+
const p = resolveClaudePaths(opts);
|
|
7833
|
+
return [
|
|
7834
|
+
p.settings,
|
|
7835
|
+
p.aislopMd,
|
|
7836
|
+
p.claudeMd
|
|
7837
|
+
];
|
|
7838
|
+
},
|
|
7839
|
+
cursor: (opts) => {
|
|
7840
|
+
const p = resolveCursorPaths(opts);
|
|
7841
|
+
return opts.scope === "project" ? [p.hooks, p.rules] : [p.hooks];
|
|
7842
|
+
},
|
|
7843
|
+
gemini: (opts) => {
|
|
7844
|
+
const p = resolveGeminiPaths(opts);
|
|
7845
|
+
return [
|
|
7846
|
+
p.settings,
|
|
7847
|
+
p.aislopMd,
|
|
7848
|
+
p.geminiMd
|
|
7849
|
+
];
|
|
7850
|
+
},
|
|
7851
|
+
codex: (opts) => [resolveCodexPaths(opts).rules],
|
|
7852
|
+
windsurf: (opts) => [resolveWindsurfPaths(opts).rules],
|
|
7853
|
+
cline: (opts) => [resolveClinePaths(opts).rules, resolveRooPaths(opts).rules],
|
|
7854
|
+
kilocode: (opts) => [resolveKilocodePaths(opts).rules],
|
|
7855
|
+
antigravity: (opts) => [resolveAntigravityPaths(opts).rules],
|
|
7856
|
+
copilot: (opts) => [resolveCopilotPaths(opts).rules]
|
|
7857
|
+
};
|
|
7858
|
+
const REGISTRY = {
|
|
7859
|
+
claude: {
|
|
7860
|
+
install: installClaude,
|
|
7861
|
+
uninstall: uninstallClaude,
|
|
7862
|
+
paths: paths.claude
|
|
7863
|
+
},
|
|
7864
|
+
cursor: {
|
|
7865
|
+
install: installCursor,
|
|
7866
|
+
uninstall: uninstallCursor,
|
|
7867
|
+
paths: paths.cursor
|
|
7868
|
+
},
|
|
7869
|
+
gemini: {
|
|
7870
|
+
install: installGemini,
|
|
7871
|
+
uninstall: uninstallGemini,
|
|
7872
|
+
paths: paths.gemini
|
|
7873
|
+
},
|
|
7874
|
+
codex: {
|
|
7875
|
+
install: installCodex,
|
|
7876
|
+
uninstall: uninstallCodex,
|
|
7877
|
+
paths: paths.codex
|
|
7878
|
+
},
|
|
7879
|
+
windsurf: {
|
|
7880
|
+
install: installWindsurf,
|
|
7881
|
+
uninstall: uninstallWindsurf,
|
|
7882
|
+
paths: paths.windsurf
|
|
7883
|
+
},
|
|
7884
|
+
cline: {
|
|
7885
|
+
install: installCline,
|
|
7886
|
+
uninstall: uninstallCline,
|
|
7887
|
+
paths: paths.cline
|
|
7888
|
+
},
|
|
7889
|
+
kilocode: {
|
|
7890
|
+
install: installKilocode,
|
|
7891
|
+
uninstall: uninstallKilocode,
|
|
7892
|
+
paths: paths.kilocode
|
|
7893
|
+
},
|
|
7894
|
+
antigravity: {
|
|
7895
|
+
install: installAntigravity,
|
|
7896
|
+
uninstall: uninstallAntigravity,
|
|
7897
|
+
paths: paths.antigravity
|
|
7898
|
+
},
|
|
7899
|
+
copilot: {
|
|
7900
|
+
install: installCopilot,
|
|
7901
|
+
uninstall: uninstallCopilot,
|
|
7902
|
+
paths: paths.copilot
|
|
7903
|
+
}
|
|
7904
|
+
};
|
|
7905
|
+
const defaultScopeFor = (agent) => AGENTS_PROJECT_ONLY.includes(agent) ? "project" : "global";
|
|
7906
|
+
const detectInstalledAgents = (opts) => {
|
|
7907
|
+
const hits = [];
|
|
7908
|
+
for (const agent of ALL_AGENTS) {
|
|
7909
|
+
const scope = defaultScopeFor(agent);
|
|
7910
|
+
if (REGISTRY[agent].paths({
|
|
7911
|
+
home: opts.home,
|
|
7912
|
+
cwd: opts.cwd,
|
|
7913
|
+
scope
|
|
7914
|
+
}).some((p) => fs.existsSync(p))) hits.push(agent);
|
|
7025
7915
|
}
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7916
|
+
return hits;
|
|
7917
|
+
};
|
|
7918
|
+
|
|
7919
|
+
//#endregion
|
|
7920
|
+
//#region src/commands/hook.ts
|
|
7921
|
+
const resolveOpts = (agent, flags) => {
|
|
7922
|
+
const scope = AGENTS_PROJECT_ONLY.includes(agent) ? "project" : flags.scope;
|
|
7923
|
+
return {
|
|
7924
|
+
home: os.homedir(),
|
|
7925
|
+
cwd: process.cwd(),
|
|
7926
|
+
scope,
|
|
7927
|
+
dryRun: flags.dryRun,
|
|
7928
|
+
qualityGate: flags.qualityGate
|
|
7929
|
+
};
|
|
7930
|
+
};
|
|
7931
|
+
const printPlan = (agent, result) => {
|
|
7932
|
+
if (result.planned.length === 0) {
|
|
7933
|
+
process.stdout.write(` ${agent}: already up to date\n`);
|
|
7029
7934
|
return;
|
|
7030
7935
|
}
|
|
7031
|
-
|
|
7032
|
-
for (const
|
|
7936
|
+
process.stdout.write(` ${agent}:\n`);
|
|
7937
|
+
for (const op of result.planned) process.stdout.write(` ${style(theme, "dim", "+")} ${op.path} — ${op.summary}\n`);
|
|
7033
7938
|
};
|
|
7034
|
-
const
|
|
7035
|
-
if (
|
|
7036
|
-
|
|
7939
|
+
const hookInstall = async (flags) => {
|
|
7940
|
+
if (flags.dryRun) process.stdout.write("aislop hook install (dry-run)\n\n");
|
|
7941
|
+
for (const agent of flags.agents) {
|
|
7942
|
+
const opts = resolveOpts(agent, flags);
|
|
7943
|
+
const result = REGISTRY[agent].install(opts);
|
|
7944
|
+
if (flags.dryRun) {
|
|
7945
|
+
printPlan(agent, result);
|
|
7946
|
+
continue;
|
|
7947
|
+
}
|
|
7948
|
+
if (result.wrote.length === 0) {
|
|
7949
|
+
process.stdout.write(`${agent}: nothing to do (already up to date)\n`);
|
|
7950
|
+
continue;
|
|
7951
|
+
}
|
|
7952
|
+
for (const f of result.wrote) process.stdout.write(` wrote ${f}\n`);
|
|
7953
|
+
for (const f of result.skipped) process.stdout.write(` skip ${f}\n`);
|
|
7954
|
+
}
|
|
7955
|
+
if (flags.dryRun) process.stdout.write("\nNo files touched. Re-run without --dry-run to apply.\n");
|
|
7956
|
+
};
|
|
7957
|
+
const hookUninstall = async (flags) => {
|
|
7958
|
+
if (flags.dryRun) process.stdout.write("aislop hook uninstall (dry-run)\n\n");
|
|
7959
|
+
for (const agent of flags.agents) {
|
|
7960
|
+
const opts = resolveOpts(agent, flags);
|
|
7961
|
+
const result = REGISTRY[agent].uninstall(opts);
|
|
7962
|
+
if (result.removed.length === 0) {
|
|
7963
|
+
process.stdout.write(`${agent}: nothing installed\n`);
|
|
7964
|
+
continue;
|
|
7965
|
+
}
|
|
7966
|
+
for (const f of result.removed) process.stdout.write(` remove ${f}\n`);
|
|
7967
|
+
for (const f of result.skipped) process.stdout.write(` skip ${f}\n`);
|
|
7968
|
+
}
|
|
7969
|
+
};
|
|
7970
|
+
const hookStatus = async () => {
|
|
7971
|
+
const home = os.homedir();
|
|
7972
|
+
const cwd = process.cwd();
|
|
7973
|
+
process.stdout.write("aislop hook status\n\n");
|
|
7974
|
+
const installed = new Set(detectInstalledAgents({
|
|
7975
|
+
home,
|
|
7976
|
+
cwd
|
|
7977
|
+
}));
|
|
7978
|
+
for (const agent of ALL_AGENTS) {
|
|
7979
|
+
const scope = defaultScopeFor(agent);
|
|
7980
|
+
const hits = REGISTRY[agent].paths({
|
|
7981
|
+
home,
|
|
7982
|
+
cwd,
|
|
7983
|
+
scope
|
|
7984
|
+
}).filter((p) => fs.existsSync(p));
|
|
7985
|
+
const status = installed.has(agent) ? "installed" : "not installed";
|
|
7986
|
+
const marker = installed.has(agent) ? "✓" : "·";
|
|
7987
|
+
process.stdout.write(` ${marker} ${agent.padEnd(12)} ${scope.padEnd(8)} ${status}\n`);
|
|
7988
|
+
for (const p of hits) process.stdout.write(` ${p}\n`);
|
|
7989
|
+
}
|
|
7990
|
+
};
|
|
7991
|
+
const hookRun = async (agent, flags) => {
|
|
7992
|
+
let exitCode = 0;
|
|
7993
|
+
if (agent === "claude") exitCode = flags?.stop ? await runClaudeStopHook() : await runClaudeHook();
|
|
7994
|
+
else if (agent === "cursor") exitCode = await runCursorHook();
|
|
7995
|
+
else if (agent === "gemini") exitCode = await runGeminiHook();
|
|
7996
|
+
else {
|
|
7997
|
+
process.stderr.write(`hook: agent "${agent}" has no runtime adapter (rules-file-only)\n`);
|
|
7037
7998
|
process.exit(0);
|
|
7038
7999
|
}
|
|
7039
|
-
const exitCode = await runClaudeHook();
|
|
7040
8000
|
process.exit(exitCode);
|
|
7041
8001
|
};
|
|
8002
|
+
const hookBaseline = async () => {
|
|
8003
|
+
const result = await captureBaseline(process.cwd());
|
|
8004
|
+
process.stdout.write(`baseline captured: score=${result.score} files=${result.fileCount}\n`);
|
|
8005
|
+
process.stdout.write(` -> ${result.path}\n`);
|
|
8006
|
+
};
|
|
8007
|
+
const parseAgentFlag = (raw, fallback) => {
|
|
8008
|
+
if (!raw) return fallback;
|
|
8009
|
+
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
8010
|
+
const unknown = parts.filter((p) => !ALL_AGENTS.includes(p));
|
|
8011
|
+
if (unknown.length > 0) throw new Error(`Unknown agent(s): ${unknown.join(", ")}. Valid: ${ALL_AGENTS.join(", ")}`);
|
|
8012
|
+
return parts;
|
|
8013
|
+
};
|
|
8014
|
+
const defaultInstallTargets = () => {
|
|
8015
|
+
return AGENTS_SUPPORTING_BOTH_SCOPES;
|
|
8016
|
+
};
|
|
7042
8017
|
|
|
7043
8018
|
//#endregion
|
|
7044
8019
|
//#region src/commands/init.ts
|
|
@@ -7612,14 +8587,43 @@ program.command("rules [directory]").description("List all available rules").act
|
|
|
7612
8587
|
await rulesCommand(directory);
|
|
7613
8588
|
});
|
|
7614
8589
|
const hook = program.command("hook").description("Install or invoke AI-agent integration hooks");
|
|
7615
|
-
|
|
8590
|
+
const resolveScope = (flags) => {
|
|
8591
|
+
if (flags.project) return "project";
|
|
8592
|
+
if (flags.global) return "global";
|
|
8593
|
+
return "global";
|
|
8594
|
+
};
|
|
8595
|
+
hook.command("install").description("Install aislop hooks for one or more coding agents").option("--agent <names>", "comma-separated agent list (claude,cursor,gemini,codex,windsurf,cline,kilocode,antigravity,copilot). default: all non-project-only agents").option("-g, --global", "install to the user-scope config (default for agents that support it)").option("--project", "install to the project-scope config").option("--dry-run", "print the planned diff without writing").option("--yes", "skip the confirmation prompt (reserved)").option("--quality-gate", "add a Stop hook that blocks when score regresses below baseline (Claude only)").action(async (opts) => {
|
|
7616
8596
|
await hookInstall({
|
|
7617
|
-
|
|
7618
|
-
|
|
8597
|
+
agents: parseAgentFlag(opts.agent, defaultInstallTargets()),
|
|
8598
|
+
scope: resolveScope(opts),
|
|
8599
|
+
dryRun: Boolean(opts.dryRun),
|
|
8600
|
+
yes: Boolean(opts.yes),
|
|
8601
|
+
qualityGate: Boolean(opts.qualityGate)
|
|
8602
|
+
});
|
|
8603
|
+
});
|
|
8604
|
+
hook.command("uninstall").description("Uninstall aislop hooks for one or more agents").option("--agent <names>", "comma-separated agent list. default: all agents with installed hooks").option("-g, --global", "uninstall from user-scope config").option("--project", "uninstall from project-scope config").option("--dry-run", "print the planned removal without writing").action(async (opts) => {
|
|
8605
|
+
await hookUninstall({
|
|
8606
|
+
agents: parseAgentFlag(opts.agent, defaultInstallTargets()),
|
|
8607
|
+
scope: resolveScope(opts),
|
|
8608
|
+
dryRun: Boolean(opts.dryRun),
|
|
8609
|
+
yes: true,
|
|
8610
|
+
qualityGate: false
|
|
7619
8611
|
});
|
|
7620
8612
|
});
|
|
7621
|
-
hook.command("
|
|
7622
|
-
await
|
|
8613
|
+
hook.command("status").description("Show which agent hooks are installed").action(async () => {
|
|
8614
|
+
await hookStatus();
|
|
8615
|
+
});
|
|
8616
|
+
hook.command("baseline").description("Capture the current project score as the quality-gate baseline").action(async () => {
|
|
8617
|
+
await hookBaseline();
|
|
8618
|
+
});
|
|
8619
|
+
hook.command("claude").description("Internal: Claude Code PostToolUse / Stop callback (reads stdin)").option("--stop", "run in Stop-hook mode for the quality gate").action(async (opts) => {
|
|
8620
|
+
await hookRun("claude", { stop: Boolean(opts.stop) });
|
|
8621
|
+
});
|
|
8622
|
+
hook.command("cursor").description("Internal: Cursor afterFileEdit callback (reads stdin)").action(async () => {
|
|
8623
|
+
await hookRun("cursor");
|
|
8624
|
+
});
|
|
8625
|
+
hook.command("gemini").description("Internal: Gemini CLI AfterTool callback (reads stdin)").action(async () => {
|
|
8626
|
+
await hookRun("gemini");
|
|
7623
8627
|
});
|
|
7624
8628
|
const main = async () => {
|
|
7625
8629
|
await program.parseAsync();
|