diffing 0.16.1 → 0.17.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/dist/{cli-agent-Dk0yRtrA.mjs → cli-agent-qNwkSjxK.mjs} +22 -7
- package/dist/cli.mjs +3 -3
- package/dist/{mcp-BgrRjzn3.mjs → mcp-DhynPewa.mjs} +35 -19
- package/dist/native/tui-darwin-arm64/diffing-tui +0 -0
- package/dist/native/tui-darwin-x64/diffing-tui +0 -0
- package/dist/native/tui-linux-arm64-gnu/diffing-tui +0 -0
- package/dist/native/tui-linux-arm64-musl/diffing-tui +0 -0
- package/dist/native/tui-linux-x64-gnu/diffing-tui +0 -0
- package/dist/native/tui-linux-x64-musl/diffing-tui +0 -0
- package/dist/native/tui-win32-x64-msvc/diffing-tui.exe +0 -0
- package/dist/{server-Ct9QuV1l.mjs → server-CEbp7K-k.mjs} +266 -29
- package/extensions/pi/index.ts +1 -0
- package/package.json +2 -1
|
@@ -25,6 +25,13 @@ const EXIT_AWAIT_TIMEOUT = 2;
|
|
|
25
25
|
const EXIT_NO_SERVER = 3;
|
|
26
26
|
const EXIT_NOT_FOUND = 4;
|
|
27
27
|
const EXIT_USAGE = 5;
|
|
28
|
+
function validateInspectSelectors(resource, file, path) {
|
|
29
|
+
const hasFile = file != null && file !== "";
|
|
30
|
+
const hasPath = path != null && path !== "";
|
|
31
|
+
if ((resource === "hunks" || resource === "slice") && hasFile && hasPath) return `diffing inspect ${resource}: --path and --file are mutually exclusive`;
|
|
32
|
+
if ((resource === "hunks" || resource === "slice") && !hasFile && !hasPath) return `diffing inspect ${resource}: --file or --path is required`;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
28
35
|
let activeCapability;
|
|
29
36
|
let activeAuthToken;
|
|
30
37
|
/** Resolve the running server's base URL from the lockfile, or exit cleanly. */
|
|
@@ -880,6 +887,8 @@ async function inspect(args) {
|
|
|
880
887
|
cursor: { type: "string" },
|
|
881
888
|
limit: { type: "string" },
|
|
882
889
|
file: { type: "string" },
|
|
890
|
+
path: { type: "string" },
|
|
891
|
+
exclude: { type: "string" },
|
|
883
892
|
generation: { type: "string" },
|
|
884
893
|
start: { type: "string" },
|
|
885
894
|
row: { type: "string" },
|
|
@@ -906,12 +915,13 @@ async function inspect(args) {
|
|
|
906
915
|
console.error(`Usage: diffing inspect <summary|files|hunks|slice|search> [options]
|
|
907
916
|
|
|
908
917
|
Read bounded data from a running session (web, TUI, or gh-pr) without transferring the full patch.
|
|
909
|
-
summary
|
|
910
|
-
files [--cursor N] [--limit N]
|
|
911
|
-
hunks --file N [--cursor N] [--limit N] [--generation N]
|
|
912
|
-
slice --file N [--start N] [--max-lines N] [--max-bytes N] [--generation N]
|
|
913
|
-
search <text>|--query <text> [--file N] [--row N] [--limit N] [--max-bytes N] [--generation N]
|
|
918
|
+
summary [--exclude lockfiles]
|
|
919
|
+
files [--path GLOB] [--cursor N] [--limit N]
|
|
920
|
+
hunks (--file N | --path GLOB) [--cursor N] [--limit N] [--generation N]
|
|
921
|
+
slice (--file N | --path GLOB) [--start N] [--max-lines N] [--max-bytes N] [--generation N]
|
|
922
|
+
search <text>|--query <text> [--path GLOB] [--file N] [--row N] [--limit N] [--max-bytes N] [--generation N]
|
|
914
923
|
|
|
924
|
+
Filtered files nextCursor is an index into the filtered list, not a global file index.
|
|
915
925
|
Add --pretty for indented JSON. Compact JSON is the token-efficient default.`);
|
|
916
926
|
return parsed.values.help ? EXIT_OK : EXIT_USAGE;
|
|
917
927
|
}
|
|
@@ -945,8 +955,13 @@ Add --pretty for indented JSON. Compact JSON is the token-efficient default.`);
|
|
|
945
955
|
}
|
|
946
956
|
params.set(parameter, value);
|
|
947
957
|
}
|
|
948
|
-
|
|
949
|
-
|
|
958
|
+
const path = parsed.values.path;
|
|
959
|
+
if (typeof path === "string") params.set("path", path);
|
|
960
|
+
const exclude = parsed.values.exclude;
|
|
961
|
+
if (typeof exclude === "string") params.set("exclude", exclude);
|
|
962
|
+
const selectorError = validateInspectSelectors(resource, typeof parsed.values.file === "string" ? parsed.values.file : void 0, typeof path === "string" ? path : void 0);
|
|
963
|
+
if (selectorError) {
|
|
964
|
+
console.error(selectorError);
|
|
950
965
|
return EXIT_USAGE;
|
|
951
966
|
}
|
|
952
967
|
if (resource === "search") {
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as buildTuiGitDiffArgs, c as printHelp, n as runTerminalDiff, o as intoShowMode, r as validateEnvironment, s as parseDiffOptions, t as startServer } from "./server-
|
|
2
|
+
import { a as buildTuiGitDiffArgs, c as printHelp, n as runTerminalDiff, o as intoShowMode, r as validateEnvironment, s as parseDiffOptions, t as startServer } from "./server-CEbp7K-k.mjs";
|
|
3
3
|
import { h as getRepoRoot, r as getBranchName } from "./git-BGj31GX-.mjs";
|
|
4
4
|
import { i as loadSettings } from "./settings-DkPDB_hS.mjs";
|
|
5
5
|
import { _ as generateSessionToken, b as isWildcardBindHost, c as removeServerLockIfOwned, p as writeServerLock, r as diffScopeKey, t as acquireServerStartupLease, u as resolveActiveServerLock } from "./server-lock-CdaUNZyX.mjs";
|
|
@@ -561,11 +561,11 @@ See docs/cli.md §5 (MCP) for the full tool table.`);
|
|
|
561
561
|
console.error("diffing mcp: --repo must be an absolute path");
|
|
562
562
|
process.exit(5);
|
|
563
563
|
}
|
|
564
|
-
const { startMcpServer } = await import("./mcp-
|
|
564
|
+
const { startMcpServer } = await import("./mcp-DhynPewa.mjs");
|
|
565
565
|
await startMcpServer({ repoPath });
|
|
566
566
|
await new Promise(() => {});
|
|
567
567
|
}
|
|
568
|
-
const { runSubcommand } = await import("./cli-agent-
|
|
568
|
+
const { runSubcommand } = await import("./cli-agent-qNwkSjxK.mjs");
|
|
569
569
|
const code = await runSubcommand(args[0], args.slice(1));
|
|
570
570
|
process.exit(code);
|
|
571
571
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as buildGitDiffArgs, s as parseDiffOptions, t as startServer } from "./server-
|
|
1
|
+
import { i as buildGitDiffArgs, s as parseDiffOptions, t as startServer } from "./server-CEbp7K-k.mjs";
|
|
2
2
|
import { t as formatComments } from "./comment-format-B85NHZnG.mjs";
|
|
3
3
|
import { n as formatPlanReview } from "./plan-format-BwJh5JSE.mjs";
|
|
4
4
|
import { _ as generateSessionToken, c as removeServerLockIfOwned, i as isLockAlive, p as writeServerLock, r as diffScopeKey, s as removeServerLock, t as acquireServerStartupLease, u as resolveActiveServerLock, x as SESSION_TOKEN_HEADER, y as isLoopbackHost } from "./server-lock-CdaUNZyX.mjs";
|
|
@@ -618,54 +618,67 @@ function createMcpServer(options) {
|
|
|
618
618
|
});
|
|
619
619
|
server.registerTool("diff_summary", {
|
|
620
620
|
title: "Summarize the active diff (bounded)",
|
|
621
|
-
description: "Return bounded totals and change-kind counts without transferring the patch. Works for web, TUI, and GitHub PR sessions. Prefer this over get_diff.",
|
|
622
|
-
inputSchema: {},
|
|
621
|
+
description: "Return bounded totals, top-level directory buckets, and change-kind counts without transferring the patch. Optional exclude=[\"lockfiles\"] drops lock/generated basenames from counts only. Works for web, TUI, and GitHub PR sessions. Prefer this over get_diff.",
|
|
622
|
+
inputSchema: { exclude: z.array(z.enum(["lockfiles"])).optional() },
|
|
623
623
|
outputSchema: { result: z.unknown() },
|
|
624
624
|
annotations: READ_ONLY
|
|
625
|
-
}, async () => {
|
|
626
|
-
const
|
|
625
|
+
}, async ({ exclude } = {}) => {
|
|
626
|
+
const session = requireInspectSession();
|
|
627
|
+
const query = new URLSearchParams();
|
|
628
|
+
if (exclude?.length) query.set("exclude", exclude.join(","));
|
|
629
|
+
const result = await requestSessionJson(session, `/api/diff/summary${query.size ? `?${query}` : ""}`);
|
|
627
630
|
return textResult(JSON.stringify(result), { result });
|
|
628
631
|
});
|
|
629
632
|
server.registerTool("diff_files", {
|
|
630
633
|
title: "Page changed files (bounded)",
|
|
631
|
-
description: "Return a bounded page of changed-file metadata
|
|
634
|
+
description: "Return a bounded page of changed-file metadata. Optional path is a git pathspec-ish glob (src/lib/**, **/foo.ts). cursor/nextCursor index the filtered list; each row still has the global file index. Works for web, TUI, and GitHub PR sessions.",
|
|
632
635
|
inputSchema: {
|
|
633
636
|
cursor: z.number().int().nonnegative().optional(),
|
|
634
|
-
limit: z.number().int().positive().max(1e3).optional()
|
|
637
|
+
limit: z.number().int().positive().max(1e3).optional(),
|
|
638
|
+
path: z.string().min(1).optional()
|
|
635
639
|
},
|
|
636
640
|
outputSchema: { result: z.unknown() },
|
|
637
641
|
annotations: READ_ONLY
|
|
638
|
-
}, async ({ cursor = 0, limit = 100 }) => {
|
|
639
|
-
const
|
|
642
|
+
}, async ({ cursor = 0, limit = 100, path }) => {
|
|
643
|
+
const session = requireInspectSession();
|
|
644
|
+
const query = new URLSearchParams({
|
|
645
|
+
cursor: String(cursor),
|
|
646
|
+
limit: String(limit)
|
|
647
|
+
});
|
|
648
|
+
if (path) query.set("path", path);
|
|
649
|
+
const result = await requestSessionJson(session, `/api/diff/files?${query}`);
|
|
640
650
|
return textResult(JSON.stringify(result), { result });
|
|
641
651
|
});
|
|
642
652
|
server.registerTool("diff_hunks", {
|
|
643
653
|
title: "Page hunk metadata (bounded)",
|
|
644
|
-
description: "Return bounded hunk metadata for one file index. Pass generation from diff_summary to reject stale navigation. Works for web, TUI, and GitHub PR sessions.",
|
|
654
|
+
description: "Return bounded hunk metadata for one file. Pass path (glob resolving to exactly one file) or file (global index), not both. Pass generation from diff_summary to reject stale navigation. Works for web, TUI, and GitHub PR sessions.",
|
|
645
655
|
inputSchema: {
|
|
646
|
-
file: z.number().int().nonnegative(),
|
|
656
|
+
file: z.number().int().nonnegative().optional(),
|
|
657
|
+
path: z.string().min(1).optional(),
|
|
647
658
|
generation: z.number().int().nonnegative().optional(),
|
|
648
659
|
cursor: z.number().int().nonnegative().optional(),
|
|
649
660
|
limit: z.number().int().positive().max(1e3).optional()
|
|
650
661
|
},
|
|
651
662
|
outputSchema: { result: z.unknown() },
|
|
652
663
|
annotations: READ_ONLY
|
|
653
|
-
}, async ({ file, generation, cursor = 0, limit = 100 }) => {
|
|
664
|
+
}, async ({ file, path, generation, cursor = 0, limit = 100 }) => {
|
|
654
665
|
const session = requireInspectSession();
|
|
655
666
|
const query = new URLSearchParams({
|
|
656
|
-
file: String(file),
|
|
657
667
|
cursor: String(cursor),
|
|
658
668
|
limit: String(limit)
|
|
659
669
|
});
|
|
670
|
+
if (file !== void 0) query.set("file", String(file));
|
|
671
|
+
if (path) query.set("path", path);
|
|
660
672
|
if (generation !== void 0) query.set("generation", String(generation));
|
|
661
673
|
const result = await requestSessionJson(session, `/api/diff/hunks?${query}`);
|
|
662
674
|
return textResult(JSON.stringify(result), { result });
|
|
663
675
|
});
|
|
664
676
|
server.registerTool("diff_slice", {
|
|
665
677
|
title: "Read a bounded diff slice",
|
|
666
|
-
description: "Read exact logical rows for one file with strict line and byte budgets; use nextRow to continue. Works for web, TUI, and GitHub PR sessions. Prefer this over get_diff.",
|
|
678
|
+
description: "Read exact logical rows for one file with strict line and byte budgets; use nextRow to continue. Pass path (glob resolving to exactly one file) or file (global index), not both. Works for web, TUI, and GitHub PR sessions. Prefer this over get_diff.",
|
|
667
679
|
inputSchema: {
|
|
668
|
-
file: z.number().int().nonnegative(),
|
|
680
|
+
file: z.number().int().nonnegative().optional(),
|
|
681
|
+
path: z.string().min(1).optional(),
|
|
669
682
|
start: z.number().int().nonnegative().optional(),
|
|
670
683
|
generation: z.number().int().nonnegative().optional(),
|
|
671
684
|
maxLines: z.number().int().positive().max(1e3).optional(),
|
|
@@ -673,23 +686,25 @@ function createMcpServer(options) {
|
|
|
673
686
|
},
|
|
674
687
|
outputSchema: { result: z.unknown() },
|
|
675
688
|
annotations: READ_ONLY
|
|
676
|
-
}, async ({ file, start = 0, generation, maxLines = 120, maxBytes = 256 * 1024 }) => {
|
|
689
|
+
}, async ({ file, path, start = 0, generation, maxLines = 120, maxBytes = 256 * 1024 }) => {
|
|
677
690
|
const session = requireInspectSession();
|
|
678
691
|
const query = new URLSearchParams({
|
|
679
|
-
file: String(file),
|
|
680
692
|
start: String(start),
|
|
681
693
|
maxLines: String(maxLines),
|
|
682
694
|
maxBytes: String(maxBytes)
|
|
683
695
|
});
|
|
696
|
+
if (file !== void 0) query.set("file", String(file));
|
|
697
|
+
if (path) query.set("path", path);
|
|
684
698
|
if (generation !== void 0) query.set("generation", String(generation));
|
|
685
699
|
const result = await requestSessionJson(session, `/api/diff/slice?${query}`);
|
|
686
700
|
return textResult(JSON.stringify(result), { result });
|
|
687
701
|
});
|
|
688
702
|
server.registerTool("diff_search", {
|
|
689
703
|
title: "Search the active diff (bounded)",
|
|
690
|
-
description: "Search changed paths and content with bounded hits/bytes and generation-safe continuation coordinates. Works for web, TUI, and GitHub PR sessions.",
|
|
704
|
+
description: "Search changed paths and content with bounded hits/bytes and generation-safe continuation coordinates. Optional path glob limits hits to matching files (in addition to file+row continuation). Works for web, TUI, and GitHub PR sessions.",
|
|
691
705
|
inputSchema: {
|
|
692
706
|
query: z.string().min(1),
|
|
707
|
+
path: z.string().min(1).optional(),
|
|
693
708
|
generation: z.number().int().nonnegative().optional(),
|
|
694
709
|
file: z.number().int().nonnegative().optional(),
|
|
695
710
|
row: z.number().int().nonnegative().optional(),
|
|
@@ -698,7 +713,7 @@ function createMcpServer(options) {
|
|
|
698
713
|
},
|
|
699
714
|
outputSchema: { result: z.unknown() },
|
|
700
715
|
annotations: READ_ONLY
|
|
701
|
-
}, async ({ query, generation, file = 0, row = 0, limit = 100, maxBytes = 256 * 1024 }) => {
|
|
716
|
+
}, async ({ query, path, generation, file = 0, row = 0, limit = 100, maxBytes = 256 * 1024 }) => {
|
|
702
717
|
const session = requireInspectSession();
|
|
703
718
|
const params = new URLSearchParams({
|
|
704
719
|
q: query,
|
|
@@ -707,6 +722,7 @@ function createMcpServer(options) {
|
|
|
707
722
|
limit: String(limit),
|
|
708
723
|
maxBytes: String(maxBytes)
|
|
709
724
|
});
|
|
725
|
+
if (path) params.set("path", path);
|
|
710
726
|
if (generation !== void 0) params.set("generation", String(generation));
|
|
711
727
|
const result = await requestSessionJson(session, `/api/diff/search?${params}`);
|
|
712
728
|
return textResult(JSON.stringify(result), { result });
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -2201,6 +2201,113 @@ var FilePrSessionStore = class {
|
|
|
2201
2201
|
} catch {}
|
|
2202
2202
|
}
|
|
2203
2203
|
};
|
|
2204
|
+
const PATH_MATCH_CAP = 20;
|
|
2205
|
+
function compilePathspecGlob(pattern) {
|
|
2206
|
+
const source = pattern.trim();
|
|
2207
|
+
if (!source) return {
|
|
2208
|
+
error: "invalid path glob: empty pattern",
|
|
2209
|
+
status: 400,
|
|
2210
|
+
path: pattern
|
|
2211
|
+
};
|
|
2212
|
+
try {
|
|
2213
|
+
return {
|
|
2214
|
+
source,
|
|
2215
|
+
test: compileGlobTester(source)
|
|
2216
|
+
};
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
return {
|
|
2219
|
+
error: error instanceof Error ? error.message : "invalid path glob",
|
|
2220
|
+
status: 400,
|
|
2221
|
+
path: pattern
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
function fileMatchesPath(matcher, oldPath, newPath) {
|
|
2226
|
+
if (newPath && matcher.test(newPath)) return true;
|
|
2227
|
+
if (oldPath && matcher.test(oldPath)) return true;
|
|
2228
|
+
return false;
|
|
2229
|
+
}
|
|
2230
|
+
function parseExcludeList(raw) {
|
|
2231
|
+
if (raw == null) return [];
|
|
2232
|
+
const values = (Array.isArray(raw) ? raw : raw.split(",")).map((value) => value.trim()).filter(Boolean);
|
|
2233
|
+
for (const value of values) if (value !== "lockfiles") return {
|
|
2234
|
+
error: `unknown exclude: ${value}`,
|
|
2235
|
+
status: 400
|
|
2236
|
+
};
|
|
2237
|
+
return [...new Set(values)];
|
|
2238
|
+
}
|
|
2239
|
+
function isLockfileNoise(path) {
|
|
2240
|
+
const base = path.split("/").pop() ?? path;
|
|
2241
|
+
if (base === "package-lock.json" || base === "pnpm-lock.yaml") return true;
|
|
2242
|
+
if (base.endsWith(".lock")) return true;
|
|
2243
|
+
if (base.endsWith(".min.js")) return true;
|
|
2244
|
+
return false;
|
|
2245
|
+
}
|
|
2246
|
+
function displayPath(oldPath, newPath) {
|
|
2247
|
+
return newPath ?? oldPath ?? "";
|
|
2248
|
+
}
|
|
2249
|
+
function capPathMatches(matches) {
|
|
2250
|
+
return matches.slice(0, PATH_MATCH_CAP);
|
|
2251
|
+
}
|
|
2252
|
+
function compileGlobTester(pattern) {
|
|
2253
|
+
const parts = (pattern.includes("/") ? pattern : `**/${pattern}`).split("/");
|
|
2254
|
+
for (const part of parts) {
|
|
2255
|
+
if (part !== "**" && part.includes("**")) throw new Error(`invalid path glob: ${pattern}`);
|
|
2256
|
+
validateGlobSegment(part, pattern);
|
|
2257
|
+
}
|
|
2258
|
+
return (path) => matchParts(parts, path.split("/"));
|
|
2259
|
+
}
|
|
2260
|
+
function matchParts(pat, path) {
|
|
2261
|
+
if (pat.length === 0) return path.length === 0;
|
|
2262
|
+
if (pat[0] === "**") return matchParts(pat.slice(1), path) || path.length > 0 && matchParts(pat, path.slice(1));
|
|
2263
|
+
if (path.length === 0) return false;
|
|
2264
|
+
return globSegment(pat[0], path[0]) && matchParts(pat.slice(1), path.slice(1));
|
|
2265
|
+
}
|
|
2266
|
+
function validateGlobSegment(segment, pattern) {
|
|
2267
|
+
for (let i = 0; i < segment.length; i++) {
|
|
2268
|
+
if (segment[i] === "[") {
|
|
2269
|
+
const close = segment.indexOf("]", i + 1);
|
|
2270
|
+
if (close < 0 || close === i + 1) throw new Error(`invalid path glob: ${pattern}`);
|
|
2271
|
+
i = close;
|
|
2272
|
+
continue;
|
|
2273
|
+
}
|
|
2274
|
+
if (segment[i] === "\\") {
|
|
2275
|
+
if (segment[i + 1] == null) throw new Error(`invalid path glob: ${pattern}`);
|
|
2276
|
+
i++;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
function globSegment(pattern, value) {
|
|
2281
|
+
return globChars([...pattern], [...value]);
|
|
2282
|
+
}
|
|
2283
|
+
function globChars(pattern, value) {
|
|
2284
|
+
if (pattern.length === 0) return value.length === 0;
|
|
2285
|
+
const [head, ...rest] = pattern;
|
|
2286
|
+
if (head === "*") return globChars(rest, value) || value.length > 0 && globChars(pattern, value.slice(1));
|
|
2287
|
+
if (head === "?") return value.length > 0 && globChars(rest, value.slice(1));
|
|
2288
|
+
if (head === "[") {
|
|
2289
|
+
const close = rest.indexOf("]");
|
|
2290
|
+
if (close < 0 || value.length === 0) return false;
|
|
2291
|
+
const classBody = rest.slice(0, close);
|
|
2292
|
+
const negated = classBody[0] === "!" || classBody[0] === "^";
|
|
2293
|
+
if (classMatches(negated ? classBody.slice(1) : classBody, value[0]) === negated) return false;
|
|
2294
|
+
return globChars(rest.slice(close + 1), value.slice(1));
|
|
2295
|
+
}
|
|
2296
|
+
if (head === "\\") return rest.length > 0 && value.length > 0 && rest[0] === value[0] && globChars(rest.slice(1), value.slice(1));
|
|
2297
|
+
return value.length > 0 && head === value[0] && globChars(rest, value.slice(1));
|
|
2298
|
+
}
|
|
2299
|
+
function classMatches(body, value) {
|
|
2300
|
+
for (let i = 0; i < body.length;) {
|
|
2301
|
+
if (i + 2 < body.length && body[i + 1] === "-") {
|
|
2302
|
+
if (body[i] <= value && value <= body[i + 2]) return true;
|
|
2303
|
+
i += 3;
|
|
2304
|
+
continue;
|
|
2305
|
+
}
|
|
2306
|
+
if (body[i] === value) return true;
|
|
2307
|
+
i++;
|
|
2308
|
+
}
|
|
2309
|
+
return false;
|
|
2310
|
+
}
|
|
2204
2311
|
//#endregion
|
|
2205
2312
|
//#region src/lib/agent-diff-index.ts
|
|
2206
2313
|
/**
|
|
@@ -2429,33 +2536,140 @@ function buildAgentDiffIndex(patch, generation = nextGeneration++) {
|
|
|
2429
2536
|
patchBytes
|
|
2430
2537
|
};
|
|
2431
2538
|
}
|
|
2432
|
-
|
|
2539
|
+
const DIRECTORY_CAP = 20;
|
|
2540
|
+
function indexSummary(index, excludeRaw) {
|
|
2541
|
+
const exclude = parseExcludeList(excludeRaw);
|
|
2542
|
+
if ("error" in exclude) return exclude;
|
|
2543
|
+
const counted = exclude.includes("lockfiles") ? index.files.filter((file) => !isLockfileNoise(displayPath(file.oldPath, file.newPath))) : index.files;
|
|
2433
2544
|
const changes = {};
|
|
2434
|
-
|
|
2435
|
-
|
|
2545
|
+
let hunks = 0;
|
|
2546
|
+
let rows = 0;
|
|
2547
|
+
let additions = 0;
|
|
2548
|
+
let deletions = 0;
|
|
2549
|
+
for (const file of counted) {
|
|
2550
|
+
changes[file.kind] = (changes[file.kind] ?? 0) + 1;
|
|
2551
|
+
hunks += file.hunks.length;
|
|
2552
|
+
rows += file.rowCount;
|
|
2553
|
+
additions += file.additions;
|
|
2554
|
+
deletions += file.deletions;
|
|
2555
|
+
}
|
|
2556
|
+
const summary = {
|
|
2436
2557
|
generation: index.generation,
|
|
2437
2558
|
complete: index.complete,
|
|
2438
|
-
files:
|
|
2439
|
-
hunks
|
|
2440
|
-
rows
|
|
2441
|
-
additions
|
|
2442
|
-
deletions
|
|
2559
|
+
files: counted.length,
|
|
2560
|
+
hunks,
|
|
2561
|
+
rows,
|
|
2562
|
+
additions,
|
|
2563
|
+
deletions,
|
|
2443
2564
|
patchBytes: index.patchBytes,
|
|
2444
2565
|
changes,
|
|
2566
|
+
directories: summarizeDirectories(counted),
|
|
2445
2567
|
next: [
|
|
2446
2568
|
"diff_files",
|
|
2447
2569
|
"diff_search",
|
|
2448
2570
|
"diff_slice"
|
|
2449
2571
|
]
|
|
2450
2572
|
};
|
|
2573
|
+
if (exclude.length > 0) summary.exclude = exclude;
|
|
2574
|
+
return summary;
|
|
2575
|
+
}
|
|
2576
|
+
function summarizeDirectories(files) {
|
|
2577
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
2578
|
+
for (const file of files) {
|
|
2579
|
+
const path = displayPath(file.oldPath, file.newPath);
|
|
2580
|
+
const slash = path.indexOf("/");
|
|
2581
|
+
const dir = slash < 0 ? "." : path.slice(0, slash);
|
|
2582
|
+
const bucket = buckets.get(dir) ?? {
|
|
2583
|
+
path: dir,
|
|
2584
|
+
files: 0,
|
|
2585
|
+
hunks: 0,
|
|
2586
|
+
additions: 0,
|
|
2587
|
+
deletions: 0
|
|
2588
|
+
};
|
|
2589
|
+
bucket.files += 1;
|
|
2590
|
+
bucket.hunks += file.hunks.length;
|
|
2591
|
+
bucket.additions += file.additions;
|
|
2592
|
+
bucket.deletions += file.deletions;
|
|
2593
|
+
buckets.set(dir, bucket);
|
|
2594
|
+
}
|
|
2595
|
+
const ranked = [...buckets.values()].sort((a, b) => {
|
|
2596
|
+
if (b.files !== a.files) return b.files - a.files;
|
|
2597
|
+
return b.additions + b.deletions - (a.additions + a.deletions);
|
|
2598
|
+
});
|
|
2599
|
+
if (ranked.length <= DIRECTORY_CAP) return ranked;
|
|
2600
|
+
const head = ranked.slice(0, DIRECTORY_CAP);
|
|
2601
|
+
const rest = ranked.slice(DIRECTORY_CAP);
|
|
2602
|
+
const other = {
|
|
2603
|
+
path: "+other",
|
|
2604
|
+
files: 0,
|
|
2605
|
+
hunks: 0,
|
|
2606
|
+
additions: 0,
|
|
2607
|
+
deletions: 0
|
|
2608
|
+
};
|
|
2609
|
+
for (const bucket of rest) {
|
|
2610
|
+
other.files += bucket.files;
|
|
2611
|
+
other.hunks += bucket.hunks;
|
|
2612
|
+
other.additions += bucket.additions;
|
|
2613
|
+
other.deletions += bucket.deletions;
|
|
2614
|
+
}
|
|
2615
|
+
head.push(other);
|
|
2616
|
+
return head;
|
|
2617
|
+
}
|
|
2618
|
+
function scopedFiles(index, path) {
|
|
2619
|
+
if (path == null || path === "") return { entries: index.files.map((file, i) => ({
|
|
2620
|
+
index: i,
|
|
2621
|
+
file
|
|
2622
|
+
})) };
|
|
2623
|
+
const matcher = compilePathspecGlob(path);
|
|
2624
|
+
if ("error" in matcher) return matcher;
|
|
2625
|
+
return {
|
|
2626
|
+
matcher,
|
|
2627
|
+
entries: index.files.flatMap((file, i) => fileMatchesPath(matcher, file.oldPath, file.newPath) ? [{
|
|
2628
|
+
index: i,
|
|
2629
|
+
file
|
|
2630
|
+
}] : [])
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
2633
|
+
function resolveInspectFile(index, file, path) {
|
|
2634
|
+
const hasFile = file != null;
|
|
2635
|
+
const hasPath = path != null && path !== "";
|
|
2636
|
+
if (hasFile && hasPath) return {
|
|
2637
|
+
error: "path and file are mutually exclusive",
|
|
2638
|
+
status: 400,
|
|
2639
|
+
path
|
|
2640
|
+
};
|
|
2641
|
+
if (!hasFile && !hasPath) return {
|
|
2642
|
+
error: "file or path is required",
|
|
2643
|
+
status: 400
|
|
2644
|
+
};
|
|
2645
|
+
if (hasFile) return { fileIndex: file };
|
|
2646
|
+
const scoped = scopedFiles(index, path);
|
|
2647
|
+
if ("error" in scoped) return scoped;
|
|
2648
|
+
if (scoped.entries.length === 0) return {
|
|
2649
|
+
error: "path matched no files",
|
|
2650
|
+
status: 404,
|
|
2651
|
+
path
|
|
2652
|
+
};
|
|
2653
|
+
if (scoped.entries.length > 1) return {
|
|
2654
|
+
error: "path matched multiple files; narrow the glob or pass file",
|
|
2655
|
+
status: 409,
|
|
2656
|
+
path,
|
|
2657
|
+
matches: capPathMatches(scoped.entries.map(({ index, file }) => ({
|
|
2658
|
+
index,
|
|
2659
|
+
path: displayPath(file.oldPath, file.newPath)
|
|
2660
|
+
})))
|
|
2661
|
+
};
|
|
2662
|
+
return { fileIndex: scoped.entries[0].index };
|
|
2451
2663
|
}
|
|
2452
|
-
function indexFiles(index, cursor = 0, limit = 100) {
|
|
2664
|
+
function indexFiles(index, cursor = 0, limit = 100, path) {
|
|
2665
|
+
const scoped = scopedFiles(index, path);
|
|
2666
|
+
if ("error" in scoped) return scoped;
|
|
2453
2667
|
const safeLimit = clamp(limit, 1, MAX_PAGE_LINES);
|
|
2454
2668
|
const start = Math.max(0, cursor);
|
|
2455
|
-
const end = Math.min(
|
|
2456
|
-
const files =
|
|
2457
|
-
index
|
|
2458
|
-
path: file.
|
|
2669
|
+
const end = Math.min(scoped.entries.length, start + safeLimit);
|
|
2670
|
+
const files = scoped.entries.slice(start, end).map(({ index, file }) => ({
|
|
2671
|
+
index,
|
|
2672
|
+
path: displayPath(file.oldPath, file.newPath),
|
|
2459
2673
|
oldPath: file.oldPath,
|
|
2460
2674
|
newPath: file.newPath,
|
|
2461
2675
|
kind: file.kind,
|
|
@@ -2465,13 +2679,16 @@ function indexFiles(index, cursor = 0, limit = 100) {
|
|
|
2465
2679
|
additions: file.additions,
|
|
2466
2680
|
deletions: file.deletions
|
|
2467
2681
|
}));
|
|
2468
|
-
|
|
2682
|
+
const page = {
|
|
2469
2683
|
generation: index.generation,
|
|
2470
2684
|
returned: files.length,
|
|
2471
2685
|
total: index.files.length,
|
|
2472
|
-
|
|
2686
|
+
matched: scoped.entries.length,
|
|
2687
|
+
nextCursor: end < scoped.entries.length ? end : null,
|
|
2473
2688
|
files
|
|
2474
2689
|
};
|
|
2690
|
+
if (path) page.path = path;
|
|
2691
|
+
return page;
|
|
2475
2692
|
}
|
|
2476
2693
|
function indexHunks(index, fileIndex, cursor = 0, limit = 100, generation) {
|
|
2477
2694
|
if (generation !== void 0 && generation !== index.generation) return {
|
|
@@ -2542,11 +2759,17 @@ function indexSlice(index, fileIndex, startRow = 0, maxLines = DEFAULT_SLICE_LIN
|
|
|
2542
2759
|
rows
|
|
2543
2760
|
};
|
|
2544
2761
|
}
|
|
2545
|
-
function indexSearch(index, query, fileStart = 0, rowStart = 0, limit = 100, maxBytes = DEFAULT_MAX_BYTES, generation) {
|
|
2762
|
+
function indexSearch(index, query, fileStart = 0, rowStart = 0, limit = 100, maxBytes = DEFAULT_MAX_BYTES, generation, path) {
|
|
2546
2763
|
if (generation !== void 0 && generation !== index.generation) return {
|
|
2547
2764
|
error: `stale generation ${generation}; current generation is ${index.generation}`,
|
|
2548
2765
|
status: 409
|
|
2549
2766
|
};
|
|
2767
|
+
let matcher;
|
|
2768
|
+
if (path) {
|
|
2769
|
+
const compiled = compilePathspecGlob(path);
|
|
2770
|
+
if ("error" in compiled) return compiled;
|
|
2771
|
+
matcher = compiled;
|
|
2772
|
+
}
|
|
2550
2773
|
const q = query.toLowerCase();
|
|
2551
2774
|
if (!q) return {
|
|
2552
2775
|
generation: index.generation,
|
|
@@ -2565,6 +2788,7 @@ function indexSearch(index, query, fileStart = 0, rowStart = 0, limit = 100, max
|
|
|
2565
2788
|
let nextRow = null;
|
|
2566
2789
|
outer: for (let fi = Math.max(0, fileStart); fi < index.files.length; fi++) {
|
|
2567
2790
|
const file = index.files[fi];
|
|
2791
|
+
if (matcher && !fileMatchesPath(matcher, file.oldPath, file.newPath)) continue;
|
|
2568
2792
|
const path = file.newPath ?? file.oldPath ?? "";
|
|
2569
2793
|
const pathMatch = path.toLowerCase().includes(q);
|
|
2570
2794
|
const rowBegin = fi === fileStart ? Math.max(0, rowStart) : 0;
|
|
@@ -3169,9 +3393,21 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3169
3393
|
const n = Number(value);
|
|
3170
3394
|
return Number.isFinite(n) && n >= 0 ? Math.trunc(n) : fallback;
|
|
3171
3395
|
}
|
|
3396
|
+
function optionalUInt(value) {
|
|
3397
|
+
if (value == null || value === "") return void 0;
|
|
3398
|
+
const n = Number(value);
|
|
3399
|
+
return Number.isFinite(n) && n >= 0 ? Math.trunc(n) : void 0;
|
|
3400
|
+
}
|
|
3401
|
+
function inspectError(c, result) {
|
|
3402
|
+
const body = { error: result.error };
|
|
3403
|
+
if (result.path != null) body.path = result.path;
|
|
3404
|
+
if (result.matches != null) body.matches = result.matches;
|
|
3405
|
+
return c.json(body, result.status);
|
|
3406
|
+
}
|
|
3172
3407
|
app.get("/api/diff/summary", async (c) => {
|
|
3173
3408
|
const patch = await resolveAgentPatch();
|
|
3174
|
-
const summary = indexSummary(agentDiffCache.getOrBuild(patch));
|
|
3409
|
+
const summary = indexSummary(agentDiffCache.getOrBuild(patch), c.req.query("exclude"));
|
|
3410
|
+
if ("status" in summary) return inspectError(c, summary);
|
|
3175
3411
|
if (!prMode) return c.json(summary);
|
|
3176
3412
|
const session = await prStore.get();
|
|
3177
3413
|
if (!session) return c.json(summary);
|
|
@@ -3189,32 +3425,33 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3189
3425
|
});
|
|
3190
3426
|
app.get("/api/diff/files", async (c) => {
|
|
3191
3427
|
const patch = await resolveAgentPatch();
|
|
3192
|
-
const
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
return c.json(indexFiles(index, cursor, limit));
|
|
3428
|
+
const result = indexFiles(agentDiffCache.getOrBuild(patch), parseUInt(c.req.query("cursor"), 0), parseUInt(c.req.query("limit"), 100), c.req.query("path"));
|
|
3429
|
+
if ("status" in result) return inspectError(c, result);
|
|
3430
|
+
return c.json(result);
|
|
3196
3431
|
});
|
|
3197
3432
|
app.get("/api/diff/hunks", async (c) => {
|
|
3198
3433
|
const patch = await resolveAgentPatch();
|
|
3199
3434
|
const index = agentDiffCache.getOrBuild(patch);
|
|
3200
|
-
const
|
|
3435
|
+
const resolved = resolveInspectFile(index, optionalUInt(c.req.query("file")), c.req.query("path"));
|
|
3436
|
+
if ("status" in resolved) return inspectError(c, resolved);
|
|
3201
3437
|
const cursor = parseUInt(c.req.query("cursor"), 0);
|
|
3202
3438
|
const limit = parseUInt(c.req.query("limit"), 100);
|
|
3203
3439
|
const generationRaw = c.req.query("generation");
|
|
3204
|
-
const result = indexHunks(index,
|
|
3205
|
-
if ("status" in result) return c
|
|
3440
|
+
const result = indexHunks(index, resolved.fileIndex, cursor, limit, generationRaw != null && generationRaw !== "" ? parseUInt(generationRaw, 0) : void 0);
|
|
3441
|
+
if ("status" in result) return inspectError(c, result);
|
|
3206
3442
|
return c.json(result);
|
|
3207
3443
|
});
|
|
3208
3444
|
app.get("/api/diff/slice", async (c) => {
|
|
3209
3445
|
const patch = await resolveAgentPatch();
|
|
3210
3446
|
const index = agentDiffCache.getOrBuild(patch);
|
|
3211
|
-
const
|
|
3447
|
+
const resolved = resolveInspectFile(index, optionalUInt(c.req.query("file")), c.req.query("path"));
|
|
3448
|
+
if ("status" in resolved) return inspectError(c, resolved);
|
|
3212
3449
|
const start = parseUInt(c.req.query("start"), 0);
|
|
3213
3450
|
const maxLines = parseUInt(c.req.query("maxLines"), 120);
|
|
3214
3451
|
const maxBytes = parseUInt(c.req.query("maxBytes"), 256 * 1024);
|
|
3215
3452
|
const generationRaw = c.req.query("generation");
|
|
3216
|
-
const result = indexSlice(index,
|
|
3217
|
-
if ("status" in result) return c
|
|
3453
|
+
const result = indexSlice(index, resolved.fileIndex, start, maxLines, maxBytes, generationRaw != null && generationRaw !== "" ? parseUInt(generationRaw, 0) : void 0);
|
|
3454
|
+
if ("status" in result) return inspectError(c, result);
|
|
3218
3455
|
return c.json(result);
|
|
3219
3456
|
});
|
|
3220
3457
|
app.get("/api/diff/search", async (c) => {
|
|
@@ -3226,8 +3463,8 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3226
3463
|
const limit = parseUInt(c.req.query("limit"), 100);
|
|
3227
3464
|
const maxBytes = parseUInt(c.req.query("maxBytes"), 256 * 1024);
|
|
3228
3465
|
const generationRaw = c.req.query("generation");
|
|
3229
|
-
const result = indexSearch(index, q, file, row, limit, maxBytes, generationRaw != null && generationRaw !== "" ? parseUInt(generationRaw, 0) : void 0);
|
|
3230
|
-
if ("status" in result) return c
|
|
3466
|
+
const result = indexSearch(index, q, file, row, limit, maxBytes, generationRaw != null && generationRaw !== "" ? parseUInt(generationRaw, 0) : void 0, c.req.query("path"));
|
|
3467
|
+
if ("status" in result) return inspectError(c, result);
|
|
3231
3468
|
return c.json(result);
|
|
3232
3469
|
});
|
|
3233
3470
|
const resolveFileVersion = async (path, version) => {
|
package/extensions/pi/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "diffing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "local-first CLI for reviewing, navigating, and discussing git diffs with AI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"verify:release-bundle": "node scripts/verify-tui-bundle.mjs",
|
|
57
57
|
"prepublishOnly": "pnpm run verify:release-bundle",
|
|
58
58
|
"version-packages": "changeset version",
|
|
59
|
+
"release": "node scripts/release.mjs",
|
|
59
60
|
"docs:dev": "pnpm --dir site dev",
|
|
60
61
|
"docs:build": "pnpm --dir site build",
|
|
61
62
|
"docs:preview": "pnpm --dir site preview"
|