@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.
Files changed (64) hide show
  1. package/README.md +5 -1
  2. package/dist/index.mjs +249 -114
  3. package/dist/packages/cli/src/alt-screen.js +36 -0
  4. package/dist/packages/cli/src/api.js +322 -0
  5. package/dist/packages/cli/src/auth.js +218 -0
  6. package/dist/packages/cli/src/bin.js +386 -0
  7. package/dist/packages/cli/src/config.js +228 -0
  8. package/dist/packages/cli/src/discover.js +126 -0
  9. package/dist/packages/cli/src/first-run.js +135 -0
  10. package/dist/packages/cli/src/hook.js +360 -0
  11. package/dist/packages/cli/src/lockfile.js +303 -0
  12. package/dist/packages/cli/src/npm-wrapper.js +218 -0
  13. package/dist/packages/cli/src/pip-wrapper.js +273 -0
  14. package/dist/packages/cli/src/sanitize.js +38 -0
  15. package/dist/packages/cli/src/scan-core.js +144 -0
  16. package/dist/packages/cli/src/setup-status.js +46 -0
  17. package/dist/packages/cli/src/static-output.js +625 -0
  18. package/dist/packages/cli/src/telemetry.js +141 -0
  19. package/dist/packages/cli/src/ui/App.js +137 -0
  20. package/dist/packages/cli/src/ui/InitApp.js +391 -0
  21. package/dist/packages/cli/src/ui/LoginApp.js +51 -0
  22. package/dist/packages/cli/src/ui/NpmWrapperApp.js +73 -0
  23. package/dist/packages/cli/src/ui/PipWrapperApp.js +72 -0
  24. package/dist/packages/cli/src/ui/components/ConfirmPrompt.js +24 -0
  25. package/dist/packages/cli/src/ui/components/DemoScanAnimation.js +26 -0
  26. package/dist/packages/cli/src/ui/components/DurationLine.js +7 -0
  27. package/dist/packages/cli/src/ui/components/ErrorView.js +30 -0
  28. package/dist/packages/cli/src/ui/components/FileSavePrompt.js +210 -0
  29. package/dist/packages/cli/src/ui/components/InteractiveResultsView.js +557 -0
  30. package/dist/packages/cli/src/ui/components/Mascot.js +33 -0
  31. package/dist/packages/cli/src/ui/components/ProgressBar.js +51 -0
  32. package/dist/packages/cli/src/ui/components/ProgressDots.js +35 -0
  33. package/dist/packages/cli/src/ui/components/ProjectSelector.js +60 -0
  34. package/dist/packages/cli/src/ui/components/ResultsView.js +105 -0
  35. package/dist/packages/cli/src/ui/components/ScanResultCard.js +54 -0
  36. package/dist/packages/cli/src/ui/components/ScoreHeader.js +142 -0
  37. package/dist/packages/cli/src/ui/components/SetupBanner.js +17 -0
  38. package/dist/packages/cli/src/ui/components/Spinner.js +11 -0
  39. package/dist/packages/cli/src/ui/hooks/useExpandAnimation.js +44 -0
  40. package/dist/packages/cli/src/ui/hooks/useInit.js +341 -0
  41. package/dist/packages/cli/src/ui/hooks/useLogin.js +121 -0
  42. package/dist/packages/cli/src/ui/hooks/useNpmWrapper.js +192 -0
  43. package/dist/packages/cli/src/ui/hooks/usePipWrapper.js +195 -0
  44. package/dist/packages/cli/src/ui/hooks/useScan.js +202 -0
  45. package/dist/packages/cli/src/ui/hooks/useTerminalSize.js +29 -0
  46. package/dist/packages/cli/src/update-check.js +152 -0
  47. package/dist/packages/cli/src/wizard-demo-data.js +63 -0
  48. package/dist/src/ecosystem.js +2 -0
  49. package/dist/src/lockfile/diff.js +38 -0
  50. package/dist/src/lockfile/parse_package_json.js +41 -0
  51. package/dist/src/lockfile/parse_package_lock.js +55 -0
  52. package/dist/src/lockfile/parse_pipfile_lock.js +69 -0
  53. package/dist/src/lockfile/parse_pnpm_lock.js +62 -0
  54. package/dist/src/lockfile/parse_poetry_lock.js +71 -0
  55. package/dist/src/lockfile/parse_requirements.js +83 -0
  56. package/dist/src/lockfile/parse_yarn_lock.js +66 -0
  57. package/dist/src/logger.js +21 -0
  58. package/dist/src/npm/h2pool.js +161 -0
  59. package/dist/src/npm/registry.js +299 -0
  60. package/dist/src/npm/tarball.js +274 -0
  61. package/dist/src/pypi/registry.js +299 -0
  62. package/dist/src/pypi/tarball.js +361 -0
  63. package/dist/src/types.js +2 -0
  64. 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
+ }