diffninja 0.3.2 → 0.4.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 +65 -11
- package/dist/executables.d.ts +18 -0
- package/dist/executables.js +32 -0
- package/dist/git.d.ts +26 -1
- package/dist/git.js +56 -4
- package/dist/languages/child-env.d.ts +11 -0
- package/dist/languages/child-env.js +60 -0
- package/dist/languages/grammar-lock.d.ts +569 -0
- package/dist/languages/grammar-lock.js +574 -0
- package/dist/languages/grammars.d.ts +69 -9
- package/dist/languages/grammars.js +186 -119
- package/dist/review/call-flow-html.js +1 -0
- package/dist/review/change-facts.d.ts +21 -1
- package/dist/review/change-facts.js +271 -49
- package/dist/review/cli.js +13 -1
- package/dist/review/connected-analysis.d.ts +4 -1
- package/dist/review/connected-analysis.js +4 -2
- package/dist/review/connected-html.d.ts +15 -4
- package/dist/review/connected-html.js +338 -43
- package/dist/review/connected.js +47 -14
- package/dist/review/escape-html.d.ts +5 -1
- package/dist/review/escape-html.js +7 -2
- package/dist/review/explanation.d.ts +4 -0
- package/dist/review/explanation.js +6 -1
- package/dist/review/github.d.ts +56 -0
- package/dist/review/github.js +234 -33
- package/dist/review/grammars-command.d.ts +12 -0
- package/dist/review/grammars-command.js +60 -0
- package/dist/review/hidden-characters.d.ts +31 -0
- package/dist/review/hidden-characters.js +113 -0
- package/dist/review/history.js +7 -3
- package/dist/review/html.js +11 -3
- package/dist/review/input.js +5 -2
- package/dist/review/intent.d.ts +7 -0
- package/dist/review/intent.js +25 -3
- package/dist/review/markdown.js +11 -0
- package/dist/review/mcp-cli.js +4 -1
- package/dist/review/mcp.d.ts +7 -1
- package/dist/review/mcp.js +145 -56
- package/dist/review/pipeline.d.ts +2 -1
- package/dist/review/pipeline.js +4 -3
- package/dist/review/pr-input.d.ts +7 -0
- package/dist/review/pr-input.js +25 -3
- package/dist/review/questions.js +11 -3
- package/dist/review/reference-check.d.ts +5 -1
- package/dist/review/reference-check.js +40 -16
- package/dist/review/report-pages.d.ts +24 -9
- package/dist/review/report-pages.js +111 -28
- package/dist/review/result-budget.d.ts +28 -0
- package/dist/review/result-budget.js +136 -0
- package/dist/review/service.js +26 -2
- package/dist/review/setup.d.ts +1 -1
- package/dist/review/setup.js +9 -4
- package/dist/review/types.d.ts +19 -9
- package/dist/review/types.js +2 -2
- package/dist/review/update-check.d.ts +35 -0
- package/dist/review/update-check.js +76 -0
- package/dist/run.js +11 -5
- package/npm-shrinkwrap.json +3483 -0
- package/package.json +3 -2
package/dist/review/service.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { grammarsInstallCommand, takeMissingGrammars } from "../languages/grammars.js";
|
|
2
|
+
import { hiddenControlsByFile } from "./hidden-characters.js";
|
|
1
3
|
import { resolve } from "node:path";
|
|
2
4
|
import { runDiff } from "../run.js";
|
|
3
|
-
import { readSnapshotFile } from "../git.js";
|
|
5
|
+
import { MAX_INDEXED_FILES, MAX_INDEXED_FILE_BYTES, readSnapshotFile, takeSkippedSources } from "../git.js";
|
|
4
6
|
import { parseDiff, gitDiff } from "./input.js";
|
|
5
7
|
import { reviewUnits } from "./pipeline.js";
|
|
6
8
|
import { buildCallFlows, reportOrderedTextHunkFiles, CALL_FLOW_MAX_DEPTH } from "./call-flow.js";
|
|
@@ -95,6 +97,12 @@ export async function reviewDiff(input, options = {}) {
|
|
|
95
97
|
if (snapshots && options.pr?.headRef !== undefined && options.pr.headRef !== snapshots.to) {
|
|
96
98
|
throw new Error("The PR head does not match the source snapshot; refusing mismatched intent evidence.");
|
|
97
99
|
}
|
|
100
|
+
const hiddenControls = hiddenControlsByFile(units);
|
|
101
|
+
if (hiddenControls.size > 0) {
|
|
102
|
+
const files = [...hiddenControls.keys()];
|
|
103
|
+
const points = [...new Set([...hiddenControls.values()].flat())].sort();
|
|
104
|
+
warnings.push(`Lines this change adds or removes in ${files.slice(0, 5).join(", ")}${files.length > 5 ? ` and ${files.length - 5} more files` : ""} contain hidden or bidirectional control characters (${points.join(", ")}). They can make code read differently from how it compiles (Trojan Source, CVE-2021-42574); the pages show each as a ⟦U+XXXX⟧ marker. Read those lines with the markers before trusting them.`);
|
|
105
|
+
}
|
|
98
106
|
let evidence = buildReviewEvidence(units);
|
|
99
107
|
const callFlow = [];
|
|
100
108
|
let trees = [];
|
|
@@ -127,6 +135,8 @@ export async function reviewDiff(input, options = {}) {
|
|
|
127
135
|
// need a git range. Every other value is decided by what analysis returned.
|
|
128
136
|
let callFlowAvailability = snapshots ? "no-changes" : "needs-git-range";
|
|
129
137
|
if (snapshots && units.length) {
|
|
138
|
+
takeMissingGrammars();
|
|
139
|
+
takeSkippedSources();
|
|
130
140
|
try {
|
|
131
141
|
const flow = runDiff({
|
|
132
142
|
cwd: cwd, from: snapshots.from, to: snapshots.to, maxDepth: CALL_FLOW_MAX_DEPTH, color: false, locs: true,
|
|
@@ -160,6 +170,20 @@ export async function reviewDiff(input, options = {}) {
|
|
|
160
170
|
evidence = buildReviewEvidence(units);
|
|
161
171
|
warnings.push("Call-flow analysis failed. Review is based on the diff only. Inspect repository context manually.");
|
|
162
172
|
}
|
|
173
|
+
const left = takeSkippedSources();
|
|
174
|
+
if (left.oversized > 0 || left.beyondLimit > 0) {
|
|
175
|
+
const parts = [
|
|
176
|
+
left.oversized > 0 ? `${left.oversized} source ${left.oversized === 1 ? "file" : "files"} over ${MAX_INDEXED_FILE_BYTES / 1024 / 1024} MiB (generated or minified code)` : "",
|
|
177
|
+
left.beyondLimit > 0 ? `${left.beyondLimit} ${left.beyondLimit === 1 ? "file" : "files"} beyond the ${MAX_INDEXED_FILES.toLocaleString("en-US")} read per revision` : "",
|
|
178
|
+
].filter((part) => part !== "");
|
|
179
|
+
warnings.push(`Call flows did not read ${parts.join(" and ")}. Flows through them are absent, which is not evidence of safety.`);
|
|
180
|
+
if (callFlowAvailability === "no-changes")
|
|
181
|
+
callFlowAvailability = "partial";
|
|
182
|
+
}
|
|
183
|
+
const missing = takeMissingGrammars();
|
|
184
|
+
if (missing.length > 0) {
|
|
185
|
+
warnings.push(`Call flows skip the files these grammars would read: ${missing.join(", ")}. diffninja does not download code while it reviews; run \`${grammarsInstallCommand(missing)}\` once and review again to include them. Flows through those files are absent, which is not evidence of safety.`);
|
|
186
|
+
}
|
|
163
187
|
}
|
|
164
188
|
if (options.referenceProject !== undefined) {
|
|
165
189
|
if (!snapshots)
|
|
@@ -186,7 +210,7 @@ export async function reviewDiff(input, options = {}) {
|
|
|
186
210
|
// Structured flows are grouped per changed file in report order, after
|
|
187
211
|
// ranking, so the HTML can order files by the severity of their worst hunk.
|
|
188
212
|
const callFlows = buildCallFlows(reportOrderedTextHunkFiles(result.items, units), trees, nodeDetail);
|
|
189
|
-
if (callFlows.length > 0)
|
|
213
|
+
if (callFlows.length > 0 && callFlowAvailability === "no-changes")
|
|
190
214
|
callFlowAvailability = "available";
|
|
191
215
|
const report = { title: options.pr?.title || "Focused PR review", source, createdAt: new Date().toISOString(),
|
|
192
216
|
pr: options.pr,
|
package/dist/review/setup.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* the `gh` session.
|
|
12
12
|
*/
|
|
13
13
|
import { type Stats } from "node:fs";
|
|
14
|
-
export declare const setupHelp = "diffninja setup. Register the diffninja MCP server on every detected agent CLI.\n\n npx -y diffninja@latest setup [--cli claude,codex,omp,pi] [--dry-run]\n diffninja setup --uninstall [--cli codex]\n\nDetects Claude Code, Codex, OMP, and pi from their config files or binaries\nand registers the diffninja MCP server in each user config, pointing at the\nglobally installed package. Installs the package globally first\n(`npm install -g diffninja@<this version>`) so the registration keeps\nworking, and updates a global install older than this setup, so running\n`npx -y diffninja@latest setup` again is how you update. When that install\nfails it registers an npx-based entry pinned to this version and says so.\n\nOptions:\n --cli NAMES Only these CLIs, comma-separated: claude,codex,omp,pi.\n --uninstall Remove the diffninja server from every detected CLI.\n --dry-run Show what would change, without installing or writing.\n --no-install Skip installing or updating the global package; without one,\n register npx-based entries.\n --help Show this help.\n\nThe server needs no API key: static reviews run locally, and pull request\nreviews reuse your authenticated gh session.\n";
|
|
14
|
+
export declare const setupHelp = "diffninja setup. Register the diffninja MCP server on every detected agent CLI.\n\n npx -y diffninja@latest setup [--cli claude,codex,omp,pi] [--dry-run]\n diffninja setup --uninstall [--cli codex]\n\nDetects Claude Code, Codex, OMP, and pi from their config files or binaries\nand registers the diffninja MCP server in each user config, pointing at the\nglobally installed package. Installs the package globally first\n(`npm install -g diffninja@<this version>`) so the registration keeps\nworking, and updates a global install older than this setup, so running\n`npx -y diffninja@latest setup` again is how you update. When that install\nfails it registers an npx-based entry pinned to this version and says so.\n\nEach JSON config it changes is rewritten in full, in standard formatting\n(indentation, string escapes and integers above 2^53 can change), and no\nbackup is kept. Copy ~/.claude.json before the first run.\n\nOptions:\n --cli NAMES Only these CLIs, comma-separated: claude,codex,omp,pi.\n --uninstall Remove the diffninja server from every detected CLI.\n --dry-run Show what would change, without installing or writing.\n --no-install Skip installing or updating the global package; without one,\n register npx-based entries.\n --help Show this help.\n\nThe server needs no API key: static reviews run locally, and pull request\nreviews reuse your authenticated gh session.\n";
|
|
15
15
|
export declare const CLI_NAMES: readonly ["claude", "codex", "omp", "pi"];
|
|
16
16
|
export type CliName = (typeof CLI_NAMES)[number];
|
|
17
17
|
export interface McpEntry {
|
package/dist/review/setup.js
CHANGED
|
@@ -18,6 +18,7 @@ import { lstat, mkdir, open, readFile, readlink, realpath, rename, rm, stat } fr
|
|
|
18
18
|
import { homedir } from "node:os";
|
|
19
19
|
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
20
20
|
import { z } from "zod";
|
|
21
|
+
import { npmEnvironment, windowsShell } from "../languages/child-env.js";
|
|
21
22
|
import { npmCliPath, npmSpawnSpec } from "../languages/grammars.js";
|
|
22
23
|
import { removeTomlTable, upsertTomlTable } from "./toml.js";
|
|
23
24
|
import { compareVersions, packageVersion } from "./version.js";
|
|
@@ -34,6 +35,10 @@ working, and updates a global install older than this setup, so running
|
|
|
34
35
|
\`npx -y diffninja@latest setup\` again is how you update. When that install
|
|
35
36
|
fails it registers an npx-based entry pinned to this version and says so.
|
|
36
37
|
|
|
38
|
+
Each JSON config it changes is rewritten in full, in standard formatting
|
|
39
|
+
(indentation, string escapes and integers above 2^53 can change), and no
|
|
40
|
+
backup is kept. Copy ~/.claude.json before the first run.
|
|
41
|
+
|
|
37
42
|
Options:
|
|
38
43
|
--cli NAMES Only these CLIs, comma-separated: claude,codex,omp,pi.
|
|
39
44
|
--uninstall Remove the diffninja server from every detected CLI.
|
|
@@ -70,7 +75,7 @@ export function npxEntry(platform = process.platform, version = packageVersion()
|
|
|
70
75
|
export function windowsCliEntry(name, args, cliPath = npmCliPath) {
|
|
71
76
|
const npmCli = cliPath(`${name}-cli.js`);
|
|
72
77
|
if (npmCli === undefined) {
|
|
73
|
-
return { command:
|
|
78
|
+
return { command: windowsShell(), args: ["/d", "/s", "/c", name, ...args] };
|
|
74
79
|
}
|
|
75
80
|
return { command: process.execPath, args: [npmCli, ...args] };
|
|
76
81
|
}
|
|
@@ -108,7 +113,8 @@ export function globalIsOlder(installed, version) {
|
|
|
108
113
|
*/
|
|
109
114
|
export function createNpm(overrides = {}) {
|
|
110
115
|
const platform = overrides.platform ?? process.platform;
|
|
111
|
-
|
|
116
|
+
// Installing runs the install scripts of diffninja and its dependencies: they get what npm needs, not the shell's tokens.
|
|
117
|
+
const env = overrides.env ?? npmEnvironment();
|
|
112
118
|
const run = (args, inherit) => {
|
|
113
119
|
const spec = npmSpawnSpec(args, platform);
|
|
114
120
|
return spawn(spec.file, spec.args, inherit ? { stdio: "inherit", env } : { env });
|
|
@@ -144,7 +150,6 @@ async function exitCode(child) {
|
|
|
144
150
|
return -1;
|
|
145
151
|
}
|
|
146
152
|
}
|
|
147
|
-
const realNpm = createNpm();
|
|
148
153
|
/** `npm root -g`, or undefined when npm is unavailable or fails. */
|
|
149
154
|
async function globalRoot(npm) {
|
|
150
155
|
try {
|
|
@@ -557,7 +562,7 @@ export async function runSetup(options = {}, deps = {}) {
|
|
|
557
562
|
let entry;
|
|
558
563
|
let viaNpx = false;
|
|
559
564
|
if (!uninstall) {
|
|
560
|
-
const resolved = await resolveEntry(options.noInstall === true, deps.npm ??
|
|
565
|
+
const resolved = await resolveEntry(options.noInstall === true, deps.npm ?? createNpm(), quiet, dryRun, version);
|
|
561
566
|
entry = resolved.entry;
|
|
562
567
|
viaNpx = resolved.viaNpx;
|
|
563
568
|
}
|
package/dist/review/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UpdateNotice } from "./update-check.js";
|
|
1
2
|
import type { PullRequestIntent, ReviewEvidence } from "./evidence-types.js";
|
|
2
3
|
import type { ChangeFacts } from "./change-facts.js";
|
|
3
4
|
import type { ReviewQuestion } from "./questions.js";
|
|
@@ -62,10 +63,12 @@ export interface CallFlowFile {
|
|
|
62
63
|
* Why structured call flows are present or absent. `available` means at least
|
|
63
64
|
* one changed file has trees; `no-changes` means there was nothing structural to
|
|
64
65
|
* attach (no changed file with text hunks, or no tree reaching one) and is not a
|
|
65
|
-
* safety claim; `
|
|
66
|
-
* the
|
|
66
|
+
* safety claim; `partial` means the analysis left source files out (over the
|
|
67
|
+
* size bound or past the file limit, named in the warnings), with or without
|
|
68
|
+
* trees, so paths through them are absent; `needs-git-range` means the input was
|
|
69
|
+
* a patch; `failed` means the analysis threw.
|
|
67
70
|
*/
|
|
68
|
-
export type CallFlowAvailability = "available" | "needs-git-range" | "no-changes" | "failed";
|
|
71
|
+
export type CallFlowAvailability = "available" | "needs-git-range" | "no-changes" | "partial" | "failed";
|
|
69
72
|
/**
|
|
70
73
|
* One keyed piece of structured review context for a hunk: a changed, caller, or
|
|
71
74
|
* callee definition with its snapshot-bound source and the call/binding evidence
|
|
@@ -132,16 +135,21 @@ export interface AgentOrder {
|
|
|
132
135
|
/** diffninja's own order of the same items, kept so the page can still offer it. */
|
|
133
136
|
diffninjaIds: string[];
|
|
134
137
|
}
|
|
135
|
-
/** How
|
|
136
|
-
export declare const
|
|
137
|
-
export type
|
|
138
|
-
/** One line comment the reviewing agent suggests
|
|
138
|
+
/** How the agent came to believe a comment: it ran or reproduced the failure, or followed the code path by reading it. A guess is not a blocker. */
|
|
139
|
+
export declare const COMMENT_EVIDENCE: readonly ["ran", "traced"];
|
|
140
|
+
export type CommentEvidence = (typeof COMMENT_EVIDENCE)[number];
|
|
141
|
+
/** One line comment the reviewing agent suggests. Every one claims to block the merge, so each carries its proof. */
|
|
139
142
|
export interface SuggestedComment {
|
|
140
143
|
path: string;
|
|
141
144
|
line: number;
|
|
142
145
|
side: "LEFT" | "RIGHT";
|
|
146
|
+
/** The reviewer's own words; the only field that joins the human's draft. */
|
|
143
147
|
body: string;
|
|
144
|
-
|
|
148
|
+
/** A concrete input or state and the wrong result, or the written rule it breaks and where the rule is written. */
|
|
149
|
+
scenario: string;
|
|
150
|
+
evidence: CommentEvidence;
|
|
151
|
+
/** What would have to be true for this not to be a problem. */
|
|
152
|
+
unlessTrue: string;
|
|
145
153
|
}
|
|
146
154
|
export interface AgentComments {
|
|
147
155
|
comments: SuggestedComment[];
|
|
@@ -180,7 +188,7 @@ export interface ReviewReport {
|
|
|
180
188
|
questions: ReviewQuestion[];
|
|
181
189
|
/** The reading order the reviewing agent recorded; once present, `items` follow it. */
|
|
182
190
|
agentOrder?: AgentOrder;
|
|
183
|
-
/**
|
|
191
|
+
/** Comments the reviewing agent says block the merge; nothing is posted until the human submits them. */
|
|
184
192
|
agentComments?: AgentComments;
|
|
185
193
|
/**
|
|
186
194
|
* The reviewing agent's own paragraph on the pull request's goal, written
|
|
@@ -189,6 +197,8 @@ export interface ReviewReport {
|
|
|
189
197
|
* agent's reading rather than a verified claim.
|
|
190
198
|
*/
|
|
191
199
|
agentSummary?: AgentSummary;
|
|
200
|
+
/** A newer diffninja exists on npm; set only by the executable's opt-in version lookup. */
|
|
201
|
+
updateNotice?: UpdateNotice;
|
|
192
202
|
/**
|
|
193
203
|
* Every function a reader meets in this report (around the hunks and in the
|
|
194
204
|
* call flows), each with a stable `<file>#<name>` id, for the reviewing agent
|
package/dist/review/types.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/** How
|
|
2
|
-
export const
|
|
1
|
+
/** How the agent came to believe a comment: it ran or reproduced the failure, or followed the code path by reading it. A guess is not a blocker. */
|
|
2
|
+
export const COMMENT_EVIDENCE = ["ran", "traced"];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tells the user when a newer diffninja is on npm. It is OFF unless the person
|
|
3
|
+
* who starts the server turns it on with `DIFFNINJA_UPDATE_CHECK=1`: a review
|
|
4
|
+
* tool that reads company code should make no request of its own by default.
|
|
5
|
+
* When on, the lookup is one GET of the package's latest version, sent the
|
|
6
|
+
* first time a review is requested (never at process start), and any failure
|
|
7
|
+
* means "no notice", never an error. The server library never looks anything up
|
|
8
|
+
* unless it is handed a lookup; only `mcp-cli.ts` builds one, from the
|
|
9
|
+
* environment.
|
|
10
|
+
*/
|
|
11
|
+
/** How to update: re-running setup installs the newest package and re-points every agent. */
|
|
12
|
+
export declare const UPDATE_COMMAND = "npx diffninja@latest setup";
|
|
13
|
+
export interface UpdateNotice {
|
|
14
|
+
current: string;
|
|
15
|
+
latest: string;
|
|
16
|
+
command: string;
|
|
17
|
+
}
|
|
18
|
+
export type LatestVersion = () => Promise<string | undefined>;
|
|
19
|
+
/** The newest published release, or undefined when the registry cannot be reached or answers oddly. */
|
|
20
|
+
export declare function registryLatest(): Promise<string | undefined>;
|
|
21
|
+
/**
|
|
22
|
+
* The lookup the executable uses: none unless `DIFFNINJA_UPDATE_CHECK=1`, and
|
|
23
|
+
* never in CI or when npm's own `NO_UPDATE_NOTIFIER` is set.
|
|
24
|
+
*/
|
|
25
|
+
export declare function updateLookupFromEnv(env?: NodeJS.ProcessEnv): LatestVersion | undefined;
|
|
26
|
+
/** One lookup per connection, started by the first review that asks and never awaited longer than WAIT_MS. */
|
|
27
|
+
export declare class UpdateNotifier {
|
|
28
|
+
private readonly lookup;
|
|
29
|
+
private readonly current;
|
|
30
|
+
private pending;
|
|
31
|
+
constructor(lookup: LatestVersion | undefined, current?: string);
|
|
32
|
+
notice(): Promise<UpdateNotice | undefined>;
|
|
33
|
+
}
|
|
34
|
+
/** The sentence for the agent: say it first, in its own words, and carry on with the review. */
|
|
35
|
+
export declare function updateStep(notice: UpdateNotice): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tells the user when a newer diffninja is on npm. It is OFF unless the person
|
|
3
|
+
* who starts the server turns it on with `DIFFNINJA_UPDATE_CHECK=1`: a review
|
|
4
|
+
* tool that reads company code should make no request of its own by default.
|
|
5
|
+
* When on, the lookup is one GET of the package's latest version, sent the
|
|
6
|
+
* first time a review is requested (never at process start), and any failure
|
|
7
|
+
* means "no notice", never an error. The server library never looks anything up
|
|
8
|
+
* unless it is handed a lookup; only `mcp-cli.ts` builds one, from the
|
|
9
|
+
* environment.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { compareVersions, packageVersion } from "./version.js";
|
|
13
|
+
/** How to update: re-running setup installs the newest package and re-points every agent. */
|
|
14
|
+
export const UPDATE_COMMAND = "npx diffninja@latest setup";
|
|
15
|
+
const REGISTRY_URL = "https://registry.npmjs.org/diffninja/latest";
|
|
16
|
+
const LOOKUP_TIMEOUT_MS = 3000;
|
|
17
|
+
/** The longest a review waits for the lookup; a slower answer shows up on the next review. */
|
|
18
|
+
const WAIT_MS = 1500;
|
|
19
|
+
/** The registry answers with the whole manifest of one release (tens of KB); anything near this is not it. */
|
|
20
|
+
const MAX_RESPONSE_CHARS = 512 * 1024;
|
|
21
|
+
/** Only a plain release counts: nothing else may reach the agent's instructions or a page. */
|
|
22
|
+
const RELEASE = /^\d{1,6}\.\d{1,6}\.\d{1,6}$/;
|
|
23
|
+
const latestSchema = z.object({ version: z.string().regex(RELEASE) });
|
|
24
|
+
/** The newest published release, or undefined when the registry cannot be reached or answers oddly. */
|
|
25
|
+
export async function registryLatest() {
|
|
26
|
+
try {
|
|
27
|
+
// A redirect would send the request somewhere the fixed URL above does not name.
|
|
28
|
+
const response = await fetch(REGISTRY_URL, { headers: { accept: "application/json" }, redirect: "error", signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS) });
|
|
29
|
+
if (!response.ok)
|
|
30
|
+
return undefined;
|
|
31
|
+
const text = await response.text();
|
|
32
|
+
if (text.length > MAX_RESPONSE_CHARS)
|
|
33
|
+
return undefined;
|
|
34
|
+
return latestSchema.parse(JSON.parse(text)).version;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The lookup the executable uses: none unless `DIFFNINJA_UPDATE_CHECK=1`, and
|
|
42
|
+
* never in CI or when npm's own `NO_UPDATE_NOTIFIER` is set.
|
|
43
|
+
*/
|
|
44
|
+
export function updateLookupFromEnv(env = process.env) {
|
|
45
|
+
if (env["DIFFNINJA_UPDATE_CHECK"] !== "1")
|
|
46
|
+
return undefined;
|
|
47
|
+
if (env["NO_UPDATE_NOTIFIER"] !== undefined || env["CI"] !== undefined)
|
|
48
|
+
return undefined;
|
|
49
|
+
return registryLatest;
|
|
50
|
+
}
|
|
51
|
+
/** One lookup per connection, started by the first review that asks and never awaited longer than WAIT_MS. */
|
|
52
|
+
export class UpdateNotifier {
|
|
53
|
+
lookup;
|
|
54
|
+
current;
|
|
55
|
+
pending;
|
|
56
|
+
constructor(lookup, current = packageVersion()) {
|
|
57
|
+
this.lookup = lookup;
|
|
58
|
+
this.current = current;
|
|
59
|
+
}
|
|
60
|
+
async notice() {
|
|
61
|
+
if (this.lookup === undefined)
|
|
62
|
+
return undefined;
|
|
63
|
+
this.pending ??= this.lookup().catch(() => undefined);
|
|
64
|
+
// AbortSignal.timeout's timer never keeps the process alive, so nothing needs clearing.
|
|
65
|
+
const timer = new Promise(resolve => AbortSignal.timeout(WAIT_MS).addEventListener("abort", () => resolve(undefined), { once: true }));
|
|
66
|
+
const latest = await Promise.race([this.pending, timer]);
|
|
67
|
+
if (latest === undefined || !RELEASE.test(latest))
|
|
68
|
+
return undefined;
|
|
69
|
+
const newer = compareVersions(latest, this.current);
|
|
70
|
+
return newer !== undefined && newer > 0 ? { current: this.current, latest, command: UPDATE_COMMAND } : undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** The sentence for the agent: say it first, in its own words, and carry on with the review. */
|
|
74
|
+
export function updateStep(notice) {
|
|
75
|
+
return `Before anything else, tell the user in one sentence that diffninja ${notice.latest} is available (this is ${notice.current}) and that they update by running \`${notice.command}\` in a terminal and restarting their agent; then continue this review.`;
|
|
76
|
+
}
|
package/dist/run.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
+
import { GrammarNotInstalledError } from "./languages/grammars.js";
|
|
1
2
|
import { buildCallTreeFromInfo, exportsInFile, resolveEntry, resolveEntrypointFile, indexedFiles, } from "./calltree.js";
|
|
2
3
|
import { buildIndex, extractCached } from "./extract.js";
|
|
3
|
-
import { assertGitRepo, describeSnapshot, listSnapshotFiles, resolveDiffSnapshotsAndPaths, resolveSnapshotAndPaths, verifyCommit, visitCommitBlobs, visitWorktreeFiles, } from "./git.js";
|
|
4
|
+
import { assertGitRepo, changedPaths, describeSnapshot, listSnapshotFiles, resolveDiffSnapshotsAndPaths, resolveSnapshotAndPaths, verifyCommit, visitCommitBlobs, visitWorktreeFiles, } from "./git.js";
|
|
4
5
|
import { diffEntry, diffPinnedEntry, inferEntries, resolveExplicitDiffEntries, } from "./infer.js";
|
|
5
6
|
import { collectPathsTo, findReachPaths } from "./reach.js";
|
|
6
7
|
import { renderDiff, renderTree } from "./render.js";
|
|
7
8
|
import { assignOptionalTreeFields } from "./types.js";
|
|
8
|
-
function loadIndex(cwd, snapshot, pathFilters, cache = new Map()) {
|
|
9
|
-
const files = listSnapshotFiles(cwd, snapshot, pathFilters);
|
|
9
|
+
function loadIndex(cwd, snapshot, pathFilters, cache = new Map(), changed = new Set()) {
|
|
10
|
+
const files = listSnapshotFiles(cwd, snapshot, pathFilters, changed);
|
|
10
11
|
const extracted = new Map();
|
|
11
12
|
const extract = (file, source) => {
|
|
12
13
|
try {
|
|
13
14
|
extracted.set(file.path, extractCached(file.path, source, cache));
|
|
14
15
|
}
|
|
15
16
|
catch (error) {
|
|
17
|
+
// A grammar that is not installed is reported once, by the review, not per file.
|
|
18
|
+
if (error instanceof GrammarNotInstalledError)
|
|
19
|
+
return;
|
|
16
20
|
const message = error instanceof Error ? error.message : String(error);
|
|
17
21
|
console.error(`warn: failed to parse ${file.path} @ ${snapshot.ref}: ${message}`);
|
|
18
22
|
}
|
|
@@ -120,8 +124,10 @@ export function runDiff(options = {}) {
|
|
|
120
124
|
if (to.kind === "commit")
|
|
121
125
|
verifyCommit(cwd, to.ref);
|
|
122
126
|
const extractionCache = new Map();
|
|
123
|
-
|
|
124
|
-
const
|
|
127
|
+
// A repository past the file limit still indexes the files this diff changes.
|
|
128
|
+
const changed = changedPaths(cwd, from, to);
|
|
129
|
+
const before = loadIndex(cwd, from, resolvedPaths, extractionCache, changed);
|
|
130
|
+
const after = loadIndex(cwd, to, resolvedPaths, extractionCache, changed);
|
|
125
131
|
extractionCache.clear();
|
|
126
132
|
options.onIndexes?.(before, after);
|
|
127
133
|
const fromLabel = describeSnapshot(from);
|