diffsplain 0.8.3 → 0.10.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 +26 -5
- package/dist/assets/{angular-html-e4D_THVy.js → angular-html-2u-u5H-N.js} +1 -1
- package/dist/assets/{angular-ts-Cx3870JF.js → angular-ts-4_HVGlKp.js} +1 -1
- package/dist/assets/{apl-DZuu22Wh.js → apl-icaNWZX3.js} +1 -1
- package/dist/assets/{astro-K-NemFAk.js → astro-HvIOE4vz.js} +1 -1
- package/dist/assets/{blade-ConbVu3U.js → blade-BpKJV8ev.js} +1 -1
- package/dist/assets/{c-DtmaBqln.js → c-BNvtxyRQ.js} +1 -1
- package/dist/assets/{cobol-BMHoPOfB.js → cobol-8js4hfxa.js} +1 -1
- package/dist/assets/{coffee-DfweJ19Y.js → coffee-DXg_piQ9.js} +1 -1
- package/dist/assets/{cpp-CPh_zIOW.js → cpp-BZDFAjyh.js} +1 -1
- package/dist/assets/{crystal-CAEX7C1J.js → crystal-aCF3ira4.js} +1 -1
- package/dist/assets/{css-C8ihGqCA.js → css-DfRtUkhE.js} +1 -1
- package/dist/assets/{edge-DVZPwQ9j.js → edge-vS6_AmQv.js} +1 -1
- package/dist/assets/{elixir-D03TKulP.js → elixir-Ce0nUHSq.js} +1 -1
- package/dist/assets/{elm-JUX-2xVV.js → elm-DmaeyizZ.js} +1 -1
- package/dist/assets/{erb-HbktAFZJ.js → erb-DD6OUyvt.js} +1 -1
- package/dist/assets/{git-rebase-Ccd_ELAP.js → git-rebase-HbJMn7iS.js} +1 -1
- package/dist/assets/{glimmer-js-BcinUfi2.js → glimmer-js-BRuqbNaV.js} +1 -1
- package/dist/assets/{glimmer-ts-DuN2UkN9.js → glimmer-ts-ZBpkdEEA.js} +1 -1
- package/dist/assets/{glsl-BWmCEPsR.js → glsl-CrQ4GScf.js} +1 -1
- package/dist/assets/{graphql-DJGYoBFW.js → graphql-Tn6PBdQY.js} +1 -1
- package/dist/assets/{hack-ZPqBE9w3.js → hack-CoApvrbo.js} +1 -1
- package/dist/assets/{haml-CZDZk6Gw.js → haml-COKq8Xjp.js} +1 -1
- package/dist/assets/{handlebars-d23PDS0n.js → handlebars-CAna2DTA.js} +1 -1
- package/dist/assets/{html-BgzX2DyB.js → html-Ch4xJ7YF.js} +1 -1
- package/dist/assets/{html-derivative-Bq6M-ZFw.js → html-derivative-0ITK-MAI.js} +1 -1
- package/dist/assets/{http-DVhMl_cW.js → http-DiMWQ8Qr.js} +1 -1
- package/dist/assets/{hurl-CbspBAqF.js → hurl-OcXymC0J.js} +1 -1
- package/dist/assets/index-BmcCug2M.js +1574 -0
- package/dist/assets/index-CADUNiTy.css +2 -0
- package/dist/assets/{java--YZbq0z2.js → java-CFb3qN4r.js} +1 -1
- package/dist/assets/{javascript-cvRuifRJ.js → javascript-CRmk2JrF.js} +1 -1
- package/dist/assets/{jinja-BeZmr3LP.js → jinja-Da6lDTEy.js} +1 -1
- package/dist/assets/{jison-qrpGZaxs.js → jison-Bnivzmtu.js} +1 -1
- package/dist/assets/{json-4xUzsNJn.js → json-m8vJP_KK.js} +1 -1
- package/dist/assets/{jsx-BlXrTlSg.js → jsx-C3HX_Sbu.js} +1 -1
- package/dist/assets/{julia-IN3jzmBc.js → julia-Ckg-wSHA.js} +1 -1
- package/dist/assets/{just-C_50nLQH.js → just-DCjIAyvO.js} +1 -1
- package/dist/assets/{latex-Cd-_2IS1.js → latex-CrYSm4pl.js} +1 -1
- package/dist/assets/{liquid-CEJf6Xg5.js → liquid-MQUFSPYU.js} +1 -1
- package/dist/assets/{lua-BlaV_n8i.js → lua-DcuzJJ77.js} +1 -1
- package/dist/assets/{marko-Cb-NPdc_.js → marko-4JNmQaB4.js} +1 -1
- package/dist/assets/{mdc-Dt4ywVAS.js → mdc-C_j8HvRL.js} +1 -1
- package/dist/assets/{nginx-C_6SKViP.js → nginx-ta3Gn1HE.js} +1 -1
- package/dist/assets/{nim-BbkBjoeZ.js → nim-BwWLst9M.js} +1 -1
- package/dist/assets/{perl-CYPU6TVa.js → perl-DfW7Uq5O.js} +1 -1
- package/dist/assets/{php-B6Oqilrc.js → php-DGf5G3Cf.js} +1 -1
- package/dist/assets/{pug-C9Poo2KU.js → pug-CWI-NaYz.js} +1 -1
- package/dist/assets/{qml-DkMURMPv.js → qml-Csblgh7U.js} +1 -1
- package/dist/assets/{r-Bm-Gfi0p.js → r-QGooaGDI.js} +1 -1
- package/dist/assets/{razor-B0LqOok3.js → razor-Dp9ihLYf.js} +1 -1
- package/dist/assets/{regexp-jAbMHzwe.js → regexp-DwUnLcVr.js} +1 -1
- package/dist/assets/{rst-BlWHfg8c.js → rst-BY8GIwDJ.js} +1 -1
- package/dist/assets/{ruby-Bt5lKwPL.js → ruby-VgzI9htT.js} +1 -1
- package/dist/assets/{sas-TuY2-8EF.js → sas-DYLsm-0j.js} +1 -1
- package/dist/assets/{scss-Dh1SsWlK.js → scss-tIFkeUQX.js} +1 -1
- package/dist/assets/{shellscript-CThiwmnp.js → shellscript-CHjuQ4Pl.js} +1 -1
- package/dist/assets/{shellsession-DQ5iRUWd.js → shellsession-Bv0S0hj9.js} +1 -1
- package/dist/assets/{soy-BfM-BrZG.js → soy-D6U1tZod.js} +1 -1
- package/dist/assets/{sql-F617W1uN.js → sql-CGz8kjKv.js} +1 -1
- package/dist/assets/{stata-Ctov3ki2.js → stata-C9ytERUq.js} +1 -1
- package/dist/assets/{surrealql-C-i2bJGB.js → surrealql-DRfRnaYu.js} +1 -1
- package/dist/assets/{svelte-oZT41pTl.js → svelte-wYnk_1sI.js} +1 -1
- package/dist/assets/{templ-CxX9Hf3e.js → templ-DWrGVE0u.js} +1 -1
- package/dist/assets/{tex-GyzJq8GJ.js → tex-CYTzoUJh.js} +1 -1
- package/dist/assets/{ts-tags-BugzKu0x.js → ts-tags-1TIXEGea.js} +1 -1
- package/dist/assets/{tsx-DLqwDEUq.js → tsx-a3BoAoOh.js} +1 -1
- package/dist/assets/{twig-DADiR9Vq.js → twig-e3dvNv6o.js} +1 -1
- package/dist/assets/{typescript-BlDIG_FO.js → typescript-BBhnpnTf.js} +1 -1
- package/dist/assets/{vue-COXmmg2A.js → vue-DzzVbu9q.js} +1 -1
- package/dist/assets/{vue-html-Cy_3KYj1.js → vue-html-quBNv38K.js} +1 -1
- package/dist/assets/{vue-vine-CUqOY8dY.js → vue-vine-CDuR69FE.js} +1 -1
- package/dist/assets/{xml-C9JY7Wf6.js → xml-DMifOLSe.js} +1 -1
- package/dist/assets/{xsl-DBCl8hSu.js → xsl-D8Ef-5HK.js} +1 -1
- package/dist/assets/{yaml-BbMebKkS.js → yaml-5c38gpmA.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +12 -3
- package/scripts/agent-config.mjs +184 -0
- package/scripts/agent-exclusions.mjs +10 -0
- package/scripts/agent-review.mjs +94 -0
- package/scripts/build-diff-data.mjs +211 -49
- package/scripts/cli-args.mjs +63 -9
- package/scripts/coding-agents.mjs +255 -77
- package/scripts/generate-summaries.mjs +872 -426
- package/scripts/local-target.mjs +106 -0
- package/scripts/present.mjs +333 -117
- package/scripts/review-chat-context.mjs +510 -0
- package/scripts/review-chat-controller.mjs +691 -0
- package/scripts/review-chat-provider.mjs +363 -0
- package/scripts/review-chat.mjs +8 -0
- package/scripts/serve-built.mjs +245 -27
- package/scripts/summary-path.mjs +32 -11
- package/dist/assets/index-C2p_1iAk.css +0 -2
- package/dist/assets/index-divlsuKs.js +0 -1568
|
@@ -9,15 +9,28 @@ import {
|
|
|
9
9
|
fstatSync,
|
|
10
10
|
lstatSync,
|
|
11
11
|
mkdirSync,
|
|
12
|
+
mkdtempSync,
|
|
12
13
|
openSync,
|
|
13
14
|
readFileSync,
|
|
14
15
|
readlinkSync,
|
|
15
16
|
renameSync,
|
|
17
|
+
rmSync,
|
|
16
18
|
statSync,
|
|
17
19
|
writeFileSync,
|
|
18
20
|
} from 'node:fs';
|
|
19
|
-
import {
|
|
21
|
+
import { tmpdir } from 'node:os';
|
|
22
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
20
23
|
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import {
|
|
25
|
+
isBaseWorktreeTarget,
|
|
26
|
+
resolveBaseWorktreeCommit,
|
|
27
|
+
} from './local-target.mjs';
|
|
28
|
+
import { createAgentExclusionMatcher } from './agent-exclusions.mjs';
|
|
29
|
+
import {
|
|
30
|
+
agentReviewContext,
|
|
31
|
+
agentReviewFile,
|
|
32
|
+
createAgentReviewFingerprint,
|
|
33
|
+
} from './agent-review.mjs';
|
|
21
34
|
import { summaryPath } from './summary-path.mjs';
|
|
22
35
|
|
|
23
36
|
const args = process.argv.slice(2);
|
|
@@ -34,7 +47,11 @@ const option = (name) => {
|
|
|
34
47
|
};
|
|
35
48
|
const options = (name) =>
|
|
36
49
|
args.flatMap((argument, index) =>
|
|
37
|
-
argument === name && args[index + 1]
|
|
50
|
+
argument === name && args[index + 1]
|
|
51
|
+
? [args[index + 1]]
|
|
52
|
+
: argument.startsWith(`${name}=`)
|
|
53
|
+
? [argument.slice(name.length + 1)]
|
|
54
|
+
: [],
|
|
38
55
|
);
|
|
39
56
|
const has = (name) => args.includes(name);
|
|
40
57
|
|
|
@@ -45,8 +62,8 @@ Targets:
|
|
|
45
62
|
--pr NUMBER|URL Fetch and show a GitHub pull request
|
|
46
63
|
--branch NAME Fetch and show a remote branch
|
|
47
64
|
--checkout Show the current checkout against its default branch
|
|
48
|
-
--base REF --head REF
|
|
49
|
-
|
|
65
|
+
--base REF [--head REF]
|
|
66
|
+
Compare a base with the working tree, or show an exact range
|
|
50
67
|
(no target) Show worktree changes against HEAD
|
|
51
68
|
|
|
52
69
|
Options:
|
|
@@ -63,6 +80,7 @@ Options:
|
|
|
63
80
|
const repo = resolve(option('--repo') || process.cwd());
|
|
64
81
|
const output = resolve(option('--output') || '.cache/diff-data.json');
|
|
65
82
|
const excludedOutputs = options('--exclude-output');
|
|
83
|
+
const agentExcludeRules = options('--exclude');
|
|
66
84
|
const baseOption = option('--base');
|
|
67
85
|
const headOption = option('--head');
|
|
68
86
|
const prOption = option('--pr');
|
|
@@ -88,6 +106,14 @@ const cacheRoot = cacheOption
|
|
|
88
106
|
? resolve(cacheOption)
|
|
89
107
|
: resolve(projectRoot, '.cache/git');
|
|
90
108
|
const remoteMode = Boolean(prOption || branchOption);
|
|
109
|
+
const baseWorktreeOption = isBaseWorktreeTarget({
|
|
110
|
+
base: baseOption,
|
|
111
|
+
branch: branchOption,
|
|
112
|
+
checkout: checkoutOption,
|
|
113
|
+
head: headOption,
|
|
114
|
+
pullRequest: prOption,
|
|
115
|
+
worktree: worktreeOption,
|
|
116
|
+
});
|
|
91
117
|
const watching = has('--watch');
|
|
92
118
|
const watchContent = has('--watch-content');
|
|
93
119
|
const ignoreSummaryWatch = has('--ignore-summary-watch');
|
|
@@ -117,13 +143,8 @@ if (
|
|
|
117
143
|
) {
|
|
118
144
|
fail('--worktree cannot be combined with another target');
|
|
119
145
|
}
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
!branchOption &&
|
|
123
|
-
!checkoutOption &&
|
|
124
|
-
Boolean(baseOption) !== Boolean(headOption)
|
|
125
|
-
) {
|
|
126
|
-
fail('--base and --head must be used together');
|
|
146
|
+
if (!prOption && !branchOption && !checkoutOption && headOption && !baseOption) {
|
|
147
|
+
fail('--head must be used with --base');
|
|
127
148
|
}
|
|
128
149
|
|
|
129
150
|
const repoPath = (file) => {
|
|
@@ -829,6 +850,36 @@ function resolveCheckoutTarget() {
|
|
|
829
850
|
};
|
|
830
851
|
}
|
|
831
852
|
|
|
853
|
+
function resolveBaseWorktreeTarget() {
|
|
854
|
+
const currentHead = tryRepo(['rev-parse', '--verify', 'HEAD']);
|
|
855
|
+
const resolvedBase = resolveBaseWorktreeCommit(repo, baseOption);
|
|
856
|
+
const remoteUrl = tryRepo(['remote', 'get-url', 'origin']) || undefined;
|
|
857
|
+
return {
|
|
858
|
+
kind: 'base-worktree',
|
|
859
|
+
runGit: runRepo,
|
|
860
|
+
range: [resolvedBase],
|
|
861
|
+
base: resolvedBase,
|
|
862
|
+
head: currentHead || 'WORKTREE',
|
|
863
|
+
branch: tryRepo(['branch', '--show-current']) || undefined,
|
|
864
|
+
remote: remoteUrl ? { name: 'origin', url: remoteUrl } : undefined,
|
|
865
|
+
sourceRepositoryUrl: undefined,
|
|
866
|
+
baseRepositoryUrl: undefined,
|
|
867
|
+
comparisonCommitsOnRemote: false,
|
|
868
|
+
target: {
|
|
869
|
+
kind: 'base-worktree',
|
|
870
|
+
base: { ref: baseOption, oid: resolvedBase },
|
|
871
|
+
head: { ref: 'WORKTREE', oid: currentHead || null },
|
|
872
|
+
},
|
|
873
|
+
changeDefaults: {
|
|
874
|
+
title: `Changes since ${baseOption}`,
|
|
875
|
+
summary: `Shows commits and working-tree changes since ${baseOption}.`,
|
|
876
|
+
why: 'Reviews the working tree against the chosen base without changing the repo.',
|
|
877
|
+
highlights: [],
|
|
878
|
+
risks: [],
|
|
879
|
+
},
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
832
883
|
function resolveLocalTarget() {
|
|
833
884
|
const currentHead = tryRepo(['rev-parse', '--verify', 'HEAD']);
|
|
834
885
|
const worktree = !baseOption && !headOption;
|
|
@@ -891,6 +942,7 @@ function resolveTarget() {
|
|
|
891
942
|
if (prOption) return resolvePullRequestTarget();
|
|
892
943
|
if (branchOption) return resolveBranchTarget();
|
|
893
944
|
if (checkoutOption) return resolveCheckoutTarget();
|
|
945
|
+
if (baseWorktreeOption) return resolveBaseWorktreeTarget();
|
|
894
946
|
return resolveLocalTarget();
|
|
895
947
|
}
|
|
896
948
|
|
|
@@ -959,6 +1011,60 @@ function untrackedStat(path) {
|
|
|
959
1011
|
};
|
|
960
1012
|
}
|
|
961
1013
|
|
|
1014
|
+
function baseWorktreeReplacement(path, target) {
|
|
1015
|
+
const directory = mkdtempSync(join(tmpdir(), 'diffsplain-base-index-'));
|
|
1016
|
+
const env = { ...process.env, GIT_INDEX_FILE: join(directory, 'index') };
|
|
1017
|
+
const diff = (args) => command('git', ['-C', repo, 'diff', ...args], { env });
|
|
1018
|
+
|
|
1019
|
+
try {
|
|
1020
|
+
command('git', ['-C', repo, 'read-tree', target.base], { env });
|
|
1021
|
+
const patch = diff([
|
|
1022
|
+
'--no-ext-diff',
|
|
1023
|
+
'--no-textconv',
|
|
1024
|
+
'--binary',
|
|
1025
|
+
'--find-renames',
|
|
1026
|
+
target.base,
|
|
1027
|
+
'--',
|
|
1028
|
+
path,
|
|
1029
|
+
]);
|
|
1030
|
+
const stat = parseNumstat(
|
|
1031
|
+
diff([
|
|
1032
|
+
'--no-ext-diff',
|
|
1033
|
+
'--no-textconv',
|
|
1034
|
+
'--numstat',
|
|
1035
|
+
'-z',
|
|
1036
|
+
'--find-renames',
|
|
1037
|
+
target.base,
|
|
1038
|
+
'--',
|
|
1039
|
+
path,
|
|
1040
|
+
]),
|
|
1041
|
+
).get(path) || {
|
|
1042
|
+
additions: 0,
|
|
1043
|
+
deletions: 0,
|
|
1044
|
+
isBinary: false,
|
|
1045
|
+
};
|
|
1046
|
+
return { patch, stat };
|
|
1047
|
+
} finally {
|
|
1048
|
+
rmSync(directory, { recursive: true, force: true });
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function reviewFileStat(file, numstat) {
|
|
1053
|
+
if (file.replacement) return file.replacement.stat;
|
|
1054
|
+
if (file.untracked) return untrackedStat(file.path);
|
|
1055
|
+
return (
|
|
1056
|
+
numstat.get(file.path) || {
|
|
1057
|
+
additions: 0,
|
|
1058
|
+
deletions: 0,
|
|
1059
|
+
isBinary: false,
|
|
1060
|
+
}
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function reviewFilePatch(file, target, patches) {
|
|
1065
|
+
return file.replacement?.patch ?? filePatch(file, target, patches);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
962
1068
|
function githubFileUrl(repositoryUrl, ref, path) {
|
|
963
1069
|
if (!repositoryUrl || !ref || ref === 'WORKTREE') return undefined;
|
|
964
1070
|
const filePath = path.split('/').map(encodeURIComponent).join('/');
|
|
@@ -972,6 +1078,11 @@ function githubComparisonUrl(repositoryUrl, base, head) {
|
|
|
972
1078
|
return `${repositoryUrl}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`;
|
|
973
1079
|
}
|
|
974
1080
|
|
|
1081
|
+
function fingerprintFile(file) {
|
|
1082
|
+
return agentReviewFile(file);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// fallow-ignore-next-line complexity -- Existing aggregation boundary; target handling stays in resolvers.
|
|
975
1086
|
function build() {
|
|
976
1087
|
const localWorkspace =
|
|
977
1088
|
tryRepo(['rev-parse', '--is-inside-work-tree']) === 'true';
|
|
@@ -979,6 +1090,9 @@ function build() {
|
|
|
979
1090
|
throw new Error(`${repo} is not a Git checkout`);
|
|
980
1091
|
}
|
|
981
1092
|
const target = resolveTarget();
|
|
1093
|
+
const agentExcluded = createAgentExclusionMatcher(agentExcludeRules, {
|
|
1094
|
+
ignoreCase: tryRepo(['config', '--bool', 'core.ignoreCase']) === 'true',
|
|
1095
|
+
});
|
|
982
1096
|
const remoteRepository = githubRepository(target.remote?.url);
|
|
983
1097
|
const summaryDoc = noSummaries ? {} : readJson(summariesPath, {}) || {};
|
|
984
1098
|
const allTrackedFiles = parseNameStatus(
|
|
@@ -1008,8 +1122,12 @@ function build() {
|
|
|
1008
1122
|
);
|
|
1009
1123
|
const patches = trackedPatches(allTrackedFiles, target);
|
|
1010
1124
|
|
|
1011
|
-
if (
|
|
1012
|
-
|
|
1125
|
+
if (
|
|
1126
|
+
target.kind === 'worktree' ||
|
|
1127
|
+
target.kind === 'base-worktree' ||
|
|
1128
|
+
target.kind === 'checkout'
|
|
1129
|
+
) {
|
|
1130
|
+
const trackedByPath = new Map(nameStatus.map((file) => [file.path, file]));
|
|
1013
1131
|
const untracked = tryRepo([
|
|
1014
1132
|
'ls-files',
|
|
1015
1133
|
'--others',
|
|
@@ -1019,22 +1137,26 @@ function build() {
|
|
|
1019
1137
|
.split('\0')
|
|
1020
1138
|
.filter((path) => path && !excludedPaths.has(path));
|
|
1021
1139
|
for (const path of untracked) {
|
|
1022
|
-
|
|
1140
|
+
const tracked = trackedByPath.get(path);
|
|
1141
|
+
if (target.kind === 'base-worktree' && tracked?.status === 'deleted') {
|
|
1142
|
+
const replacement = baseWorktreeReplacement(path, target);
|
|
1143
|
+
if (replacement.patch) {
|
|
1144
|
+
tracked.status = 'modified';
|
|
1145
|
+
tracked.replacement = replacement;
|
|
1146
|
+
} else {
|
|
1147
|
+
tracked.omit = true;
|
|
1148
|
+
}
|
|
1149
|
+
} else if (!tracked) {
|
|
1023
1150
|
nameStatus.push({ path, status: 'added', untracked: true });
|
|
1024
1151
|
}
|
|
1025
1152
|
}
|
|
1026
1153
|
nameStatus.sort((left, right) => left.path.localeCompare(right.path));
|
|
1027
1154
|
}
|
|
1028
1155
|
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
additions: 0,
|
|
1034
|
-
deletions: 0,
|
|
1035
|
-
isBinary: false,
|
|
1036
|
-
};
|
|
1037
|
-
const patch = filePatch(file, target, patches);
|
|
1156
|
+
const reviewFiles = nameStatus.filter((file) => !file.omit);
|
|
1157
|
+
const filesWithoutSummaries = reviewFiles.map((file) => {
|
|
1158
|
+
const stat = reviewFileStat(file, numstat);
|
|
1159
|
+
const patch = reviewFilePatch(file, target, patches);
|
|
1038
1160
|
const binary =
|
|
1039
1161
|
stat.isBinary ||
|
|
1040
1162
|
patch.includes('Binary files ') ||
|
|
@@ -1065,11 +1187,16 @@ function build() {
|
|
|
1065
1187
|
totalDiffLines: textPatch ? textPatch.split('\n').length - 1 : 0,
|
|
1066
1188
|
patch: textPatch,
|
|
1067
1189
|
snippet: binary ? '' : compactSnippet(textPatch),
|
|
1190
|
+
...(agentExcluded(file.path) ? { agentExcluded: true } : {}),
|
|
1068
1191
|
...(sourceUrl ? { sourceUrl } : {}),
|
|
1069
1192
|
...(comparisonUrl ? { comparisonUrl } : {}),
|
|
1070
1193
|
};
|
|
1071
1194
|
});
|
|
1072
1195
|
|
|
1196
|
+
const agentFiles = filesWithoutSummaries.filter(
|
|
1197
|
+
(file) => !file.agentExcluded,
|
|
1198
|
+
);
|
|
1199
|
+
|
|
1073
1200
|
const reviewFingerprint = createHash('sha256')
|
|
1074
1201
|
.update(
|
|
1075
1202
|
JSON.stringify({
|
|
@@ -1081,38 +1208,55 @@ function build() {
|
|
|
1081
1208
|
remote: target.remote?.name,
|
|
1082
1209
|
targetKind: target.kind,
|
|
1083
1210
|
},
|
|
1084
|
-
files: filesWithoutSummaries.map(
|
|
1085
|
-
path: file.path,
|
|
1086
|
-
oldPath: file.oldPath,
|
|
1087
|
-
status: file.status,
|
|
1088
|
-
additions: file.additions,
|
|
1089
|
-
deletions: file.deletions,
|
|
1090
|
-
isBinary: file.isBinary,
|
|
1091
|
-
patch: file.patch,
|
|
1092
|
-
})),
|
|
1211
|
+
files: filesWithoutSummaries.map(fingerprintFile),
|
|
1093
1212
|
}),
|
|
1094
1213
|
)
|
|
1095
1214
|
.digest('hex');
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1215
|
+
const agentReviewFingerprint = createAgentReviewFingerprint({
|
|
1216
|
+
context: agentReviewContext({
|
|
1217
|
+
name: remoteRepository?.name || basename(repo),
|
|
1218
|
+
selector: remoteRepository?.selector,
|
|
1219
|
+
target: target.target,
|
|
1220
|
+
branch: target.branch,
|
|
1221
|
+
baseBranch: target.baseBranch,
|
|
1222
|
+
}),
|
|
1223
|
+
files: agentFiles,
|
|
1224
|
+
});
|
|
1225
|
+
const hasAgentReviewFingerprint =
|
|
1226
|
+
typeof summaryDoc.meta?.agentReviewFingerprint === 'string';
|
|
1227
|
+
const hasLegacyReviewFingerprint =
|
|
1228
|
+
!hasAgentReviewFingerprint &&
|
|
1229
|
+
typeof summaryDoc.meta?.reviewFingerprint === 'string';
|
|
1230
|
+
const emptyAgentReviewFingerprint =
|
|
1231
|
+
typeof summaryDoc.meta?.emptyAgentReviewFingerprint === 'string'
|
|
1232
|
+
? summaryDoc.meta.emptyAgentReviewFingerprint
|
|
1100
1233
|
: undefined;
|
|
1234
|
+
const generatedFor = !noSummaries && hasAgentReviewFingerprint
|
|
1235
|
+
? agentFiles.length === 0 && emptyAgentReviewFingerprint
|
|
1236
|
+
? emptyAgentReviewFingerprint
|
|
1237
|
+
: summaryDoc.meta.agentReviewFingerprint
|
|
1238
|
+
: undefined;
|
|
1101
1239
|
const summariesAreFresh =
|
|
1102
|
-
!noSummaries &&
|
|
1240
|
+
!noSummaries &&
|
|
1241
|
+
!hasLegacyReviewFingerprint &&
|
|
1242
|
+
(!generatedFor || generatedFor === agentReviewFingerprint);
|
|
1103
1243
|
const sourceSummaries = summariesAreFresh ? summaryDoc : {};
|
|
1244
|
+
const agentPaths = new Set(agentFiles.map((file) => file.path));
|
|
1245
|
+
const reviewPaths = new Set(filesWithoutSummaries.map((file) => file.path));
|
|
1104
1246
|
const failedFiles = summariesAreFresh
|
|
1105
|
-
? failedFileRecords(summaryDoc.meta?.failedFiles)
|
|
1247
|
+
? failedFileRecords(summaryDoc.meta?.failedFiles).filter((failure) =>
|
|
1248
|
+
agentPaths.has(failure.path) || !reviewPaths.has(failure.path),
|
|
1249
|
+
)
|
|
1106
1250
|
: [];
|
|
1107
|
-
const summaryErrors = summariesAreFresh
|
|
1251
|
+
const summaryErrors = summariesAreFresh && agentFiles.length
|
|
1108
1252
|
? cleanList(summaryDoc.meta?.errors)
|
|
1109
1253
|
: [];
|
|
1110
1254
|
const failureByPath = new Map(
|
|
1111
1255
|
failedFiles.map((failure) => [failure.path, failure.reason]),
|
|
1112
1256
|
);
|
|
1113
1257
|
const emptyReviewComplete =
|
|
1114
|
-
|
|
1115
|
-
generatedFor ===
|
|
1258
|
+
agentFiles.length === 0 &&
|
|
1259
|
+
generatedFor === agentReviewFingerprint &&
|
|
1116
1260
|
sourceSummaries.meta?.status === 'complete';
|
|
1117
1261
|
const summariesAreComplete =
|
|
1118
1262
|
summariesAreFresh &&
|
|
@@ -1120,12 +1264,12 @@ function build() {
|
|
|
1120
1264
|
summaryErrors.length === 0 &&
|
|
1121
1265
|
(emptyReviewComplete ||
|
|
1122
1266
|
(completeChangeSummary(sourceSummaries.change) &&
|
|
1123
|
-
|
|
1267
|
+
agentFiles.every((file) =>
|
|
1124
1268
|
completeFileSummary(sourceSummaries.files?.[file.path]),
|
|
1125
1269
|
)));
|
|
1126
1270
|
const completedFiles = noSummaries
|
|
1127
1271
|
? 0
|
|
1128
|
-
:
|
|
1272
|
+
: agentFiles.filter((file) =>
|
|
1129
1273
|
completeFileSummary(sourceSummaries.files?.[file.path]),
|
|
1130
1274
|
).length;
|
|
1131
1275
|
const storedStatus = summaryDoc.meta?.status;
|
|
@@ -1140,18 +1284,28 @@ function build() {
|
|
|
1140
1284
|
: 'idle';
|
|
1141
1285
|
const files = filesWithoutSummaries.map((file) => ({
|
|
1142
1286
|
...file,
|
|
1143
|
-
summary: fileSummary(
|
|
1287
|
+
summary: fileSummary(
|
|
1288
|
+
file.path,
|
|
1289
|
+
file.agentExcluded ? undefined : sourceSummaries.files?.[file.path],
|
|
1290
|
+
),
|
|
1144
1291
|
noteReady: Boolean(
|
|
1145
|
-
|
|
1292
|
+
!file.agentExcluded &&
|
|
1293
|
+
completeFileSummary(sourceSummaries.files?.[file.path]),
|
|
1146
1294
|
),
|
|
1147
|
-
...(failureByPath.has(file.path)
|
|
1295
|
+
...(!file.agentExcluded && failureByPath.has(file.path)
|
|
1148
1296
|
? { noteFailure: failureByPath.get(file.path) }
|
|
1149
1297
|
: {}),
|
|
1150
1298
|
}));
|
|
1151
|
-
const change = changeSummary(
|
|
1299
|
+
const change = changeSummary(
|
|
1300
|
+
agentFiles.length ? sourceSummaries.change : undefined,
|
|
1301
|
+
target.changeDefaults,
|
|
1302
|
+
);
|
|
1152
1303
|
const content = {
|
|
1153
1304
|
repo: {
|
|
1154
1305
|
name: remoteRepository?.name || basename(repo),
|
|
1306
|
+
...(remoteRepository?.selector
|
|
1307
|
+
? { repository: remoteRepository.selector }
|
|
1308
|
+
: {}),
|
|
1155
1309
|
root: localWorkspace ? repo : target.remote?.url || repo,
|
|
1156
1310
|
base: target.base,
|
|
1157
1311
|
head: target.head,
|
|
@@ -1166,12 +1320,13 @@ function build() {
|
|
|
1166
1320
|
files,
|
|
1167
1321
|
notes: {
|
|
1168
1322
|
reviewFingerprint,
|
|
1323
|
+
agentReviewFingerprint,
|
|
1169
1324
|
...(generatedFor ? { generatedFor } : {}),
|
|
1170
1325
|
fresh: summariesAreFresh,
|
|
1171
1326
|
complete: summariesAreComplete,
|
|
1172
1327
|
status: noteStatus,
|
|
1173
1328
|
completedFiles,
|
|
1174
|
-
totalFiles:
|
|
1329
|
+
totalFiles: agentFiles.length,
|
|
1175
1330
|
...(failedFiles.length ? { failedFiles } : {}),
|
|
1176
1331
|
...(summaryErrors.length ? { errors: summaryErrors } : {}),
|
|
1177
1332
|
...(typeof summaryDoc.meta?.model === 'string'
|
|
@@ -1183,6 +1338,9 @@ function build() {
|
|
|
1183
1338
|
...(typeof summaryDoc.meta?.reasoning === 'string'
|
|
1184
1339
|
? { reasoning: summaryDoc.meta.reasoning }
|
|
1185
1340
|
: {}),
|
|
1341
|
+
...(typeof summaryDoc.meta?.accessMode === 'string'
|
|
1342
|
+
? { accessMode: summaryDoc.meta.accessMode }
|
|
1343
|
+
: {}),
|
|
1186
1344
|
},
|
|
1187
1345
|
};
|
|
1188
1346
|
const version = createHash('sha256')
|
|
@@ -1268,8 +1426,11 @@ function fingerprint() {
|
|
|
1268
1426
|
].join('|');
|
|
1269
1427
|
}
|
|
1270
1428
|
const content = createHash('sha256');
|
|
1429
|
+
const base = baseWorktreeOption
|
|
1430
|
+
? resolveBaseWorktreeCommit(repo, baseOption)
|
|
1431
|
+
: 'HEAD';
|
|
1271
1432
|
content.update(
|
|
1272
|
-
tryRepo(['diff', '--no-ext-diff', '--no-textconv', '--binary',
|
|
1433
|
+
tryRepo(['diff', '--no-ext-diff', '--no-textconv', '--binary', base, '--']),
|
|
1273
1434
|
);
|
|
1274
1435
|
const untracked = tryRepo([
|
|
1275
1436
|
'ls-files',
|
|
@@ -1284,6 +1445,7 @@ function fingerprint() {
|
|
|
1284
1445
|
fingerprintUntrackedPath(content, path);
|
|
1285
1446
|
}
|
|
1286
1447
|
return [
|
|
1448
|
+
baseWorktreeOption ? base : undefined,
|
|
1287
1449
|
tryRepo(['rev-parse', 'HEAD']),
|
|
1288
1450
|
tryRepo(['status', '--porcelain=v1', '--untracked-files=all']),
|
|
1289
1451
|
content.digest('hex'),
|
package/scripts/cli-args.mjs
CHANGED
|
@@ -29,6 +29,7 @@ export const cliOptions = defineCliOptions({
|
|
|
29
29
|
'--cache-dir': { kind: 'value', path: true },
|
|
30
30
|
'--codex-bin': { kind: 'value', path: true },
|
|
31
31
|
'--support-record-file': { kind: 'value', path: true },
|
|
32
|
+
'--exclude': { kind: 'repeatable-value' },
|
|
32
33
|
'--model': { kind: 'value' },
|
|
33
34
|
'--reasoning': { kind: 'value' },
|
|
34
35
|
'--batch-size': { kind: 'value', default: 12, min: 1, max: 50 },
|
|
@@ -39,6 +40,7 @@ export const cliOptions = defineCliOptions({
|
|
|
39
40
|
'--version': { kind: 'flag' },
|
|
40
41
|
'--agent': { kind: 'agent' },
|
|
41
42
|
'--no-agent': { kind: 'no-agent' },
|
|
43
|
+
'--no-checkout-access': { kind: 'flag' },
|
|
42
44
|
'--force': { kind: 'flag' },
|
|
43
45
|
'--worktree': { kind: 'flag' },
|
|
44
46
|
'--no-browser': { kind: 'flag' },
|
|
@@ -55,6 +57,11 @@ const flagOptions = new Set(
|
|
|
55
57
|
.filter(([, record]) => record.kind === 'flag')
|
|
56
58
|
.map(([name]) => name),
|
|
57
59
|
);
|
|
60
|
+
const repeatableValueOptions = new Set(
|
|
61
|
+
Object.entries(cliOptions)
|
|
62
|
+
.filter(([, record]) => record.kind === 'repeatable-value')
|
|
63
|
+
.map(([name]) => name),
|
|
64
|
+
);
|
|
58
65
|
const pathOptions = new Set(
|
|
59
66
|
Object.entries(cliOptions)
|
|
60
67
|
.filter(([, record]) => record.path)
|
|
@@ -75,19 +82,24 @@ Commands:
|
|
|
75
82
|
doctor [--json] [--deep]
|
|
76
83
|
Check review, agent, and pull request capabilities
|
|
77
84
|
cache Show or prune saved agent notes
|
|
85
|
+
config agent [NAME|--unset]
|
|
86
|
+
Show, set, or unset the default coding agent
|
|
78
87
|
|
|
79
88
|
Targets:
|
|
80
89
|
--branch NAME Show a remote branch against its default branch
|
|
81
90
|
--pr NUMBER|URL Show a GitHub pull request
|
|
82
91
|
--worktree Show only worktree changes against HEAD
|
|
83
|
-
--base REF --head REF
|
|
84
|
-
|
|
92
|
+
--base REF [--head REF]
|
|
93
|
+
Compare a base with the working tree, or show an exact range
|
|
85
94
|
|
|
86
95
|
Options:
|
|
87
96
|
--repo PATH|URL|OWNER/NAME
|
|
88
97
|
Repo to review (default: current repo)
|
|
89
98
|
--agent NAME Use codex, claude, copilot, cursor, or opencode
|
|
90
99
|
--no-agent Do not write agent notes
|
|
100
|
+
--no-checkout-access
|
|
101
|
+
Limit agent notes to the supplied snapshot
|
|
102
|
+
--exclude PATTERN Omit matching files from automatic agent context
|
|
91
103
|
--model NAME Model for agent notes
|
|
92
104
|
--reasoning LEVEL Agent reasoning effort when supported
|
|
93
105
|
--batch-size COUNT Maximum files per agent pass (default: ${batchSizeOption.default})
|
|
@@ -107,8 +119,9 @@ Options:
|
|
|
107
119
|
-h, --help Show this help
|
|
108
120
|
-v, --version Show the installed version
|
|
109
121
|
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
Agent choice:
|
|
123
|
+
Choice order is --no-agent, --agent, then the configured default.
|
|
124
|
+
Without any choice, an interactive terminal lists usable agents.
|
|
112
125
|
|
|
113
126
|
Cursor:
|
|
114
127
|
Requires Cursor Agent 2026.08.11 or newer. Uses the signed-in CLI in the
|
|
@@ -184,6 +197,31 @@ export function parseCliArgs(
|
|
|
184
197
|
pathExists = existsSync,
|
|
185
198
|
} = {},
|
|
186
199
|
) {
|
|
200
|
+
if (rawArgs[0] === 'config') {
|
|
201
|
+
if (rawArgs[1] !== 'agent') {
|
|
202
|
+
fail('Use: diffsplain config agent [NAME|--unset]');
|
|
203
|
+
}
|
|
204
|
+
if (rawArgs.length === 2) {
|
|
205
|
+
return { config: { kind: 'show' } };
|
|
206
|
+
}
|
|
207
|
+
if (rawArgs.length !== 3) {
|
|
208
|
+
fail('Use: diffsplain config agent [NAME|--unset]');
|
|
209
|
+
}
|
|
210
|
+
const value = rawArgs[2];
|
|
211
|
+
if (value === '--unset') {
|
|
212
|
+
return { config: { kind: 'unset' } };
|
|
213
|
+
}
|
|
214
|
+
if (value.startsWith('--')) {
|
|
215
|
+
fail('Use: diffsplain config agent [NAME|--unset]');
|
|
216
|
+
}
|
|
217
|
+
if (!codingAgents.includes(value)) {
|
|
218
|
+
fail(
|
|
219
|
+
`Unsupported agent "${value}". Choose ${enabledCodingAgents.join(', ')}.`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return { config: { kind: 'set', agent: value } };
|
|
223
|
+
}
|
|
224
|
+
|
|
187
225
|
if (rawArgs[0] === 'doctor') {
|
|
188
226
|
const options = new Set(rawArgs.slice(1));
|
|
189
227
|
for (const option of options) {
|
|
@@ -199,6 +237,7 @@ export function parseCliArgs(
|
|
|
199
237
|
|
|
200
238
|
const options = new Map();
|
|
201
239
|
const positionals = [];
|
|
240
|
+
const excludePatterns = [];
|
|
202
241
|
let agent;
|
|
203
242
|
let agentSet = false;
|
|
204
243
|
let noAgent = false;
|
|
@@ -246,10 +285,13 @@ export function parseCliArgs(
|
|
|
246
285
|
continue;
|
|
247
286
|
}
|
|
248
287
|
|
|
249
|
-
|
|
288
|
+
const repeatable = repeatableValueOptions.has(parsed.name);
|
|
289
|
+
if (!repeatable && !valueOptions.has(parsed.name)) {
|
|
250
290
|
fail(`Unknown option: ${parsed.name}`);
|
|
251
291
|
}
|
|
252
|
-
if (options.has(parsed.name))
|
|
292
|
+
if (!repeatable && options.has(parsed.name)) {
|
|
293
|
+
fail(`${parsed.name} was passed more than once`);
|
|
294
|
+
}
|
|
253
295
|
|
|
254
296
|
let value = parsed.value;
|
|
255
297
|
if (value === undefined) {
|
|
@@ -258,7 +300,8 @@ export function parseCliArgs(
|
|
|
258
300
|
index += 1;
|
|
259
301
|
}
|
|
260
302
|
if (!value) fail(`${parsed.name} needs a value`);
|
|
261
|
-
|
|
303
|
+
if (repeatable) excludePatterns.push(value);
|
|
304
|
+
else options.set(parsed.name, value);
|
|
262
305
|
}
|
|
263
306
|
|
|
264
307
|
if (options.has('--help')) return { help: true };
|
|
@@ -303,8 +346,8 @@ export function parseCliArgs(
|
|
|
303
346
|
if (worktree && (branch || pullRequest || base || head)) {
|
|
304
347
|
fail('--worktree cannot be combined with another target');
|
|
305
348
|
}
|
|
306
|
-
if (!branch && !pullRequest && !worktree &&
|
|
307
|
-
fail('--
|
|
349
|
+
if (!branch && !pullRequest && !worktree && head && !base) {
|
|
350
|
+
fail('--head must be used with --base');
|
|
308
351
|
}
|
|
309
352
|
if (pullRequest && !validPullRequest(pullRequest)) {
|
|
310
353
|
fail('--pr must be a positive number or a pull request URL');
|
|
@@ -359,6 +402,9 @@ export function parseCliArgs(
|
|
|
359
402
|
}
|
|
360
403
|
|
|
361
404
|
const feedArgs = [...commonArgs];
|
|
405
|
+
for (const pattern of excludePatterns) {
|
|
406
|
+
feedArgs.push(`--exclude=${pattern}`);
|
|
407
|
+
}
|
|
362
408
|
const supportRecordFile = options.get('--support-record-file');
|
|
363
409
|
if (supportRecordFile) {
|
|
364
410
|
feedArgs.push(
|
|
@@ -367,6 +413,9 @@ export function parseCliArgs(
|
|
|
367
413
|
);
|
|
368
414
|
}
|
|
369
415
|
const agentArgs = [...commonArgs];
|
|
416
|
+
for (const pattern of excludePatterns) {
|
|
417
|
+
agentArgs.push(`--exclude=${pattern}`);
|
|
418
|
+
}
|
|
370
419
|
if (options.has('--force')) agentArgs.push('--force');
|
|
371
420
|
for (const name of [
|
|
372
421
|
'--codex-bin',
|
|
@@ -382,6 +431,9 @@ export function parseCliArgs(
|
|
|
382
431
|
}
|
|
383
432
|
}
|
|
384
433
|
if (!noAgent) {
|
|
434
|
+
if (options.has('--no-checkout-access')) {
|
|
435
|
+
agentArgs.push('--no-checkout-access');
|
|
436
|
+
}
|
|
385
437
|
for (const name of ['--batch-size', '--jobs']) {
|
|
386
438
|
agentArgs.push(
|
|
387
439
|
name,
|
|
@@ -458,6 +510,8 @@ export function parseCliArgs(
|
|
|
458
510
|
version: false,
|
|
459
511
|
agentEnabled: !noAgent,
|
|
460
512
|
agent,
|
|
513
|
+
noCheckoutAccess: options.has('--no-checkout-access'),
|
|
514
|
+
excludePatterns,
|
|
461
515
|
model: options.get('--model'),
|
|
462
516
|
reasoning,
|
|
463
517
|
codexBin: options.get('--codex-bin'),
|