diffsplain 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { rm } from "node:fs/promises";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8
+
9
+ await rm(resolve(root, "dist/diff-data.json"), { force: true });
@@ -0,0 +1,293 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ const valueOptions = new Set([
5
+ '--repo',
6
+ '--branch',
7
+ '--pr',
8
+ '--base',
9
+ '--head',
10
+ '--remote',
11
+ '--summaries',
12
+ '--output',
13
+ '--cache-dir',
14
+ '--codex-bin',
15
+ '--model',
16
+ '--reasoning',
17
+ '--batch-size',
18
+ '--port',
19
+ ]);
20
+ const flagOptions = new Set([
21
+ '--help',
22
+ '--agent',
23
+ '--no-agent',
24
+ '--worktree',
25
+ ]);
26
+ const pathOptions = new Set([
27
+ '--summaries',
28
+ '--output',
29
+ '--cache-dir',
30
+ '--codex-bin',
31
+ ]);
32
+
33
+ export const helpText = `Usage: diffsplain [REPO] [options]
34
+
35
+ Show the current checkout against its default branch:
36
+ diffsplain
37
+
38
+ Targets:
39
+ --branch NAME Show a remote branch against its default branch
40
+ --pr NUMBER|URL Show a GitHub pull request
41
+ --worktree Show only worktree changes against HEAD
42
+ --base REF --head REF
43
+ Show an exact local Git range
44
+
45
+ Options:
46
+ --repo PATH|URL|OWNER/NAME
47
+ Repo to review (default: current repo)
48
+ --agent [codex] Coding agent (default: codex)
49
+ --no-agent Do not write agent notes
50
+ --model NAME Codex model for agent notes
51
+ --reasoning LEVEL Codex reasoning effort for agent notes
52
+ --batch-size COUNT Files per agent pass (default: 4)
53
+ --remote NAME|URL Git remote (default: origin)
54
+ --port NUMBER Local page port (default: 3000)
55
+ --help Show this help
56
+
57
+ Examples:
58
+ diffsplain
59
+ diffsplain --repo owner/project --pr 42
60
+ diffsplain owner/project --branch feature/search
61
+ diffsplain --agent codex`;
62
+
63
+ function fail(message) {
64
+ throw new Error(message);
65
+ }
66
+
67
+ function splitOption(argument) {
68
+ if (!argument.startsWith('--')) return undefined;
69
+ const separator = argument.indexOf('=');
70
+ if (separator === -1) return { name: argument, value: undefined };
71
+ return {
72
+ name: argument.slice(0, separator),
73
+ value: argument.slice(separator + 1),
74
+ };
75
+ }
76
+
77
+ function githubRepoFromPullRequest(value) {
78
+ try {
79
+ const url = new URL(value);
80
+ const match = url.pathname.match(/^\/([^/]+)\/([^/]+)\/pull\/\d+(?:\/|$)/);
81
+ if (!match) return undefined;
82
+ return `${url.origin}/${match[1]}/${match[2].replace(/\.git$/, '')}.git`;
83
+ } catch {
84
+ return undefined;
85
+ }
86
+ }
87
+
88
+ function remoteRepo(value, callerDirectory, pathExists) {
89
+ if (pathExists(resolve(callerDirectory, value))) return undefined;
90
+ if (
91
+ /^(?:https?|ssh|git|file):\/\//i.test(value) ||
92
+ /^(?:[^@/\s]+@)?[^:/\s]+:.+/.test(value)
93
+ ) {
94
+ return value;
95
+ }
96
+ if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/.test(value)) {
97
+ return `https://github.com/${value.replace(/\.git$/, '')}.git`;
98
+ }
99
+ return undefined;
100
+ }
101
+
102
+ export function parseCliArgs(
103
+ rawArgs,
104
+ {
105
+ callerDirectory = process.cwd(),
106
+ pathExists = existsSync,
107
+ } = {},
108
+ ) {
109
+ const options = new Map();
110
+ const positionals = [];
111
+ let agent = 'codex';
112
+ let agentSet = false;
113
+ let noAgent = false;
114
+
115
+ for (let index = 0; index < rawArgs.length; index += 1) {
116
+ const argument = rawArgs[index];
117
+ const parsed = splitOption(argument);
118
+ if (!parsed) {
119
+ positionals.push(argument);
120
+ continue;
121
+ }
122
+
123
+ if (parsed.name === '--agent') {
124
+ if (parsed.value !== undefined) {
125
+ if (!parsed.value) fail('--agent needs a value');
126
+ agent = parsed.value;
127
+ agentSet = true;
128
+ } else {
129
+ const next = rawArgs[index + 1];
130
+ if (next && !next.startsWith('-')) {
131
+ agent = next;
132
+ agentSet = true;
133
+ index += 1;
134
+ } else {
135
+ agentSet = true;
136
+ }
137
+ }
138
+ continue;
139
+ }
140
+
141
+ if (parsed.name === '--no-agent') {
142
+ if (parsed.value !== undefined) fail('--no-agent does not take a value');
143
+ noAgent = true;
144
+ continue;
145
+ }
146
+
147
+ if (flagOptions.has(parsed.name)) {
148
+ if (parsed.value !== undefined) {
149
+ fail(`${parsed.name} does not take a value`);
150
+ }
151
+ options.set(parsed.name, true);
152
+ continue;
153
+ }
154
+
155
+ if (!valueOptions.has(parsed.name)) {
156
+ fail(`Unknown option: ${parsed.name}`);
157
+ }
158
+ if (options.has(parsed.name)) fail(`${parsed.name} was passed more than once`);
159
+
160
+ let value = parsed.value;
161
+ if (value === undefined) {
162
+ value = rawArgs[index + 1];
163
+ if (!value || value.startsWith('--')) fail(`${parsed.name} needs a value`);
164
+ index += 1;
165
+ }
166
+ if (!value) fail(`${parsed.name} needs a value`);
167
+ options.set(parsed.name, value);
168
+ }
169
+
170
+ if (options.has('--help')) return { help: true };
171
+ if (positionals.length > 1) fail('Pass at most one repo');
172
+ if (positionals.length && options.has('--repo')) {
173
+ fail('Pass the repo once, either as REPO or with --repo');
174
+ }
175
+ if (noAgent && agentSet) fail('--agent and --no-agent cannot be used together');
176
+ if (!noAgent && agent !== 'codex') {
177
+ fail(`Unsupported agent "${agent}". Only "codex" is supported for now.`);
178
+ }
179
+
180
+ const branch = options.get('--branch');
181
+ const pullRequest = options.get('--pr');
182
+ const base = options.get('--base');
183
+ const head = options.get('--head');
184
+ const worktree = options.has('--worktree');
185
+ if (branch && pullRequest) fail('--branch and --pr cannot be used together');
186
+ if (pullRequest && (base || head)) {
187
+ fail('--pr cannot be used with --base or --head');
188
+ }
189
+ if (branch && head) fail('--branch cannot be used with --head');
190
+ if (worktree && (branch || pullRequest || base || head)) {
191
+ fail('--worktree cannot be combined with another target');
192
+ }
193
+ if (!branch && !pullRequest && !worktree && Boolean(base) !== Boolean(head)) {
194
+ fail('--base and --head must be used together');
195
+ }
196
+
197
+ const repoArgument = positionals[0] || options.get('--repo');
198
+ let remote = options.get('--remote');
199
+ let repo = callerDirectory;
200
+ if (repoArgument) {
201
+ const selectedRemote = remoteRepo(
202
+ repoArgument,
203
+ callerDirectory,
204
+ pathExists,
205
+ );
206
+ if (selectedRemote) {
207
+ if (remote) fail('--repo URL and --remote cannot be used together');
208
+ remote = selectedRemote;
209
+ } else {
210
+ repo = resolve(callerDirectory, repoArgument);
211
+ }
212
+ }
213
+
214
+ const pullRequestRemote = pullRequest
215
+ ? githubRepoFromPullRequest(pullRequest)
216
+ : undefined;
217
+ if (pullRequestRemote && !repoArgument && !remote) remote = pullRequestRemote;
218
+ if (repoArgument && remoteRepo(repoArgument, callerDirectory, pathExists)) {
219
+ if (!branch && !pullRequest) {
220
+ fail('A remote repo needs --branch or --pr');
221
+ }
222
+ }
223
+
224
+ const commonArgs = ['--repo', repo];
225
+ if (pullRequest) commonArgs.push('--pr', pullRequest);
226
+ if (branch) commonArgs.push('--branch', branch);
227
+ if (worktree) commonArgs.push('--worktree');
228
+ if (!pullRequest && !branch && !worktree && !base && !head) {
229
+ commonArgs.push('--checkout');
230
+ }
231
+ if (base) commonArgs.push('--base', base);
232
+ if (head) commonArgs.push('--head', head);
233
+ if (remote) commonArgs.push('--remote', remote);
234
+
235
+ for (const name of ['--summaries', '--output', '--cache-dir']) {
236
+ const value = options.get(name);
237
+ if (value) {
238
+ commonArgs.push(
239
+ name,
240
+ pathOptions.has(name) ? resolve(callerDirectory, value) : value,
241
+ );
242
+ }
243
+ }
244
+
245
+ const agentArgs = [...commonArgs];
246
+ for (const name of [
247
+ '--codex-bin',
248
+ '--model',
249
+ '--reasoning',
250
+ '--batch-size',
251
+ ]) {
252
+ const value = options.get(name);
253
+ if (value) {
254
+ agentArgs.push(
255
+ name,
256
+ pathOptions.has(name) ? resolve(callerDirectory, value) : value,
257
+ );
258
+ }
259
+ }
260
+
261
+ const reasoning = options.get('--reasoning');
262
+ if (
263
+ reasoning &&
264
+ !['minimal', 'low', 'medium', 'high', 'xhigh'].includes(
265
+ reasoning,
266
+ )
267
+ ) {
268
+ fail(
269
+ '--reasoning must be minimal, low, medium, high, or xhigh',
270
+ );
271
+ }
272
+ const batchSize = options.get('--batch-size');
273
+ if (
274
+ batchSize &&
275
+ (!/^[1-9]\d*$/.test(batchSize) || Number(batchSize) > 50)
276
+ ) {
277
+ fail('--batch-size must be a number from 1 to 50');
278
+ }
279
+
280
+ const portValue = options.get('--port') || '3000';
281
+ if (!/^\d+$/.test(portValue) || Number(portValue) > 65_535) {
282
+ fail('--port must be a number from 0 to 65535');
283
+ }
284
+
285
+ return {
286
+ help: false,
287
+ agentEnabled: !noAgent,
288
+ agent,
289
+ feedArgs: commonArgs,
290
+ agentArgs,
291
+ port: Number(portValue),
292
+ };
293
+ }