diffsplain 0.5.0 → 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 +17 -12
- package/dist/assets/{angular-html-CoQle5-T.js → angular-html-vBvPnd6a.js} +1 -1
- package/dist/assets/{angular-ts-BB_A1Lpy.js → angular-ts-CDzF1Z6a.js} +1 -1
- package/dist/assets/{apl-B7_4_FNq.js → apl-Bm9bxvP7.js} +1 -1
- package/dist/assets/{astro-CXVo4VE4.js → astro-RNrNWCYO.js} +1 -1
- package/dist/assets/{blade-BVX41dq7.js → blade-On-NBYUv.js} +1 -1
- package/dist/assets/{c-Dhyi8Nd2.js → c-hRmhFgD0.js} +1 -1
- package/dist/assets/{cobol-CU5oLeZj.js → cobol-CIqc3k2t.js} +1 -1
- package/dist/assets/{coffee-D_FU6gC_.js → coffee-Cd5EyvvS.js} +1 -1
- package/dist/assets/{cpp-CxoYLSi3.js → cpp-zXFAAp_S.js} +1 -1
- package/dist/assets/{crystal-BdgqBN3z.js → crystal-DRYY4cSw.js} +1 -1
- package/dist/assets/{css-wLk1vVwJ.js → css-C4op_MsD.js} +1 -1
- package/dist/assets/{edge-DMylWoGl.js → edge-d3rJkErf.js} +1 -1
- package/dist/assets/{elixir-Djmgto_Z.js → elixir-DPqlJhYM.js} +1 -1
- package/dist/assets/{elm-D7B-eBLA.js → elm-DgU37I8a.js} +1 -1
- package/dist/assets/{erb-g1sso1GW.js → erb-CZVB_ntC.js} +1 -1
- package/dist/assets/{git-rebase-DEAhxutZ.js → git-rebase-hUkltHZG.js} +1 -1
- package/dist/assets/{glimmer-js-CHvGFGMp.js → glimmer-js-5XOT9GQ8.js} +1 -1
- package/dist/assets/{glimmer-ts-DvhL705p.js → glimmer-ts-DT7D0Kfq.js} +1 -1
- package/dist/assets/{glsl-C8mS_t8I.js → glsl-Br4_Eh5O.js} +1 -1
- package/dist/assets/{graphql-DW2pnvqi.js → graphql--BmZ_AYP.js} +1 -1
- package/dist/assets/{hack-DYc1d3he.js → hack-CMdqGDoA.js} +1 -1
- package/dist/assets/{haml-_5OmzH31.js → haml-DLEOHGmg.js} +1 -1
- package/dist/assets/{handlebars-rN-CaXUf.js → handlebars-BfUftoV0.js} +1 -1
- package/dist/assets/{html-derivative-BovOubry.js → html-derivative-D5cZE6C4.js} +1 -1
- package/dist/assets/{html-DQnaJUjl.js → html-uOgx2iYC.js} +1 -1
- package/dist/assets/{http-BKTgwOYc.js → http-cW9c66J_.js} +1 -1
- package/dist/assets/{hurl-ChS3_IgD.js → hurl-BztURO2n.js} +1 -1
- package/dist/assets/{index-BLtRBc2n.css → index-C2p_1iAk.css} +1 -1
- package/dist/assets/{index-CEglkSs-.js → index-DZFM4-3L.js} +11 -11
- package/dist/assets/{java-Dgnyk--z.js → java-Cy4mnKaj.js} +1 -1
- package/dist/assets/{javascript-BfnrwkRN.js → javascript-_Z02t_9Q.js} +1 -1
- package/dist/assets/{jinja-CD2AIvOi.js → jinja-BppBm5gt.js} +1 -1
- package/dist/assets/{jison-DmKNw3Pl.js → jison-Dzns436M.js} +1 -1
- package/dist/assets/{json-CKB5a8aj.js → json-BYkfLOry.js} +1 -1
- package/dist/assets/{jsx-jJXrDRBM.js → jsx-DtAm8NUb.js} +1 -1
- package/dist/assets/{julia-G8vVBW-t.js → julia-CLIMRuh0.js} +1 -1
- package/dist/assets/{just-DMXLrLZL.js → just-DMg47an6.js} +1 -1
- package/dist/assets/{latex-DS3xuxld.js → latex-D05ztCOx.js} +1 -1
- package/dist/assets/{liquid-COQnD_C4.js → liquid-DpImQR4t.js} +1 -1
- package/dist/assets/{lua-BwMKwosd.js → lua--ZHUYQUU.js} +1 -1
- package/dist/assets/{marko-cQCK_WIk.js → marko-Dh3Xi08i.js} +1 -1
- package/dist/assets/{mdc-DQUjY13N.js → mdc-Br6Pt78u.js} +1 -1
- package/dist/assets/{nginx-C4Kv3DJi.js → nginx-CwSU6U6d.js} +1 -1
- package/dist/assets/{nim-ChzLzuYD.js → nim-CBbTwyeq.js} +1 -1
- package/dist/assets/{perl-z5xUiWAY.js → perl-D3jQu3LN.js} +1 -1
- package/dist/assets/{php-BHqm-Xeh.js → php-BMeXBzlU.js} +1 -1
- package/dist/assets/{pug-C9C_Llft.js → pug-BGdY1g5E.js} +1 -1
- package/dist/assets/{qml-dtpuwXJZ.js → qml-DsT5biza.js} +1 -1
- package/dist/assets/{r-BicQMs_c.js → r-CbS9PfN5.js} +1 -1
- package/dist/assets/{razor-D5h00YTZ.js → razor-Bwm8roHi.js} +1 -1
- package/dist/assets/{regexp-CTa8S86v.js → regexp-Bc_9DyAu.js} +1 -1
- package/dist/assets/{rst-Bg-b-7HK.js → rst-Cj1Aii91.js} +1 -1
- package/dist/assets/{ruby-BiWYm056.js → ruby-BPMtFZnc.js} +1 -1
- package/dist/assets/{sas-bJTdfgUz.js → sas-CoOau1t2.js} +1 -1
- package/dist/assets/{scss-0qkNyHr1.js → scss-CvfaI808.js} +1 -1
- package/dist/assets/{shellscript-3sDkDE9z.js → shellscript-B0C-TdLE.js} +1 -1
- package/dist/assets/{shellsession-D4EBhEmz.js → shellsession-UhmLiqcA.js} +1 -1
- package/dist/assets/{soy-DG9hUwLL.js → soy-D419u0RL.js} +1 -1
- package/dist/assets/{sql-B0UoKHkx.js → sql-Do9k0rRn.js} +1 -1
- package/dist/assets/{stata-CsQbuXQp.js → stata-x47J6BC4.js} +1 -1
- package/dist/assets/{surrealql-D63VLxos.js → surrealql-BERQ6pHu.js} +1 -1
- package/dist/assets/{svelte-BApdnAma.js → svelte-dRWkyqKW.js} +1 -1
- package/dist/assets/{templ-B64pN6Lz.js → templ-CTq-KoWN.js} +1 -1
- package/dist/assets/{tex-DR2UVIPF.js → tex-CdRGjyYI.js} +1 -1
- package/dist/assets/{ts-tags-IwhomvQv.js → ts-tags-CgaMqyVb.js} +1 -1
- package/dist/assets/{tsx-DROdxqzS.js → tsx-cpBQKcPG.js} +1 -1
- package/dist/assets/{twig-BtbUqpR4.js → twig-vbs_PM4-.js} +1 -1
- package/dist/assets/{typescript-CHpSjYZL.js → typescript-Cc--nwIo.js} +1 -1
- package/dist/assets/{vue-DCvEu_ug.js → vue-CgHwdBmB.js} +1 -1
- package/dist/assets/{vue-html-BgL4Rf-L.js → vue-html-BHfoMw47.js} +1 -1
- package/dist/assets/{vue-vine-CRNdhPTn.js → vue-vine-_lVRp_NK.js} +1 -1
- package/dist/assets/{xml-CkfPxZuz.js → xml-G4NVmTde.js} +1 -1
- package/dist/assets/{xsl-CteaoFSV.js → xsl-s0so2M1i.js} +1 -1
- package/dist/assets/{yaml-CUr3TDRN.js → yaml-CpsmW05t.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +49 -23
- package/scripts/access-token.mjs +21 -0
- package/scripts/build-diff-data.mjs +260 -39
- package/scripts/cache.mjs +301 -0
- package/scripts/cli-args.mjs +206 -57
- package/scripts/coding-agents.mjs +279 -162
- package/scripts/dev.mjs +218 -0
- package/scripts/doctor.mjs +265 -46
- package/scripts/generate-summaries.mjs +760 -260
- package/scripts/mock-agent.mjs +165 -0
- package/scripts/present.mjs +394 -85
- package/scripts/presenter-runtime.mjs +148 -0
- package/scripts/serve-built.mjs +343 -71
- package/scripts/summary-path.mjs +24 -7
- package/scripts/support-record.mjs +242 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
closeSync,
|
|
5
|
+
linkSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
openSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
statSync,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
utimesSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
} from 'node:fs';
|
|
17
|
+
import { hostname } from 'node:os';
|
|
18
|
+
import { dirname, join } from 'node:path';
|
|
19
|
+
import { defaultCacheRoot } from './summary-path.mjs';
|
|
20
|
+
|
|
21
|
+
export const leaseDurationMs = 5 * 60_000;
|
|
22
|
+
|
|
23
|
+
function files(root) {
|
|
24
|
+
try {
|
|
25
|
+
return readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
|
|
26
|
+
const path = join(root, entry.name);
|
|
27
|
+
return entry.isDirectory() ? files(path) : [path];
|
|
28
|
+
});
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function leaseRecord(path) {
|
|
35
|
+
try {
|
|
36
|
+
const value = JSON.parse(readFileSync(path, 'utf8'));
|
|
37
|
+
return typeof value?.token === 'string' ? value : undefined;
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function leaseIsActive(path, now = Date.now(), duration = leaseDurationMs) {
|
|
44
|
+
try {
|
|
45
|
+
return now - statSync(path).mtimeMs < duration;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createLease(path, record, duration) {
|
|
52
|
+
const descriptor = openSync(path, 'wx', 0o600);
|
|
53
|
+
writeFileSync(descriptor, `${JSON.stringify(record)}\n`);
|
|
54
|
+
closeSync(descriptor);
|
|
55
|
+
return { path, token: record.token, duration };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renameLease(path, stalePath) {
|
|
59
|
+
try {
|
|
60
|
+
renameSync(path, stalePath);
|
|
61
|
+
return true;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function restoreLease(path, stalePath) {
|
|
69
|
+
try {
|
|
70
|
+
linkSync(stalePath, path);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
73
|
+
}
|
|
74
|
+
rmSync(stalePath, { force: true });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function removeStaleLease(path, observedToken) {
|
|
78
|
+
const stalePath = `${path}.stale-${randomUUID()}`;
|
|
79
|
+
if (!renameLease(path, stalePath)) return false;
|
|
80
|
+
if (leaseRecord(stalePath)?.token === observedToken) {
|
|
81
|
+
rmSync(stalePath, { force: true });
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
restoreLease(path, stalePath);
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function rejectNonConflict(error) {
|
|
89
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function rejectActiveLease(active, path) {
|
|
93
|
+
if (active) {
|
|
94
|
+
throw new Error(`Notes for this target are already being generated: ${path}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function handleLeaseConflict(error, path, now, duration) {
|
|
99
|
+
rejectNonConflict(error);
|
|
100
|
+
const observed = leaseRecord(path);
|
|
101
|
+
rejectActiveLease(leaseIsActive(path, now, duration), path);
|
|
102
|
+
removeStaleLease(path, observed?.token);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function acquireLease(path, {
|
|
106
|
+
token = randomUUID(),
|
|
107
|
+
now = Date.now(),
|
|
108
|
+
duration = leaseDurationMs,
|
|
109
|
+
pid = process.pid,
|
|
110
|
+
leaseHostname = hostname(),
|
|
111
|
+
} = {}) {
|
|
112
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
113
|
+
for (;;) {
|
|
114
|
+
try {
|
|
115
|
+
return createLease(
|
|
116
|
+
path,
|
|
117
|
+
{ token, startedAt: now, pid, hostname: leaseHostname },
|
|
118
|
+
duration,
|
|
119
|
+
);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
handleLeaseConflict(error, path, now, duration);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function assertLease(lease) {
|
|
127
|
+
if (leaseRecord(lease.path)?.token !== lease.token) {
|
|
128
|
+
throw new Error('This process no longer owns the note cache');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function refreshLease(lease, now = Date.now()) {
|
|
133
|
+
assertLease(lease);
|
|
134
|
+
utimesSync(lease.path, new Date(now), new Date(now));
|
|
135
|
+
assertLease(lease);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function releaseLease(lease) {
|
|
139
|
+
assertLease(lease);
|
|
140
|
+
unlinkSync(lease.path);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function writePrivateFile(path, value) {
|
|
144
|
+
writeFileAtomic(path, value, 0o600);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function existingFileMode(path) {
|
|
148
|
+
try {
|
|
149
|
+
return statSync(path).mode & 0o777;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function chmodIfSet(path, mode) {
|
|
157
|
+
if (mode !== undefined) chmodSync(path, mode);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function writeFileAtomic(path, value, mode) {
|
|
161
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
162
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
163
|
+
const outputMode = mode === undefined ? existingFileMode(path) : mode;
|
|
164
|
+
writeFileSync(
|
|
165
|
+
temporary,
|
|
166
|
+
value,
|
|
167
|
+
outputMode === undefined ? undefined : { mode: outputMode },
|
|
168
|
+
);
|
|
169
|
+
chmodIfSet(temporary, outputMode);
|
|
170
|
+
renameSync(temporary, path);
|
|
171
|
+
chmodIfSet(path, outputMode);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function publishLeaseFile(
|
|
175
|
+
lease,
|
|
176
|
+
path,
|
|
177
|
+
value,
|
|
178
|
+
{ privateFile = true } = {},
|
|
179
|
+
) {
|
|
180
|
+
assertLease(lease);
|
|
181
|
+
refreshLease(lease);
|
|
182
|
+
if (privateFile) writePrivateFile(path, value);
|
|
183
|
+
else writeFileAtomic(path, value);
|
|
184
|
+
assertLease(lease);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function active(path, now) {
|
|
188
|
+
return leaseIsActive(`${path}.lock`, now);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function sameEntry(left, right) {
|
|
192
|
+
return (
|
|
193
|
+
left.dev === right.dev &&
|
|
194
|
+
left.ino === right.ino &&
|
|
195
|
+
left.size === right.size &&
|
|
196
|
+
left.mtimeMs === right.mtimeMs
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function fileSize(path) {
|
|
201
|
+
try {
|
|
202
|
+
return statSync(path).size;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function removedCacheEntry() {
|
|
210
|
+
return { removed: true, retainedActive: false, size: 0 };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function cacheEntryState(entry, removedPath, now) {
|
|
214
|
+
return {
|
|
215
|
+
changed: !sameEntry(entry, statSync(removedPath)),
|
|
216
|
+
retainedActive: active(entry.path, now),
|
|
217
|
+
replaced: typeof fileSize(entry.path) === 'number',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function restoreCacheEntry(entry, removedPath, retainedActive) {
|
|
222
|
+
restoreLease(entry.path, removedPath);
|
|
223
|
+
return {
|
|
224
|
+
removed: false,
|
|
225
|
+
retainedActive,
|
|
226
|
+
size: fileSize(entry.path) ?? 0,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function removeCacheEntry(entry, now = Date.now()) {
|
|
231
|
+
const removedPath = `${entry.path}.remove-${randomUUID()}`;
|
|
232
|
+
if (!renameLease(entry.path, removedPath)) return removedCacheEntry();
|
|
233
|
+
const state = cacheEntryState(entry, removedPath, now);
|
|
234
|
+
if ([state.changed, state.retainedActive, state.replaced].includes(true)) {
|
|
235
|
+
return restoreCacheEntry(entry, removedPath, state.retainedActive);
|
|
236
|
+
}
|
|
237
|
+
rmSync(removedPath, { force: true });
|
|
238
|
+
return removedCacheEntry();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function entries(cacheRoot) {
|
|
242
|
+
return files(join(cacheRoot, 'summaries'))
|
|
243
|
+
.filter((path) => path.endsWith('.json'))
|
|
244
|
+
.map((path) => ({ path, ...statSync(path) }));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function activeLeases(cacheRoot, now) {
|
|
248
|
+
return files(join(cacheRoot, 'summaries')).filter(
|
|
249
|
+
(path) => path.endsWith('.json.lock') && leaseIsActive(path, now),
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function cacheStatus({ cacheRoot = defaultCacheRoot(), now = Date.now() } = {}) {
|
|
254
|
+
const cached = entries(cacheRoot);
|
|
255
|
+
const bytes = cached.reduce((sum, entry) => sum + entry.size, 0);
|
|
256
|
+
const ages = cached.map((entry) => now - entry.mtimeMs);
|
|
257
|
+
return {
|
|
258
|
+
location: cacheRoot,
|
|
259
|
+
entries: cached.length,
|
|
260
|
+
bytes,
|
|
261
|
+
ageMs: ages.length ? Math.max(...ages) : 0,
|
|
262
|
+
active: activeLeases(cacheRoot, now).length,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function shouldPrune(entry, { maxAgeMs, maxBytes, now, bytes }) {
|
|
267
|
+
const overAge =
|
|
268
|
+
maxAgeMs !== undefined && now - entry.mtimeMs > maxAgeMs;
|
|
269
|
+
const overSize = maxBytes !== undefined && bytes > maxBytes;
|
|
270
|
+
return overAge || overSize;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function applyPruneResult(entry, result, removed, retainedActive) {
|
|
274
|
+
if (result.removed) {
|
|
275
|
+
removed.push(entry.path);
|
|
276
|
+
return -entry.size;
|
|
277
|
+
}
|
|
278
|
+
if (result.retainedActive) retainedActive.push(entry.path);
|
|
279
|
+
return result.size - entry.size;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function pruneCache({ cacheRoot = defaultCacheRoot(), maxAgeMs, maxBytes, now = Date.now() } = {}) {
|
|
283
|
+
const cached = entries(cacheRoot).sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
284
|
+
let bytes = cached.reduce((sum, entry) => sum + entry.size, 0);
|
|
285
|
+
const removed = [];
|
|
286
|
+
const retainedActive = [];
|
|
287
|
+
for (const entry of cached) {
|
|
288
|
+
if (!shouldPrune(entry, { maxAgeMs, maxBytes, now, bytes })) continue;
|
|
289
|
+
const result = removeCacheEntry(entry, now);
|
|
290
|
+
bytes += applyPruneResult(entry, result, removed, retainedActive);
|
|
291
|
+
}
|
|
292
|
+
return { removed, retainedActive, bytes };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function clearCache(options) {
|
|
296
|
+
return pruneCache({ ...options, maxAgeMs: 0 });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function formatCacheStatus(status) {
|
|
300
|
+
return `Location: ${status.location}\nSize: ${status.bytes} bytes\nOldest entry: ${Math.floor(status.ageMs / 1000)} seconds\nActive use: ${status.active} target${status.active === 1 ? '' : 's'}`;
|
|
301
|
+
}
|
package/scripts/cli-args.mjs
CHANGED
|
@@ -1,38 +1,70 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
'--
|
|
24
|
-
'--
|
|
25
|
-
'--
|
|
26
|
-
'--
|
|
27
|
-
'--
|
|
28
|
-
'--
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
'--
|
|
32
|
-
'--
|
|
33
|
-
'--
|
|
34
|
-
'--
|
|
35
|
-
|
|
3
|
+
import {
|
|
4
|
+
assertReasoningSupported,
|
|
5
|
+
codingAgents,
|
|
6
|
+
enabledCodingAgents,
|
|
7
|
+
} from './coding-agents.mjs';
|
|
8
|
+
|
|
9
|
+
function defineCliOptions(records) {
|
|
10
|
+
return Object.freeze(
|
|
11
|
+
Object.fromEntries(
|
|
12
|
+
Object.entries(records).map(([name, record]) => [
|
|
13
|
+
name,
|
|
14
|
+
Object.freeze(record),
|
|
15
|
+
]),
|
|
16
|
+
),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const cliOptions = defineCliOptions({
|
|
21
|
+
'--repo': { kind: 'value' },
|
|
22
|
+
'--branch': { kind: 'value' },
|
|
23
|
+
'--pr': { kind: 'value' },
|
|
24
|
+
'--base': { kind: 'value' },
|
|
25
|
+
'--head': { kind: 'value' },
|
|
26
|
+
'--remote': { kind: 'value' },
|
|
27
|
+
'--summaries': { kind: 'value', path: true },
|
|
28
|
+
'--output': { kind: 'value', path: true },
|
|
29
|
+
'--cache-dir': { kind: 'value', path: true },
|
|
30
|
+
'--codex-bin': { kind: 'value', path: true },
|
|
31
|
+
'--support-record-file': { kind: 'value', path: true },
|
|
32
|
+
'--model': { kind: 'value' },
|
|
33
|
+
'--reasoning': { kind: 'value' },
|
|
34
|
+
'--batch-size': { kind: 'value', default: 12, min: 1, max: 50 },
|
|
35
|
+
'--jobs': { kind: 'value', default: 3, min: 1, max: 8 },
|
|
36
|
+
'--port': { kind: 'value', default: 2299, min: 0, max: 65_535 },
|
|
37
|
+
'--host': { kind: 'value', default: 'localhost' },
|
|
38
|
+
'--help': { kind: 'flag' },
|
|
39
|
+
'--version': { kind: 'flag' },
|
|
40
|
+
'--agent': { kind: 'agent' },
|
|
41
|
+
'--no-agent': { kind: 'no-agent' },
|
|
42
|
+
'--force': { kind: 'flag' },
|
|
43
|
+
'--worktree': { kind: 'flag' },
|
|
44
|
+
'--no-browser': { kind: 'flag' },
|
|
45
|
+
'--support-record': { kind: 'flag' },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const valueOptions = new Set(
|
|
49
|
+
Object.entries(cliOptions)
|
|
50
|
+
.filter(([, record]) => record.kind === 'value')
|
|
51
|
+
.map(([name]) => name),
|
|
52
|
+
);
|
|
53
|
+
const flagOptions = new Set(
|
|
54
|
+
Object.entries(cliOptions)
|
|
55
|
+
.filter(([, record]) => record.kind === 'flag')
|
|
56
|
+
.map(([name]) => name),
|
|
57
|
+
);
|
|
58
|
+
const pathOptions = new Set(
|
|
59
|
+
Object.entries(cliOptions)
|
|
60
|
+
.filter(([, record]) => record.path)
|
|
61
|
+
.map(([name]) => name),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const batchSizeOption = cliOptions['--batch-size'];
|
|
65
|
+
const jobsOption = cliOptions['--jobs'];
|
|
66
|
+
const portOption = cliOptions['--port'];
|
|
67
|
+
const hostOption = cliOptions['--host'];
|
|
36
68
|
|
|
37
69
|
export const helpText = `Usage: diffsplain [REPO] [options]
|
|
38
70
|
|
|
@@ -40,7 +72,9 @@ Show the current checkout against its default branch:
|
|
|
40
72
|
diffsplain
|
|
41
73
|
|
|
42
74
|
Commands:
|
|
43
|
-
doctor
|
|
75
|
+
doctor [--json] [--deep]
|
|
76
|
+
Check review, agent, and pull request capabilities
|
|
77
|
+
cache Show or prune saved agent notes
|
|
44
78
|
|
|
45
79
|
Targets:
|
|
46
80
|
--branch NAME Show a remote branch against its default branch
|
|
@@ -52,24 +86,39 @@ Targets:
|
|
|
52
86
|
Options:
|
|
53
87
|
--repo PATH|URL|OWNER/NAME
|
|
54
88
|
Repo to review (default: current repo)
|
|
55
|
-
--agent NAME Use codex, claude, copilot,
|
|
89
|
+
--agent NAME Use codex, claude, copilot, or opencode
|
|
56
90
|
--no-agent Do not write agent notes
|
|
57
91
|
--model NAME Model for agent notes
|
|
58
92
|
--reasoning LEVEL Agent reasoning effort when supported
|
|
59
|
-
--batch-size COUNT Maximum files per agent pass (default:
|
|
60
|
-
--jobs COUNT Agent passes to run at once (default:
|
|
93
|
+
--batch-size COUNT Maximum files per agent pass (default: ${batchSizeOption.default})
|
|
94
|
+
--jobs COUNT Agent passes to run at once (default: ${jobsOption.default})
|
|
61
95
|
--force Regenerate all agent notes
|
|
96
|
+
--support-record Print a safe record if agent notes fail
|
|
97
|
+
--support-record-file FILE
|
|
98
|
+
Write a safe record if agent notes fail
|
|
62
99
|
--remote NAME|URL Git remote (default: origin)
|
|
63
|
-
--port NUMBER Local page port (default:
|
|
100
|
+
--port NUMBER Local page port (default: ${portOption.default})
|
|
101
|
+
--host ADDRESS Page bind address (default: ${hostOption.default})
|
|
102
|
+
--no-browser Do not open the page in a browser
|
|
103
|
+
--summaries FILE Saved agent-note file
|
|
104
|
+
--output FILE Live snapshot file
|
|
105
|
+
--cache-dir PATH Bare Git cache folder
|
|
106
|
+
--codex-bin PATH Codex executable
|
|
64
107
|
-h, --help Show this help
|
|
65
108
|
-v, --version Show the installed version
|
|
66
109
|
|
|
67
110
|
Agent fallback:
|
|
68
|
-
codex, claude, copilot,
|
|
111
|
+
codex, claude, copilot, opencode
|
|
112
|
+
|
|
113
|
+
Cursor:
|
|
114
|
+
Disabled because Cursor Agent has no supported read-only, no-network,
|
|
115
|
+
no-tool mode
|
|
69
116
|
|
|
70
117
|
Examples:
|
|
71
118
|
diffsplain
|
|
72
119
|
diffsplain doctor
|
|
120
|
+
diffsplain doctor --json
|
|
121
|
+
diffsplain cache status
|
|
73
122
|
diffsplain --repo owner/project --pr 42
|
|
74
123
|
diffsplain owner/project --branch feature/search
|
|
75
124
|
diffsplain --agent claude`;
|
|
@@ -101,6 +150,19 @@ function githubRepoFromPullRequest(value) {
|
|
|
101
150
|
}
|
|
102
151
|
}
|
|
103
152
|
|
|
153
|
+
function validPullRequest(value) {
|
|
154
|
+
if (/^[1-9]\d*$/.test(value)) return true;
|
|
155
|
+
try {
|
|
156
|
+
const url = new URL(value);
|
|
157
|
+
return (
|
|
158
|
+
(url.protocol === 'https:' || url.protocol === 'http:') &&
|
|
159
|
+
/^\/[^/]+\/[^/]+\/pull\/[1-9]\d*(?:\/|$)/.test(url.pathname)
|
|
160
|
+
);
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
104
166
|
function remoteRepo(value, callerDirectory, pathExists) {
|
|
105
167
|
if (pathExists(resolve(callerDirectory, value))) return undefined;
|
|
106
168
|
if (
|
|
@@ -123,8 +185,16 @@ export function parseCliArgs(
|
|
|
123
185
|
} = {},
|
|
124
186
|
) {
|
|
125
187
|
if (rawArgs[0] === 'doctor') {
|
|
126
|
-
|
|
127
|
-
|
|
188
|
+
const options = new Set(rawArgs.slice(1));
|
|
189
|
+
for (const option of options) {
|
|
190
|
+
if (!['--json', '--deep'].includes(option)) {
|
|
191
|
+
fail('doctor only accepts --json and --deep');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (options.size !== rawArgs.length - 1) {
|
|
195
|
+
fail('doctor options can only be passed once');
|
|
196
|
+
}
|
|
197
|
+
return { doctor: { json: options.has('--json'), deep: options.has('--deep') } };
|
|
128
198
|
}
|
|
129
199
|
|
|
130
200
|
const options = new Map();
|
|
@@ -137,32 +207,38 @@ export function parseCliArgs(
|
|
|
137
207
|
const argument = rawArgs[index];
|
|
138
208
|
const parsed = splitOption(argument);
|
|
139
209
|
if (!parsed) {
|
|
210
|
+
if (argument.startsWith('-')) fail(`Unknown option: ${argument}`);
|
|
211
|
+
if (!argument) fail('Repo cannot be empty');
|
|
140
212
|
positionals.push(argument);
|
|
141
213
|
continue;
|
|
142
214
|
}
|
|
143
215
|
|
|
144
216
|
if (parsed.name === '--agent') {
|
|
217
|
+
if (agentSet) fail('--agent was passed more than once');
|
|
145
218
|
agentSet = true;
|
|
146
219
|
if (parsed.value !== undefined) {
|
|
147
220
|
if (!parsed.value) fail('--agent needs a value');
|
|
148
221
|
agent = parsed.value;
|
|
149
222
|
} else {
|
|
150
223
|
const next = rawArgs[index + 1];
|
|
151
|
-
if (next
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
224
|
+
if (!next || splitOption(next)) fail('--agent needs a value');
|
|
225
|
+
agent = next;
|
|
226
|
+
index += 1;
|
|
155
227
|
}
|
|
156
228
|
continue;
|
|
157
229
|
}
|
|
158
230
|
|
|
159
231
|
if (parsed.name === '--no-agent') {
|
|
232
|
+
if (noAgent) fail('--no-agent was passed more than once');
|
|
160
233
|
if (parsed.value !== undefined) fail('--no-agent does not take a value');
|
|
161
234
|
noAgent = true;
|
|
162
235
|
continue;
|
|
163
236
|
}
|
|
164
237
|
|
|
165
238
|
if (flagOptions.has(parsed.name)) {
|
|
239
|
+
if (options.has(parsed.name)) {
|
|
240
|
+
fail(`${parsed.name} was passed more than once`);
|
|
241
|
+
}
|
|
166
242
|
if (parsed.value !== undefined) {
|
|
167
243
|
fail(`${parsed.name} does not take a value`);
|
|
168
244
|
}
|
|
@@ -178,7 +254,7 @@ export function parseCliArgs(
|
|
|
178
254
|
let value = parsed.value;
|
|
179
255
|
if (value === undefined) {
|
|
180
256
|
value = rawArgs[index + 1];
|
|
181
|
-
if (!value || value
|
|
257
|
+
if (!value || splitOption(value)) fail(`${parsed.name} needs a value`);
|
|
182
258
|
index += 1;
|
|
183
259
|
}
|
|
184
260
|
if (!value) fail(`${parsed.name} needs a value`);
|
|
@@ -192,9 +268,25 @@ export function parseCliArgs(
|
|
|
192
268
|
fail('Pass the repo once, either as REPO or with --repo');
|
|
193
269
|
}
|
|
194
270
|
if (noAgent && agentSet) fail('--agent and --no-agent cannot be used together');
|
|
271
|
+
if (noAgent && options.has('--summaries')) {
|
|
272
|
+
fail('--no-agent cannot be used with --summaries');
|
|
273
|
+
}
|
|
274
|
+
if (
|
|
275
|
+
noAgent &&
|
|
276
|
+
(options.has('--support-record') ||
|
|
277
|
+
options.has('--support-record-file'))
|
|
278
|
+
) {
|
|
279
|
+
fail('--no-agent cannot be used with a support record');
|
|
280
|
+
}
|
|
281
|
+
if (
|
|
282
|
+
options.has('--support-record') &&
|
|
283
|
+
options.has('--support-record-file')
|
|
284
|
+
) {
|
|
285
|
+
fail('Pass either --support-record or --support-record-file, not both');
|
|
286
|
+
}
|
|
195
287
|
if (!noAgent && agent && !codingAgents.includes(agent)) {
|
|
196
288
|
fail(
|
|
197
|
-
`Unsupported agent "${agent}". Choose ${
|
|
289
|
+
`Unsupported agent "${agent}". Choose ${enabledCodingAgents.join(', ')}.`,
|
|
198
290
|
);
|
|
199
291
|
}
|
|
200
292
|
|
|
@@ -214,6 +306,9 @@ export function parseCliArgs(
|
|
|
214
306
|
if (!branch && !pullRequest && !worktree && Boolean(base) !== Boolean(head)) {
|
|
215
307
|
fail('--base and --head must be used together');
|
|
216
308
|
}
|
|
309
|
+
if (pullRequest && !validPullRequest(pullRequest)) {
|
|
310
|
+
fail('--pr must be a positive number or a pull request URL');
|
|
311
|
+
}
|
|
217
312
|
|
|
218
313
|
const repoArgument = positionals[0] || options.get('--repo');
|
|
219
314
|
let remote = options.get('--remote');
|
|
@@ -263,14 +358,20 @@ export function parseCliArgs(
|
|
|
263
358
|
}
|
|
264
359
|
}
|
|
265
360
|
|
|
361
|
+
const feedArgs = [...commonArgs];
|
|
362
|
+
const supportRecordFile = options.get('--support-record-file');
|
|
363
|
+
if (supportRecordFile) {
|
|
364
|
+
feedArgs.push(
|
|
365
|
+
'--exclude-output',
|
|
366
|
+
resolve(callerDirectory, supportRecordFile),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
266
369
|
const agentArgs = [...commonArgs];
|
|
267
370
|
if (options.has('--force')) agentArgs.push('--force');
|
|
268
371
|
for (const name of [
|
|
269
372
|
'--codex-bin',
|
|
270
373
|
'--model',
|
|
271
374
|
'--reasoning',
|
|
272
|
-
'--batch-size',
|
|
273
|
-
'--jobs',
|
|
274
375
|
]) {
|
|
275
376
|
const value = options.get(name);
|
|
276
377
|
if (value) {
|
|
@@ -280,6 +381,23 @@ export function parseCliArgs(
|
|
|
280
381
|
);
|
|
281
382
|
}
|
|
282
383
|
}
|
|
384
|
+
if (!noAgent) {
|
|
385
|
+
for (const name of ['--batch-size', '--jobs']) {
|
|
386
|
+
agentArgs.push(
|
|
387
|
+
name,
|
|
388
|
+
options.get(name) || String(cliOptions[name].default),
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (supportRecordFile) {
|
|
393
|
+
agentArgs.push(
|
|
394
|
+
'--support-record-file',
|
|
395
|
+
resolve(callerDirectory, supportRecordFile),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
if (options.has('--support-record')) {
|
|
399
|
+
agentArgs.push('--support-record');
|
|
400
|
+
}
|
|
283
401
|
|
|
284
402
|
const reasoning = options.get('--reasoning');
|
|
285
403
|
if (
|
|
@@ -292,36 +410,67 @@ export function parseCliArgs(
|
|
|
292
410
|
'--reasoning must be minimal, low, medium, high, or xhigh',
|
|
293
411
|
);
|
|
294
412
|
}
|
|
413
|
+
if (reasoning && agent) assertReasoningSupported(agent, reasoning);
|
|
295
414
|
const batchSize = options.get('--batch-size');
|
|
296
415
|
if (
|
|
297
416
|
batchSize &&
|
|
298
|
-
(
|
|
417
|
+
(
|
|
418
|
+
!/^[1-9]\d*$/.test(batchSize) ||
|
|
419
|
+
Number(batchSize) < batchSizeOption.min ||
|
|
420
|
+
Number(batchSize) > batchSizeOption.max
|
|
421
|
+
)
|
|
299
422
|
) {
|
|
300
|
-
fail(
|
|
423
|
+
fail(
|
|
424
|
+
`--batch-size must be a number from ${batchSizeOption.min} to ${batchSizeOption.max}`,
|
|
425
|
+
);
|
|
301
426
|
}
|
|
302
427
|
const jobs = options.get('--jobs');
|
|
303
428
|
if (
|
|
304
429
|
jobs &&
|
|
305
|
-
(
|
|
430
|
+
(
|
|
431
|
+
!/^[1-9]\d*$/.test(jobs) ||
|
|
432
|
+
Number(jobs) < jobsOption.min ||
|
|
433
|
+
Number(jobs) > jobsOption.max
|
|
434
|
+
)
|
|
306
435
|
) {
|
|
307
|
-
fail(
|
|
436
|
+
fail(
|
|
437
|
+
`--jobs must be a number from ${jobsOption.min} to ${jobsOption.max}`,
|
|
438
|
+
);
|
|
308
439
|
}
|
|
309
440
|
|
|
310
|
-
const portValue = options.get('--port') ||
|
|
311
|
-
if (
|
|
312
|
-
|
|
441
|
+
const portValue = options.get('--port') || String(portOption.default);
|
|
442
|
+
if (
|
|
443
|
+
!/^\d+$/.test(portValue) ||
|
|
444
|
+
Number(portValue) < portOption.min ||
|
|
445
|
+
Number(portValue) > portOption.max
|
|
446
|
+
) {
|
|
447
|
+
fail(
|
|
448
|
+
`--port must be a number from ${portOption.min} to ${portOption.max}`,
|
|
449
|
+
);
|
|
313
450
|
}
|
|
451
|
+
const host = (options.get('--host') || hostOption.default).replace(
|
|
452
|
+
/^\[|\]$/g,
|
|
453
|
+
'',
|
|
454
|
+
);
|
|
314
455
|
|
|
315
456
|
return {
|
|
316
457
|
help: false,
|
|
317
458
|
version: false,
|
|
318
459
|
agentEnabled: !noAgent,
|
|
319
460
|
agent,
|
|
461
|
+
model: options.get('--model'),
|
|
462
|
+
reasoning,
|
|
320
463
|
codexBin: options.get('--codex-bin'),
|
|
321
|
-
feedArgs
|
|
464
|
+
feedArgs,
|
|
322
465
|
agentArgs,
|
|
466
|
+
supportRecord: options.has('--support-record'),
|
|
467
|
+
supportRecordFile: supportRecordFile
|
|
468
|
+
? resolve(callerDirectory, supportRecordFile)
|
|
469
|
+
: undefined,
|
|
323
470
|
port: Number(portValue),
|
|
324
471
|
portWasPassed: options.has('--port'),
|
|
472
|
+
host,
|
|
473
|
+
browserEnabled: !options.has('--no-browser'),
|
|
325
474
|
forceSummaryRegeneration: options.has('--force'),
|
|
326
475
|
};
|
|
327
476
|
}
|