@westbayberry/dg 1.0.53 → 1.0.56
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 +5 -1
- package/dist/index.mjs +249 -114
- package/dist/packages/cli/src/alt-screen.js +36 -0
- package/dist/packages/cli/src/api.js +322 -0
- package/dist/packages/cli/src/auth.js +218 -0
- package/dist/packages/cli/src/bin.js +386 -0
- package/dist/packages/cli/src/config.js +228 -0
- package/dist/packages/cli/src/discover.js +126 -0
- package/dist/packages/cli/src/first-run.js +135 -0
- package/dist/packages/cli/src/hook.js +360 -0
- package/dist/packages/cli/src/lockfile.js +303 -0
- package/dist/packages/cli/src/npm-wrapper.js +218 -0
- package/dist/packages/cli/src/pip-wrapper.js +273 -0
- package/dist/packages/cli/src/sanitize.js +38 -0
- package/dist/packages/cli/src/scan-core.js +144 -0
- package/dist/packages/cli/src/setup-status.js +46 -0
- package/dist/packages/cli/src/static-output.js +625 -0
- package/dist/packages/cli/src/telemetry.js +141 -0
- package/dist/packages/cli/src/ui/App.js +137 -0
- package/dist/packages/cli/src/ui/InitApp.js +391 -0
- package/dist/packages/cli/src/ui/LoginApp.js +51 -0
- package/dist/packages/cli/src/ui/NpmWrapperApp.js +73 -0
- package/dist/packages/cli/src/ui/PipWrapperApp.js +72 -0
- package/dist/packages/cli/src/ui/components/ConfirmPrompt.js +24 -0
- package/dist/packages/cli/src/ui/components/DemoScanAnimation.js +26 -0
- package/dist/packages/cli/src/ui/components/DurationLine.js +7 -0
- package/dist/packages/cli/src/ui/components/ErrorView.js +30 -0
- package/dist/packages/cli/src/ui/components/FileSavePrompt.js +210 -0
- package/dist/packages/cli/src/ui/components/InteractiveResultsView.js +557 -0
- package/dist/packages/cli/src/ui/components/Mascot.js +33 -0
- package/dist/packages/cli/src/ui/components/ProgressBar.js +51 -0
- package/dist/packages/cli/src/ui/components/ProgressDots.js +35 -0
- package/dist/packages/cli/src/ui/components/ProjectSelector.js +60 -0
- package/dist/packages/cli/src/ui/components/ResultsView.js +105 -0
- package/dist/packages/cli/src/ui/components/ScanResultCard.js +54 -0
- package/dist/packages/cli/src/ui/components/ScoreHeader.js +142 -0
- package/dist/packages/cli/src/ui/components/SetupBanner.js +17 -0
- package/dist/packages/cli/src/ui/components/Spinner.js +11 -0
- package/dist/packages/cli/src/ui/hooks/useExpandAnimation.js +44 -0
- package/dist/packages/cli/src/ui/hooks/useInit.js +341 -0
- package/dist/packages/cli/src/ui/hooks/useLogin.js +121 -0
- package/dist/packages/cli/src/ui/hooks/useNpmWrapper.js +192 -0
- package/dist/packages/cli/src/ui/hooks/usePipWrapper.js +195 -0
- package/dist/packages/cli/src/ui/hooks/useScan.js +202 -0
- package/dist/packages/cli/src/ui/hooks/useTerminalSize.js +29 -0
- package/dist/packages/cli/src/update-check.js +152 -0
- package/dist/packages/cli/src/wizard-demo-data.js +63 -0
- package/dist/src/ecosystem.js +2 -0
- package/dist/src/lockfile/diff.js +38 -0
- package/dist/src/lockfile/parse_package_json.js +41 -0
- package/dist/src/lockfile/parse_package_lock.js +55 -0
- package/dist/src/lockfile/parse_pipfile_lock.js +69 -0
- package/dist/src/lockfile/parse_pnpm_lock.js +62 -0
- package/dist/src/lockfile/parse_poetry_lock.js +71 -0
- package/dist/src/lockfile/parse_requirements.js +83 -0
- package/dist/src/lockfile/parse_yarn_lock.js +66 -0
- package/dist/src/logger.js +21 -0
- package/dist/src/npm/h2pool.js +161 -0
- package/dist/src/npm/registry.js +299 -0
- package/dist/src/npm/tarball.js +274 -0
- package/dist/src/pypi/registry.js +299 -0
- package/dist/src/pypi/tarball.js +361 -0
- package/dist/src/types.js +2 -0
- package/package.json +6 -3
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.discoverChanges = discoverChanges;
|
|
4
|
+
exports.parsePythonDepFile = parsePythonDepFile;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const MAX_LOCKFILE_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
9
|
+
const SELF_PACKAGE = "@westbayberry/dg"; // skip scanning ourselves
|
|
10
|
+
function readFileSafe(path) {
|
|
11
|
+
const size = (0, node_fs_1.statSync)(path).size;
|
|
12
|
+
if (size > MAX_LOCKFILE_BYTES) {
|
|
13
|
+
throw new Error(`Lockfile too large (${(size / 1024 / 1024).toFixed(0)} MB, max 50 MB): ${path}`);
|
|
14
|
+
}
|
|
15
|
+
return (0, node_fs_1.readFileSync)(path, "utf-8");
|
|
16
|
+
}
|
|
17
|
+
// Shared with the GitHub App & root scanner — parsers live at dependency-guardian/src/lockfile/
|
|
18
|
+
// esbuild bundles these at build time; runtime artifact is self-contained.
|
|
19
|
+
const parse_package_lock_1 = require("../../../src/lockfile/parse_package_lock");
|
|
20
|
+
const parse_yarn_lock_1 = require("../../../src/lockfile/parse_yarn_lock");
|
|
21
|
+
const parse_pnpm_lock_1 = require("../../../src/lockfile/parse_pnpm_lock");
|
|
22
|
+
const diff_1 = require("../../../src/lockfile/diff");
|
|
23
|
+
const parse_package_json_1 = require("../../../src/lockfile/parse_package_json");
|
|
24
|
+
/**
|
|
25
|
+
* Discover changed (or all) packages by reading the local lockfile
|
|
26
|
+
* and comparing against a base.
|
|
27
|
+
*/
|
|
28
|
+
function discoverChanges(cwd, config) {
|
|
29
|
+
if (config.workspace) {
|
|
30
|
+
cwd = (0, node_path_1.join)(cwd, config.workspace);
|
|
31
|
+
}
|
|
32
|
+
const lockfileInfo = findLockfile(cwd);
|
|
33
|
+
// Discover Python packages from requirements.txt / Pipfile.lock / poetry.lock
|
|
34
|
+
const pythonDepFiles = ["requirements.txt", "Pipfile.lock", "poetry.lock"];
|
|
35
|
+
let pythonPackages = [];
|
|
36
|
+
for (const pyFile of pythonDepFiles) {
|
|
37
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(cwd, pyFile))) {
|
|
38
|
+
const pyPkgs = parsePythonDepFile(cwd, pyFile);
|
|
39
|
+
for (const p of pyPkgs) {
|
|
40
|
+
if (p.version === "latest")
|
|
41
|
+
continue; // skip unpinned
|
|
42
|
+
pythonPackages.push({ name: p.name, version: p.version, previousVersion: null, isNew: true });
|
|
43
|
+
}
|
|
44
|
+
break; // use first found
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!lockfileInfo) {
|
|
48
|
+
// No npm lockfile — return Python packages only if found
|
|
49
|
+
if (pythonPackages.length > 0) {
|
|
50
|
+
const skipped = [];
|
|
51
|
+
if (pythonPackages.length > config.maxPackages) {
|
|
52
|
+
skipped.push(...pythonPackages.slice(config.maxPackages).map((p) => `${p.name}@${p.version}`));
|
|
53
|
+
pythonPackages = pythonPackages.slice(0, config.maxPackages);
|
|
54
|
+
}
|
|
55
|
+
return { packages: [], pythonPackages, method: "scan-all", skipped };
|
|
56
|
+
}
|
|
57
|
+
throw new Error("No lockfile found (package-lock.json, yarn.lock, pnpm-lock.yaml, requirements.txt, Pipfile.lock, or poetry.lock). Run from your project root or use --base-lockfile.");
|
|
58
|
+
}
|
|
59
|
+
const headContent = readFileSafe(lockfileInfo.path);
|
|
60
|
+
const headParsed = parseLockfileByType(headContent, lockfileInfo.type);
|
|
61
|
+
const directDeps = getDirectDeps(cwd);
|
|
62
|
+
// 1. --base-lockfile: explicit diff (wins over scanAll default)
|
|
63
|
+
if (config.baseLockfile) {
|
|
64
|
+
if (!(0, node_fs_1.existsSync)(config.baseLockfile)) {
|
|
65
|
+
throw new Error(`Base lockfile not found: ${config.baseLockfile}`);
|
|
66
|
+
}
|
|
67
|
+
const baseContent = readFileSafe(config.baseLockfile);
|
|
68
|
+
const baseParsed = (0, parse_package_lock_1.parseLockfile)(baseContent);
|
|
69
|
+
const diff = (0, diff_1.diffLockfiles)(baseParsed, headParsed, config.maxPackages, directDeps);
|
|
70
|
+
return {
|
|
71
|
+
packages: diff.changes.map(toPackageInput).filter((p) => p.name !== SELF_PACKAGE),
|
|
72
|
+
pythonPackages,
|
|
73
|
+
method: "base-lockfile",
|
|
74
|
+
skipped: diff.skipped,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// 2. --changed-only: diff against git history
|
|
78
|
+
if (!config.scanAll) {
|
|
79
|
+
const baseContent = getGitBaseLockfile(cwd);
|
|
80
|
+
if (baseContent !== null) {
|
|
81
|
+
const baseParsed = (0, parse_package_lock_1.parseLockfile)(baseContent);
|
|
82
|
+
const diff = (0, diff_1.diffLockfiles)(baseParsed, headParsed, config.maxPackages, directDeps);
|
|
83
|
+
return {
|
|
84
|
+
packages: diff.changes.map(toPackageInput).filter((p) => p.name !== SELF_PACKAGE),
|
|
85
|
+
pythonPackages,
|
|
86
|
+
method: "git-diff",
|
|
87
|
+
skipped: diff.skipped,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const pkgJsonPath = (0, node_path_1.join)(cwd, "package.json");
|
|
91
|
+
if ((0, node_fs_1.existsSync)(pkgJsonPath)) {
|
|
92
|
+
const headPkgJson = readFileSafe(pkgJsonPath);
|
|
93
|
+
const basePkgJson = getGitBaseFile(cwd, "package.json");
|
|
94
|
+
if (basePkgJson !== null) {
|
|
95
|
+
const diff = (0, parse_package_json_1.diffPackageJsons)(basePkgJson, headPkgJson, config.maxPackages);
|
|
96
|
+
const resolved = diff.changes.map((change) => {
|
|
97
|
+
const lockEntry = headParsed.packages.get(change.name);
|
|
98
|
+
return {
|
|
99
|
+
...change,
|
|
100
|
+
newVersion: lockEntry?.version ?? change.newVersion,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
packages: resolved.map(toPackageInput).filter((p) => p.name !== SELF_PACKAGE),
|
|
105
|
+
pythonPackages,
|
|
106
|
+
method: "fallback",
|
|
107
|
+
skipped: [],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// 3. Default: scan everything in the lockfile
|
|
113
|
+
const packages = [];
|
|
114
|
+
for (const [name, entry] of headParsed.packages) {
|
|
115
|
+
if (packages.length >= config.maxPackages)
|
|
116
|
+
break;
|
|
117
|
+
if (entry.optional && entry.hasPlatformRestriction)
|
|
118
|
+
continue;
|
|
119
|
+
if (name === SELF_PACKAGE)
|
|
120
|
+
continue;
|
|
121
|
+
packages.push({
|
|
122
|
+
name,
|
|
123
|
+
version: entry.version,
|
|
124
|
+
previousVersion: null,
|
|
125
|
+
isNew: true,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return { packages, pythonPackages, method: "scan-all", skipped: [] };
|
|
129
|
+
}
|
|
130
|
+
function findLockfile(cwd) {
|
|
131
|
+
const candidates = [
|
|
132
|
+
["package-lock.json", "npm"],
|
|
133
|
+
["npm-shrinkwrap.json", "npm"],
|
|
134
|
+
["yarn.lock", "yarn"],
|
|
135
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
136
|
+
];
|
|
137
|
+
for (const [name, type] of candidates) {
|
|
138
|
+
const p = (0, node_path_1.join)(cwd, name);
|
|
139
|
+
if ((0, node_fs_1.existsSync)(p))
|
|
140
|
+
return { path: p, type };
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
function parseLockfileByType(content, type) {
|
|
145
|
+
switch (type) {
|
|
146
|
+
case "npm": return (0, parse_package_lock_1.parseLockfile)(content);
|
|
147
|
+
case "yarn": return (0, parse_yarn_lock_1.parseYarnLock)(content);
|
|
148
|
+
case "pnpm": return (0, parse_pnpm_lock_1.parsePnpmLock)(content);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function getDirectDeps(cwd) {
|
|
152
|
+
try {
|
|
153
|
+
const content = readFileSafe((0, node_path_1.join)(cwd, "package.json"));
|
|
154
|
+
const pkg = JSON.parse(content);
|
|
155
|
+
return new Set([
|
|
156
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
157
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return new Set();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Resolve the default branch with a three-step fallback:
|
|
165
|
+
// 1. Ask git what origin's HEAD points at (works for main, master, develop, etc.)
|
|
166
|
+
// 2. Try master explicitly — some repos don't have origin/HEAD set
|
|
167
|
+
// 3. Last resort: main
|
|
168
|
+
function getDefaultBranch(cwd) {
|
|
169
|
+
try {
|
|
170
|
+
const ref = (0, node_child_process_1.execFileSync)("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
171
|
+
cwd,
|
|
172
|
+
encoding: "utf-8",
|
|
173
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
174
|
+
}).trim();
|
|
175
|
+
const branch = ref.replace(/^refs\/remotes\/origin\//, "");
|
|
176
|
+
if (branch)
|
|
177
|
+
return branch;
|
|
178
|
+
}
|
|
179
|
+
catch { /* fall through */ }
|
|
180
|
+
try {
|
|
181
|
+
(0, node_child_process_1.execFileSync)("git", ["rev-parse", "--verify", "refs/remotes/origin/master"], {
|
|
182
|
+
cwd,
|
|
183
|
+
encoding: "utf-8",
|
|
184
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
185
|
+
});
|
|
186
|
+
return "master";
|
|
187
|
+
}
|
|
188
|
+
catch { /* fall through */ }
|
|
189
|
+
return "main";
|
|
190
|
+
}
|
|
191
|
+
function getGitBaseLockfile(cwd) {
|
|
192
|
+
try {
|
|
193
|
+
const defaultBranch = getDefaultBranch(cwd);
|
|
194
|
+
const mergeBase = (0, node_child_process_1.execFileSync)("git", ["merge-base", "HEAD", defaultBranch], {
|
|
195
|
+
cwd,
|
|
196
|
+
encoding: "utf-8",
|
|
197
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
198
|
+
}).trim();
|
|
199
|
+
if (!mergeBase)
|
|
200
|
+
return null;
|
|
201
|
+
for (const name of ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"]) {
|
|
202
|
+
try {
|
|
203
|
+
return (0, node_child_process_1.execFileSync)("git", ["show", `${mergeBase}:${name}`], {
|
|
204
|
+
cwd,
|
|
205
|
+
encoding: "utf-8",
|
|
206
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function getGitBaseFile(cwd, filename) {
|
|
220
|
+
try {
|
|
221
|
+
const defaultBranch = getDefaultBranch(cwd);
|
|
222
|
+
const mergeBase = (0, node_child_process_1.execFileSync)("git", ["merge-base", "HEAD", defaultBranch], {
|
|
223
|
+
cwd,
|
|
224
|
+
encoding: "utf-8",
|
|
225
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
226
|
+
}).trim();
|
|
227
|
+
if (!mergeBase)
|
|
228
|
+
return null;
|
|
229
|
+
return (0, node_child_process_1.execFileSync)("git", ["show", `${mergeBase}:${filename}`], {
|
|
230
|
+
cwd,
|
|
231
|
+
encoding: "utf-8",
|
|
232
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function toPackageInput(change) {
|
|
240
|
+
return {
|
|
241
|
+
name: change.name,
|
|
242
|
+
version: change.newVersion,
|
|
243
|
+
previousVersion: change.oldVersion,
|
|
244
|
+
isNew: change.oldVersion === null,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function parsePythonDepFile(projectDir, depFile) {
|
|
248
|
+
const content = readFileSafe((0, node_path_1.join)(projectDir, depFile));
|
|
249
|
+
if (depFile === "Pipfile.lock") {
|
|
250
|
+
try {
|
|
251
|
+
const parsed = JSON.parse(content);
|
|
252
|
+
const packages = [];
|
|
253
|
+
for (const section of ["default", "develop"]) {
|
|
254
|
+
const deps = parsed[section];
|
|
255
|
+
if (!deps)
|
|
256
|
+
continue;
|
|
257
|
+
for (const [name, info] of Object.entries(deps)) {
|
|
258
|
+
const entry = info;
|
|
259
|
+
const version = entry?.version?.replace(/^==/, "") ?? "0.0.0";
|
|
260
|
+
packages.push({ name, version });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return packages;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (depFile === "poetry.lock") {
|
|
270
|
+
const packages = [];
|
|
271
|
+
const blocks = content.split(/^\[\[package\]\]/gm);
|
|
272
|
+
for (const block of blocks) {
|
|
273
|
+
const nameMatch = block.match(/^name\s*=\s*"([^"]+)"/m);
|
|
274
|
+
const versionMatch = block.match(/^version\s*=\s*"([^"]+)"/m);
|
|
275
|
+
if (nameMatch && versionMatch) {
|
|
276
|
+
packages.push({ name: nameMatch[1], version: versionMatch[1] });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return packages;
|
|
280
|
+
}
|
|
281
|
+
// requirements.txt
|
|
282
|
+
const packages = [];
|
|
283
|
+
for (const raw of content.split("\n")) {
|
|
284
|
+
const line = raw.trim();
|
|
285
|
+
if (!line || line.startsWith("#") || line.startsWith("-"))
|
|
286
|
+
continue;
|
|
287
|
+
// Strip environment markers (e.g. ; python_version >= "3.6")
|
|
288
|
+
const withoutMarker = line.split(";")[0].trim();
|
|
289
|
+
// Strip extras (e.g. package[extra1,extra2])
|
|
290
|
+
const withoutExtras = withoutMarker.replace(/\[.*?\]/, "");
|
|
291
|
+
const eqMatch = withoutExtras.match(/^([a-zA-Z0-9_.-]+)\s*==\s*([^\s,]+)/);
|
|
292
|
+
if (eqMatch) {
|
|
293
|
+
packages.push({ name: eqMatch[1], version: eqMatch[2] });
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
const nameMatch = withoutExtras.match(/^([a-zA-Z0-9_.-]+)/);
|
|
297
|
+
if (nameMatch) {
|
|
298
|
+
packages.push({ name: nameMatch[1], version: "latest" });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return packages;
|
|
303
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseNpmArgs = parseNpmArgs;
|
|
4
|
+
exports.parsePackageSpec = parsePackageSpec;
|
|
5
|
+
exports.resolveVersion = resolveVersion;
|
|
6
|
+
exports.resolvePackages = resolvePackages;
|
|
7
|
+
exports.runNpm = runNpm;
|
|
8
|
+
exports.readBareInstallPackages = readBareInstallPackages;
|
|
9
|
+
exports.handleWrapCommand = handleWrapCommand;
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
const node_fs_1 = require("node:fs");
|
|
12
|
+
const node_path_1 = require("node:path");
|
|
13
|
+
/** npm commands that install packages and should trigger a scan */
|
|
14
|
+
const INSTALL_COMMANDS = new Set(["install", "i", "add", "update", "up"]);
|
|
15
|
+
/**
|
|
16
|
+
* Parse the argv after `dg npm ...` to extract the npm command and package specifiers.
|
|
17
|
+
*/
|
|
18
|
+
function parseNpmArgs(args) {
|
|
19
|
+
let dgForce = false;
|
|
20
|
+
const filtered = [];
|
|
21
|
+
// Extract dg-specific flags before passing to npm
|
|
22
|
+
for (const arg of args) {
|
|
23
|
+
if (arg === "--dg-force") {
|
|
24
|
+
dgForce = true;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
filtered.push(arg);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const command = filtered[0] ?? "";
|
|
31
|
+
const shouldScan = INSTALL_COMMANDS.has(command);
|
|
32
|
+
// Extract package specifiers: anything that's not a flag and not the command itself
|
|
33
|
+
const packages = [];
|
|
34
|
+
if (shouldScan) {
|
|
35
|
+
for (let i = 1; i < filtered.length; i++) {
|
|
36
|
+
const arg = filtered[i];
|
|
37
|
+
// Skip flags and their values
|
|
38
|
+
if (arg.startsWith("-")) {
|
|
39
|
+
// Flags that take a value: skip next arg too
|
|
40
|
+
if (flagTakesValue(arg)) {
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
packages.push(arg);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
command,
|
|
50
|
+
packages,
|
|
51
|
+
rawArgs: filtered,
|
|
52
|
+
dgForce,
|
|
53
|
+
shouldScan,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** npm flags that consume the next argument as a value */
|
|
57
|
+
function flagTakesValue(flag) {
|
|
58
|
+
const valueFlagPrefixes = [
|
|
59
|
+
"--save-prefix",
|
|
60
|
+
"--tag",
|
|
61
|
+
"--registry",
|
|
62
|
+
"--cache",
|
|
63
|
+
"--prefix",
|
|
64
|
+
"--fund",
|
|
65
|
+
"--omit",
|
|
66
|
+
"--install-strategy",
|
|
67
|
+
"--workspace",
|
|
68
|
+
];
|
|
69
|
+
// Short flags that take values
|
|
70
|
+
if (flag === "-w")
|
|
71
|
+
return true;
|
|
72
|
+
for (const prefix of valueFlagPrefixes) {
|
|
73
|
+
if (flag === prefix)
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
// --flag=value style doesn't consume next arg
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Parse a package specifier like "express", "@scope/pkg@^2.0.0", "pkg@latest"
|
|
81
|
+
* into { name, versionSpec }.
|
|
82
|
+
*/
|
|
83
|
+
function parsePackageSpec(spec) {
|
|
84
|
+
// Scoped: @scope/pkg@version
|
|
85
|
+
if (spec.startsWith("@")) {
|
|
86
|
+
const slashIdx = spec.indexOf("/");
|
|
87
|
+
if (slashIdx === -1) {
|
|
88
|
+
return { name: spec, versionSpec: null };
|
|
89
|
+
}
|
|
90
|
+
const afterSlash = spec.slice(slashIdx + 1);
|
|
91
|
+
const atIdx = afterSlash.indexOf("@");
|
|
92
|
+
if (atIdx === -1) {
|
|
93
|
+
return { name: spec, versionSpec: null };
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
name: spec.slice(0, slashIdx + 1 + atIdx),
|
|
97
|
+
versionSpec: afterSlash.slice(atIdx + 1),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// Unscoped: pkg@version
|
|
101
|
+
const atIdx = spec.indexOf("@");
|
|
102
|
+
if (atIdx === -1 || atIdx === 0) {
|
|
103
|
+
return { name: spec, versionSpec: null };
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
name: spec.slice(0, atIdx),
|
|
107
|
+
versionSpec: spec.slice(atIdx + 1),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Resolve what version npm would install for a given package specifier.
|
|
112
|
+
* Uses `npm view <spec> version` to get the resolved version.
|
|
113
|
+
*/
|
|
114
|
+
async function resolveVersion(spec) {
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
const child = (0, node_child_process_1.spawn)("npm", ["view", spec, "version"], {
|
|
117
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
118
|
+
});
|
|
119
|
+
let stdout = "";
|
|
120
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
|
|
121
|
+
const timer = setTimeout(() => { child.kill(); resolve(null); }, 15000);
|
|
122
|
+
child.on("close", (code) => {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
if (code !== 0) {
|
|
125
|
+
resolve(null);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const version = stdout.trim();
|
|
129
|
+
resolve(version || null);
|
|
130
|
+
});
|
|
131
|
+
child.on("error", () => { clearTimeout(timer); resolve(null); });
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Build PackageInput[] from package specifiers by resolving versions in parallel.
|
|
136
|
+
*/
|
|
137
|
+
async function resolvePackages(specs) {
|
|
138
|
+
const results = await Promise.allSettled(specs.map(async (spec) => {
|
|
139
|
+
const { name, versionSpec } = parsePackageSpec(spec);
|
|
140
|
+
const querySpec = versionSpec ? `${name}@${versionSpec}` : name;
|
|
141
|
+
const version = await resolveVersion(querySpec);
|
|
142
|
+
return { spec, name, version };
|
|
143
|
+
}));
|
|
144
|
+
const resolved = [];
|
|
145
|
+
const failed = [];
|
|
146
|
+
for (const result of results) {
|
|
147
|
+
if (result.status === "rejected") {
|
|
148
|
+
failed.push("unknown");
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const { spec, name, version } = result.value;
|
|
152
|
+
if (version) {
|
|
153
|
+
resolved.push({
|
|
154
|
+
name,
|
|
155
|
+
version,
|
|
156
|
+
previousVersion: null,
|
|
157
|
+
isNew: true,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
failed.push(spec);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { resolved, failed };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Run the actual npm command, inheriting stdio.
|
|
168
|
+
* Returns the npm exit code.
|
|
169
|
+
*/
|
|
170
|
+
function runNpm(args) {
|
|
171
|
+
return new Promise((resolve) => {
|
|
172
|
+
const child = (0, node_child_process_1.spawn)("npm", args, {
|
|
173
|
+
stdio: "inherit",
|
|
174
|
+
shell: false,
|
|
175
|
+
});
|
|
176
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
177
|
+
child.on("error", () => resolve(1));
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Read package.json dependencies for bare `npm install` scanning.
|
|
182
|
+
*/
|
|
183
|
+
function readBareInstallPackages(cwd) {
|
|
184
|
+
const pkgPath = (0, node_path_1.join)(cwd, "package.json");
|
|
185
|
+
if (!(0, node_fs_1.existsSync)(pkgPath))
|
|
186
|
+
return [];
|
|
187
|
+
try {
|
|
188
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)(pkgPath, "utf-8"));
|
|
189
|
+
const specs = [];
|
|
190
|
+
for (const [name, range] of Object.entries(pkg.dependencies ?? {})) {
|
|
191
|
+
if (typeof range === "string")
|
|
192
|
+
specs.push(`${name}@${range}`);
|
|
193
|
+
}
|
|
194
|
+
for (const [name, range] of Object.entries(pkg.devDependencies ?? {})) {
|
|
195
|
+
if (typeof range === "string")
|
|
196
|
+
specs.push(`${name}@${range}`);
|
|
197
|
+
}
|
|
198
|
+
return specs;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const WRAP_USAGE = `
|
|
205
|
+
Set up dg as your npm wrapper:
|
|
206
|
+
|
|
207
|
+
Option 1 — Shell alias (recommended):
|
|
208
|
+
Add to your ~/.zshrc or ~/.bashrc:
|
|
209
|
+
alias npm='dg npm'
|
|
210
|
+
|
|
211
|
+
Option 2 — Per-project .npmrc:
|
|
212
|
+
Not yet supported.
|
|
213
|
+
|
|
214
|
+
Once set up, every \`npm install\` will be scanned automatically.
|
|
215
|
+
`.trimStart();
|
|
216
|
+
function handleWrapCommand() {
|
|
217
|
+
process.stdout.write(WRAP_USAGE);
|
|
218
|
+
}
|