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/README.md
CHANGED
|
@@ -10,7 +10,7 @@ submit the review to GitHub yourself.
|
|
|
10
10
|
|
|
11
11
|
- **Read the important changes first.** Big PRs are hard to follow file by
|
|
12
12
|
file. diffninja numbers each change, most important first, and lets you step
|
|
13
|
-
through them with `j` and `k
|
|
13
|
+
through them with `j` and `k` and tick them off.
|
|
14
14
|
- **See what the change does to the product, not just the code.** Your agent
|
|
15
15
|
explains the change in business terms: the processes it touches as
|
|
16
16
|
flowcharts, with new and changed steps highlighted, the business rules it
|
|
@@ -19,15 +19,29 @@ submit the review to GitHub yourself.
|
|
|
19
19
|
- **Your agent does the first pass.** For each change, the agent answers simple
|
|
20
20
|
questions: does it change behavior, is it tested, does it match the PR's
|
|
21
21
|
goal. The answers sit above the code.
|
|
22
|
-
- **Suggested comments, never posted for you.** The agent
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
- **Suggested comments are blockers only, never posted for you.** The agent
|
|
23
|
+
suggests a comment only for what blocks the merge, and shows its proof: the
|
|
24
|
+
case that fails, how it checked, and what would make it fine. No comment at
|
|
25
|
+
all is the normal result, and anything that does not block is dropped. You
|
|
26
|
+
add a comment with one click, edit it, or dismiss it.
|
|
27
|
+
None of diffninja's tools posts anything. The review reaches GitHub when
|
|
28
|
+
Submit is pressed on your review page, and your agent is told never to do
|
|
29
|
+
that itself.
|
|
25
30
|
- **Facts you can check.** diffninja points at the exact lines that changed a
|
|
26
31
|
comparison, a limit, an input check, or error handling. It also shows call
|
|
27
32
|
flows: which functions call the changed code.
|
|
28
|
-
- **
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
- **The analysis runs on your machine.** diffninja calls no AI model, needs no
|
|
34
|
+
API key, has no telemetry of its own, and does not download or build code
|
|
35
|
+
during a review. It makes no request of its own while it reviews, except an
|
|
36
|
+
update notice that is off unless you turn it on. It uses your existing GitHub
|
|
37
|
+
CLI login to read the PR, to post your review when Submit is pressed, and to
|
|
38
|
+
mark a file Viewed on GitHub when you tick its changes.
|
|
39
|
+
Installing diffninja and adding language grammars use npm. Your agent sends
|
|
40
|
+
what diffninja returns, source text included, to its own model, as it does
|
|
41
|
+
with any tool result. It is not "local only" in every respect.
|
|
42
|
+
[docs/security.md](docs/security.md) lists what runs, what is downloaded, what
|
|
43
|
+
is written and what your agent can reach, including the parts that are not
|
|
44
|
+
local.
|
|
31
45
|
|
|
32
46
|
## What you need
|
|
33
47
|
|
|
@@ -45,8 +59,23 @@ Run this once:
|
|
|
45
59
|
npx -y diffninja@latest setup
|
|
46
60
|
```
|
|
47
61
|
|
|
48
|
-
It installs diffninja
|
|
49
|
-
|
|
62
|
+
It installs diffninja globally with `npm install -g` (if that fails, it registers
|
|
63
|
+
an `npx` entry instead), and adds diffninja to every agent CLI it finds on your
|
|
64
|
+
machine by editing that CLI's config file. Then restart your agent CLI.
|
|
65
|
+
|
|
66
|
+
Setup rewrites a JSON config file in full, in standard formatting, and keeps no
|
|
67
|
+
backup. Indentation, string escapes and integers above 2^53 in it can change.
|
|
68
|
+
`~/.claude.json` is Claude Code's main state file, so copy it before the first
|
|
69
|
+
run (`cp ~/.claude.json ~/.claude.json.bak`).
|
|
70
|
+
|
|
71
|
+
[docs/security.md](docs/security.md) and this README describe diffninja 0.4.0
|
|
72
|
+
and later. Version 0.3.2 and earlier behave as two security audits found. The
|
|
73
|
+
review page's submit API had no secret in its path, so any local process could
|
|
74
|
+
use it. Reviews installed grammars with npm at review time. Hidden characters
|
|
75
|
+
were not marked, and the package had no pinned dependency tree.
|
|
76
|
+
`npx -y diffninja@latest setup` installs the newest published version. Check
|
|
77
|
+
that it is 0.4.0 or later with `npm ls -g diffninja`, or read the version in the
|
|
78
|
+
entry setup wrote to your agent's config.
|
|
50
79
|
|
|
51
80
|
To update later, run the same command again: setup brings an older global
|
|
52
81
|
install up to its own version, then restart your agent CLI. (Setup from
|
|
@@ -76,6 +105,12 @@ your machine at `127.0.0.1`). On the page:
|
|
|
76
105
|
`j` and `k` move to the next and previous change, and the list on the left
|
|
77
106
|
shows where you are. The agent sets the reading order; the connected page
|
|
78
107
|
does not label changes “Attention”.
|
|
108
|
+
Tick a change in that list when you have read it. Scrolling ticks nothing.
|
|
109
|
+
GitHub's **Viewed** mark belongs to a file, so a file is marked Viewed on
|
|
110
|
+
GitHub once every change of it is ticked, and unticking one clears the mark.
|
|
111
|
+
A file already marked Viewed on GitHub starts ticked. If diffninja cannot
|
|
112
|
+
read or write the marks, the page says "not synced with GitHub" and keeps
|
|
113
|
+
your ticks in the tab.
|
|
79
114
|
2. Hover a line and press **+** to write a comment, or add the agent's
|
|
80
115
|
suggestions.
|
|
81
116
|
3. Write a summary, choose **Comment**, **Approve**, or **Request changes**.
|
|
@@ -84,7 +119,21 @@ your machine at `127.0.0.1`). On the page:
|
|
|
84
119
|
|
|
85
120
|
Tip: if your agent is running inside a local clone of the repository, it can
|
|
86
121
|
pass the clone to diffninja. You then also get call-flow diagrams for the
|
|
87
|
-
changed files.
|
|
122
|
+
changed files. Call flows read JavaScript and TypeScript out of the box. For
|
|
123
|
+
other languages (Python, Go, Java, Rust, C#, Ruby, and more), install their
|
|
124
|
+
grammars once. This step downloads grammar code through npm at exact versions,
|
|
125
|
+
checked against hashes shipped with diffninja, and runs no install script. Use
|
|
126
|
+
the version of diffninja your agent runs, because diffninja may not read
|
|
127
|
+
grammars that another version installed. A review that needs them says which
|
|
128
|
+
files its call flows skipped and prints the exact command, which has the form
|
|
129
|
+
`npx -y diffninja@<version> grammars install`. Kotlin and Perl also need
|
|
130
|
+
`--build`, which compiles them on your machine and runs their install scripts.
|
|
131
|
+
A review never installs anything itself.
|
|
132
|
+
|
|
133
|
+
If the local clone is a partial clone (for example made with
|
|
134
|
+
`git clone --filter=blob:none`), git may fetch missing objects from that clone's
|
|
135
|
+
own remote while diffninja reads it, as `git log -p` would. diffninja itself
|
|
136
|
+
never runs a fetch or a checkout.
|
|
88
137
|
|
|
89
138
|
You can also review changes that aren't a PR yet:
|
|
90
139
|
|
|
@@ -100,7 +149,12 @@ and gives you a link to read the same report in your browser.
|
|
|
100
149
|
- The review page closes when you exit your agent CLI.
|
|
101
150
|
- The page and the agent's session contain source code. Treat them like the
|
|
102
151
|
code itself.
|
|
103
|
-
- diffninja
|
|
152
|
+
- No diffninja tool approves, blocks, merges or posts anything. A review is
|
|
153
|
+
submitted when Submit is pressed on the review page. The page's one other
|
|
154
|
+
write to GitHub is a file's Viewed mark, which follows your ticks. Anyone
|
|
155
|
+
holding that page's link can do both through the page's API, and your agent
|
|
156
|
+
holds the link. diffninja tells the agent never to submit. It cannot enforce
|
|
157
|
+
that.
|
|
104
158
|
|
|
105
159
|
## More
|
|
106
160
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a command the review runs (`git`, `gh`) really is.
|
|
3
|
+
*
|
|
4
|
+
* On Windows the runtime looks for a bare command name in the child's working
|
|
5
|
+
* directory before it looks at PATH. Every git call runs with the repository under
|
|
6
|
+
* review as its working directory, so a `git.exe` committed in that repository, or
|
|
7
|
+
* a `gh.exe` in the directory the agent was started in, would run instead of the
|
|
8
|
+
* real tool as soon as its branch is checked out. Here the name is resolved once
|
|
9
|
+
* against PATH's absolute entries only, to a full path, and that path is what runs.
|
|
10
|
+
* Elsewhere the name is returned as it is: PATH lookup does not search the
|
|
11
|
+
* working directory.
|
|
12
|
+
*/
|
|
13
|
+
export interface ExecutableLookup {
|
|
14
|
+
readonly platform?: NodeJS.Platform;
|
|
15
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
16
|
+
readonly exists?: (path: string) => boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function resolveExecutable(name: string, lookup?: ExecutableLookup): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { win32 } from "node:path";
|
|
3
|
+
/** Only real executables: a `.cmd` or `.bat` cannot be started without a shell. */
|
|
4
|
+
const EXTENSIONS = [".exe", ".com"];
|
|
5
|
+
const resolved = new Map();
|
|
6
|
+
export function resolveExecutable(name, lookup = {}) {
|
|
7
|
+
const platform = lookup.platform ?? process.platform;
|
|
8
|
+
if (platform !== "win32")
|
|
9
|
+
return name;
|
|
10
|
+
const env = lookup.env ?? process.env;
|
|
11
|
+
const cacheable = lookup.env === undefined && lookup.exists === undefined;
|
|
12
|
+
const known = cacheable ? resolved.get(name) : undefined;
|
|
13
|
+
if (known !== undefined)
|
|
14
|
+
return known;
|
|
15
|
+
const exists = lookup.exists ?? existsSync;
|
|
16
|
+
const path = env["PATH"] ?? env["Path"] ?? "";
|
|
17
|
+
for (const raw of path.split(";")) {
|
|
18
|
+
const directory = raw.trim().replace(/^"|"$/g, "");
|
|
19
|
+
// A relative or empty entry means "here": never a place to find a tool.
|
|
20
|
+
if (directory === "" || !win32.isAbsolute(directory))
|
|
21
|
+
continue;
|
|
22
|
+
for (const extension of EXTENSIONS) {
|
|
23
|
+
const candidate = win32.join(directory, name + extension);
|
|
24
|
+
if (!exists(candidate))
|
|
25
|
+
continue;
|
|
26
|
+
if (cacheable)
|
|
27
|
+
resolved.set(name, candidate);
|
|
28
|
+
return candidate;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`${name} was not found on PATH. Only absolute PATH entries are searched, never the repository or the current directory.`);
|
|
32
|
+
}
|
package/dist/git.d.ts
CHANGED
|
@@ -26,7 +26,32 @@ export interface SnapshotFile {
|
|
|
26
26
|
oid?: string;
|
|
27
27
|
size?: number;
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Bounds on what call-flow analysis parses from one revision: a source file over
|
|
31
|
+
* this size is generated or minified code, not something a person reads. Past
|
|
32
|
+
* the file limit, the files the diff changes are read first, then the rest of
|
|
33
|
+
* their directories, then everything else in code-point order, so the same range
|
|
34
|
+
* leaves out the same files on every machine. The review names what was skipped.
|
|
35
|
+
*/
|
|
36
|
+
export declare const MAX_INDEXED_FILE_BYTES: number;
|
|
37
|
+
export declare const MAX_INDEXED_FILES = 15000;
|
|
38
|
+
export interface SkippedSources {
|
|
39
|
+
/** Source files over the size bound. */
|
|
40
|
+
oversized: number;
|
|
41
|
+
/** Files past the count bound. */
|
|
42
|
+
beyondLimit: number;
|
|
43
|
+
}
|
|
44
|
+
/** What was left out of call-flow analysis since the last call; clears the count. */
|
|
45
|
+
export declare function takeSkippedSources(): SkippedSources;
|
|
46
|
+
/**
|
|
47
|
+
* Paths that differ between two snapshots, both sides of a rename included,
|
|
48
|
+
* relative to `cwd` as the snapshot listings are.
|
|
49
|
+
*/
|
|
50
|
+
export declare function changedPaths(cwd: string, from: Snapshot, to: Snapshot): Set<string>;
|
|
51
|
+
export declare function listSnapshotFiles(cwd: string, snapshot: Snapshot, pathFilters?: string[], changed?: ReadonlySet<string>, limits?: {
|
|
52
|
+
readonly maxFiles: number;
|
|
53
|
+
readonly maxFileBytes: number;
|
|
54
|
+
}): SnapshotFile[];
|
|
30
55
|
export declare function visitCommitBlobs(cwd: string, files: SnapshotFile[], visit: (oid: string, source: string) => void): void;
|
|
31
56
|
export declare function visitWorktreeFiles(cwd: string, files: SnapshotFile[], visit: (file: SnapshotFile, source: string) => void): void;
|
|
32
57
|
export declare function describeSnapshot(snapshot: Snapshot): string;
|
package/dist/git.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import { resolveExecutable } from "./executables.js";
|
|
1
2
|
import { execFileSync } from "node:child_process";
|
|
2
3
|
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
3
4
|
import { resolve } from "node:path";
|
|
4
5
|
import { listSupportedExtensions } from "./languages/registry.js";
|
|
6
|
+
/** A git command that has not answered in this long (a dead network share) is stopped, not waited for forever. */
|
|
7
|
+
const GIT_TIMEOUT_MS = 120_000;
|
|
5
8
|
function gitBuffer(cwd, args, input, maxBuffer = 64 * 1024 * 1024) {
|
|
6
|
-
return execFileSync("git", ["--no-replace-objects", ...args], {
|
|
9
|
+
return execFileSync(resolveExecutable("git"), ["--no-replace-objects", ...args], {
|
|
7
10
|
cwd,
|
|
8
11
|
input,
|
|
9
12
|
maxBuffer,
|
|
13
|
+
timeout: GIT_TIMEOUT_MS,
|
|
10
14
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11
15
|
});
|
|
12
16
|
}
|
|
@@ -183,13 +187,61 @@ function pathAllowed(file, pathFilters) {
|
|
|
183
187
|
file.endsWith(normalized));
|
|
184
188
|
});
|
|
185
189
|
}
|
|
186
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Bounds on what call-flow analysis parses from one revision: a source file over
|
|
192
|
+
* this size is generated or minified code, not something a person reads. Past
|
|
193
|
+
* the file limit, the files the diff changes are read first, then the rest of
|
|
194
|
+
* their directories, then everything else in code-point order, so the same range
|
|
195
|
+
* leaves out the same files on every machine. The review names what was skipped.
|
|
196
|
+
*/
|
|
197
|
+
export const MAX_INDEXED_FILE_BYTES = 1024 * 1024;
|
|
198
|
+
export const MAX_INDEXED_FILES = 15_000;
|
|
199
|
+
/** Paths, so a file left out of both revisions of a diff counts once. */
|
|
200
|
+
const skipped = { oversized: new Set(), beyondLimit: new Set() };
|
|
201
|
+
/** What was left out of call-flow analysis since the last call; clears the count. */
|
|
202
|
+
export function takeSkippedSources() {
|
|
203
|
+
const counts = { oversized: skipped.oversized.size, beyondLimit: skipped.beyondLimit.size };
|
|
204
|
+
skipped.oversized.clear();
|
|
205
|
+
skipped.beyondLimit.clear();
|
|
206
|
+
return counts;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Paths that differ between two snapshots, both sides of a rename included,
|
|
210
|
+
* relative to `cwd` as the snapshot listings are.
|
|
211
|
+
*/
|
|
212
|
+
export function changedPaths(cwd, from, to) {
|
|
213
|
+
const refs = [from, to].filter((snapshot) => snapshot.kind === "commit").map((snapshot) => snapshot.ref);
|
|
214
|
+
return new Set(git(cwd, ["diff", "--relative", "--name-only", "-z", "--no-renames", ...refs, "--"]).split("\0").filter(Boolean));
|
|
215
|
+
}
|
|
216
|
+
function directoryOf(path) {
|
|
217
|
+
return path.slice(0, path.lastIndexOf("/") + 1);
|
|
218
|
+
}
|
|
219
|
+
export function listSnapshotFiles(cwd, snapshot, pathFilters = [], changed = new Set(), limits = { maxFiles: MAX_INDEXED_FILES, maxFileBytes: MAX_INDEXED_FILE_BYTES }) {
|
|
187
220
|
const files = snapshot.kind === "worktree"
|
|
188
221
|
? listWorktreeFiles(cwd)
|
|
189
222
|
: listCommitFiles(cwd, snapshot.ref);
|
|
190
|
-
|
|
223
|
+
const wanted = files
|
|
191
224
|
.filter((file) => pathAllowed(file.path, pathFilters))
|
|
192
|
-
.sort((a, b) => a.path.
|
|
225
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
226
|
+
const small = [];
|
|
227
|
+
for (const file of wanted) {
|
|
228
|
+
if (file.size !== undefined && file.size > limits.maxFileBytes)
|
|
229
|
+
skipped.oversized.add(file.path);
|
|
230
|
+
else
|
|
231
|
+
small.push(file);
|
|
232
|
+
}
|
|
233
|
+
if (small.length <= limits.maxFiles)
|
|
234
|
+
return small;
|
|
235
|
+
const directories = new Set([...changed].map(directoryOf));
|
|
236
|
+
const first = [], neighbours = [], rest = [];
|
|
237
|
+
for (const file of small) {
|
|
238
|
+
(changed.has(file.path) ? first : directories.has(directoryOf(file.path)) ? neighbours : rest).push(file);
|
|
239
|
+
}
|
|
240
|
+
const kept = new Set([...first, ...neighbours, ...rest].slice(0, limits.maxFiles));
|
|
241
|
+
for (const file of small)
|
|
242
|
+
if (!kept.has(file))
|
|
243
|
+
skipped.beyondLimit.add(file.path);
|
|
244
|
+
return small.filter((file) => kept.has(file));
|
|
193
245
|
}
|
|
194
246
|
const BATCH_BYTES = 32 * 1024 * 1024;
|
|
195
247
|
function chunkBlobs(files) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Compared without regard to case: Windows spells `Path`, `ComSpec` and `SystemRoot` its own way. */
|
|
2
|
+
export declare function npmEnvironment(env?: NodeJS.ProcessEnv, extra?: Readonly<Record<string, string>>, options?: {
|
|
3
|
+
readonly build?: boolean;
|
|
4
|
+
}): NodeJS.ProcessEnv;
|
|
5
|
+
/**
|
|
6
|
+
* The shell Windows runs a `.cmd` shim with, when no npm JS entry point can be
|
|
7
|
+
* found. ComSpec is used only when it is an absolute path to `cmd.exe`; anything
|
|
8
|
+
* else in that variable would otherwise be written into an agent's config and run
|
|
9
|
+
* every time the agent starts.
|
|
10
|
+
*/
|
|
11
|
+
export declare function windowsShell(env?: NodeJS.ProcessEnv): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* The environment npm gets when diffninja runs it. npm and the install scripts
|
|
4
|
+
* of the packages it fetches inherit their parent's environment, and an engineer's
|
|
5
|
+
* shell carries tokens (GitHub, cloud providers, package registries) that no
|
|
6
|
+
* install has any business seeing. Only what npm and node-gyp need to find their
|
|
7
|
+
* tools, cache, proxy and certificates is passed on; npm's own `npm_config_*`
|
|
8
|
+
* variables are the user's npm configuration and pass too.
|
|
9
|
+
*/
|
|
10
|
+
const KEPT = new Set([
|
|
11
|
+
"PATH", "HOME", "USER", "LOGNAME", "LANG", "LC_ALL", "LC_CTYPE", "TERM", "TZ",
|
|
12
|
+
"TMPDIR", "TMP", "TEMP",
|
|
13
|
+
"XDG_CACHE_HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME",
|
|
14
|
+
"HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "ALL_PROXY",
|
|
15
|
+
"NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "SSL_CERT_DIR",
|
|
16
|
+
// Windows: without these npm cannot find its own files, Python or the compiler.
|
|
17
|
+
"USERPROFILE", "HOMEDRIVE", "HOMEPATH", "APPDATA", "LOCALAPPDATA", "PROGRAMDATA",
|
|
18
|
+
"PROGRAMFILES", "PROGRAMFILES(X86)", "PROGRAMW6432", "COMMONPROGRAMFILES", "COMMONPROGRAMFILES(X86)",
|
|
19
|
+
"SYSTEMROOT", "SYSTEMDRIVE", "WINDIR", "COMSPEC", "PATHEXT", "OS", "PROCESSOR_ARCHITECTURE", "NUMBER_OF_PROCESSORS",
|
|
20
|
+
]);
|
|
21
|
+
/** What a source build (node-gyp) also looks for: compiler, Python and SDK locations. Only when the person asked for a build. */
|
|
22
|
+
const BUILD_KEPT = new Set(["CC", "CXX", "CFLAGS", "CXXFLAGS", "CPPFLAGS", "LDFLAGS", "PYTHON", "SDKROOT", "DEVELOPER_DIR", "MACOSX_DEPLOYMENT_TARGET", "INCLUDE", "LIB", "LIBPATH"]);
|
|
23
|
+
const BUILD_PREFIXES = ["GYP_", "VSINSTALLDIR", "VCINSTALLDIR", "VCTOOLS", "VSCMD_", "VS1", "VISUALSTUDIO", "WINDOWSSDK", "UNIVERSALCRT", "UCRT"];
|
|
24
|
+
/**
|
|
25
|
+
* The variables the person running diffninja chose to pass to npm as well, named in
|
|
26
|
+
* DIFFNINJA_NPM_ENV (for example `NPM_TOKEN,NODE_AUTH_TOKEN`). A private registry whose
|
|
27
|
+
* `.npmrc` reads its token from the environment answers 401 without it. Names only. Anything
|
|
28
|
+
* else is refused without being echoed, because what was written there may be the secret itself.
|
|
29
|
+
*/
|
|
30
|
+
function chosenNames(env) {
|
|
31
|
+
const names = (env["DIFFNINJA_NPM_ENV"] ?? "").split(",").map((name) => name.trim()).filter((name) => name !== "");
|
|
32
|
+
if (names.some((name) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))) {
|
|
33
|
+
throw new Error("DIFFNINJA_NPM_ENV must list only environment variable names separated by commas, such as NPM_TOKEN,NODE_AUTH_TOKEN.");
|
|
34
|
+
}
|
|
35
|
+
return new Set(names.map((name) => name.toUpperCase()));
|
|
36
|
+
}
|
|
37
|
+
/** Compared without regard to case: Windows spells `Path`, `ComSpec` and `SystemRoot` its own way. */
|
|
38
|
+
export function npmEnvironment(env = process.env, extra = {}, options = {}) {
|
|
39
|
+
const chosen = chosenNames(env);
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [name, value] of Object.entries(env)) {
|
|
42
|
+
if (value === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
const upper = name.toUpperCase();
|
|
45
|
+
const forBuild = options.build === true && (BUILD_KEPT.has(upper) || BUILD_PREFIXES.some((prefix) => upper.startsWith(prefix)));
|
|
46
|
+
if (KEPT.has(upper) || upper.startsWith("NPM_CONFIG_") || upper.startsWith("LC_") || forBuild || chosen.has(upper))
|
|
47
|
+
out[name] = value;
|
|
48
|
+
}
|
|
49
|
+
return { ...out, ...extra };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The shell Windows runs a `.cmd` shim with, when no npm JS entry point can be
|
|
53
|
+
* found. ComSpec is used only when it is an absolute path to `cmd.exe`; anything
|
|
54
|
+
* else in that variable would otherwise be written into an agent's config and run
|
|
55
|
+
* every time the agent starts.
|
|
56
|
+
*/
|
|
57
|
+
export function windowsShell(env = process.env) {
|
|
58
|
+
const candidate = env["ComSpec"] ?? env["COMSPEC"];
|
|
59
|
+
return candidate !== undefined && /[\\/]cmd\.exe$/i.test(candidate) && (isAbsolute(candidate) || /^[A-Za-z]:[\\/]/.test(candidate)) ? candidate : "cmd.exe";
|
|
60
|
+
}
|