@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,386 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const telemetry_1 = require("./telemetry");
38
+ const config_1 = require("./config");
39
+ const npm_wrapper_1 = require("./npm-wrapper");
40
+ const update_check_1 = require("./update-check");
41
+ const CLI_VERSION = (0, config_1.getVersion)();
42
+ (0, telemetry_1.initTelemetry)(CLI_VERSION);
43
+ // tiny Levenshtein for "did you mean" — closest match within distance 3
44
+ function closestCommand(input, commands) {
45
+ let best = "", bestDist = Infinity;
46
+ for (const cmd of commands) {
47
+ const m = input.length, n = cmd.length;
48
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1));
49
+ for (let i = 0; i <= m; i++)
50
+ dp[i][0] = i;
51
+ for (let j = 0; j <= n; j++)
52
+ dp[0][j] = j;
53
+ for (let i = 1; i <= m; i++)
54
+ for (let j = 1; j <= n; j++)
55
+ dp[i][j] = input[i - 1] === cmd[j - 1]
56
+ ? dp[i - 1][j - 1]
57
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
58
+ if (dp[m][n] < bestDist) {
59
+ bestDist = dp[m][n];
60
+ best = cmd;
61
+ }
62
+ }
63
+ return bestDist <= 3 ? best : null;
64
+ }
65
+ const alt_screen_1 = require("./alt-screen");
66
+ process.on("SIGINT", () => {
67
+ (0, alt_screen_1.leaveAltScreen)();
68
+ process.stderr.write("\n");
69
+ process.exit(130);
70
+ });
71
+ process.on("SIGTERM", () => {
72
+ (0, alt_screen_1.leaveAltScreen)();
73
+ process.exit(143);
74
+ });
75
+ const isInteractive = process.stdout.isTTY === true &&
76
+ !process.env.CI &&
77
+ !process.env.NO_COLOR;
78
+ async function main() {
79
+ const rawCommand = process.argv[2];
80
+ // Help and version are positional commands first, with the conventional
81
+ // double-dash forms preserved as aliases since users WILL type --help.
82
+ if (!rawCommand ||
83
+ rawCommand === "help" ||
84
+ rawCommand === "--help" ||
85
+ rawCommand === "-h") {
86
+ const { USAGE } = await Promise.resolve().then(() => __importStar(require("./config")));
87
+ process.stdout.write(USAGE);
88
+ return;
89
+ }
90
+ if (rawCommand === "version" ||
91
+ rawCommand === "--version" ||
92
+ rawCommand === "-v") {
93
+ process.stdout.write(`dependency-guardian v${CLI_VERSION}\n`);
94
+ return;
95
+ }
96
+ // Reject unknown commands BEFORE the first-run wizard check. If the user
97
+ // made a typo they should see the correction immediately, not walk through
98
+ // a guided tour and THEN get told their command doesn't exist.
99
+ const KNOWN_COMMANDS = ["scan", "npm", "pip", "wrap", "login", "logout", "status", "hook", "update", "kitty", "help", "version"];
100
+ if (rawCommand && !rawCommand.startsWith("-") && !KNOWN_COMMANDS.includes(rawCommand)) {
101
+ const chalk = (await Promise.resolve().then(() => __importStar(require("chalk")))).default;
102
+ const best = closestCommand(rawCommand, KNOWN_COMMANDS);
103
+ const hint = best ? ` Did you mean '${best}'?` : "";
104
+ process.stderr.write(`\n ${chalk.bold.red("Error:")} Unknown command '${rawCommand}'.${hint}\n`);
105
+ process.stderr.write(chalk.dim(` Run 'dg --help' for available commands.\n\n`));
106
+ process.exit(1);
107
+ }
108
+ // `dg kitty` — manually re-run the guided tour. The user can run this any
109
+ // time they want the cat back; it's the documented way to refresh on the
110
+ // commands or rerun setup after they've changed projects.
111
+ if (rawCommand === "kitty") {
112
+ if (isInteractive) {
113
+ const { render } = await Promise.resolve().then(() => __importStar(require("ink")));
114
+ const React = await Promise.resolve().then(() => __importStar(require("react")));
115
+ const { InitApp } = await Promise.resolve().then(() => __importStar(require("./ui/InitApp")));
116
+ (0, alt_screen_1.enterAltScreen)();
117
+ try {
118
+ const { waitUntilExit } = render(React.createElement(InitApp, { firstRun: true, returning: true }));
119
+ await waitUntilExit();
120
+ }
121
+ finally {
122
+ (0, alt_screen_1.leaveAltScreen)();
123
+ }
124
+ // Mark the sentinel so the auto-wizard doesn't fire on the next
125
+ // command if the user happened to run kitty before anything else.
126
+ try {
127
+ const { markFirstRunComplete } = await Promise.resolve().then(() => __importStar(require("./auth")));
128
+ markFirstRunComplete();
129
+ }
130
+ catch { /* best-effort */ }
131
+ }
132
+ else {
133
+ const chalk = (await Promise.resolve().then(() => __importStar(require("chalk")))).default;
134
+ process.stderr.write(chalk.yellow(" dg kitty needs an interactive terminal.\n"));
135
+ }
136
+ return;
137
+ }
138
+ // First-run guided tour. On a fresh machine running any in-scope command,
139
+ // mounts the InitApp wizard at the `greet` phase and walks the user through
140
+ // setup before their original command runs. The helper owns the skip-list
141
+ // (update / logout / kitty / help / version / hook) and the TTY check;
142
+ // it's a no-op if the user has already seen the wizard or if we're in CI.
143
+ const { maybeOfferFirstRunWizard } = await Promise.resolve().then(() => __importStar(require("./first-run")));
144
+ await maybeOfferFirstRunWizard({ rawCommand, isInteractive });
145
+ if (rawCommand === "wrap") {
146
+ (0, npm_wrapper_1.handleWrapCommand)();
147
+ return;
148
+ }
149
+ if (rawCommand === "login") {
150
+ if (isInteractive) {
151
+ const { render } = await Promise.resolve().then(() => __importStar(require("ink")));
152
+ const React = await Promise.resolve().then(() => __importStar(require("react")));
153
+ const { LoginApp } = await Promise.resolve().then(() => __importStar(require("./ui/LoginApp")));
154
+ const { waitUntilExit } = render(React.createElement(LoginApp));
155
+ await waitUntilExit();
156
+ }
157
+ else {
158
+ const { runStaticLogin } = await Promise.resolve().then(() => __importStar(require("./static-output")));
159
+ await runStaticLogin();
160
+ }
161
+ return;
162
+ }
163
+ if (rawCommand === "status") {
164
+ const { getStoredApiKey } = await Promise.resolve().then(() => __importStar(require("./auth")));
165
+ const chalk = (await Promise.resolve().then(() => __importStar(require("chalk")))).default;
166
+ const apiKey = getStoredApiKey();
167
+ if (!apiKey) {
168
+ process.stderr.write(chalk.yellow(` Not authenticated.`) + chalk.dim(` Run \`dg login\` to sign in.\n`));
169
+ return;
170
+ }
171
+ process.stderr.write(chalk.green(` Authenticated\n`));
172
+ try {
173
+ const { parseConfig } = await Promise.resolve().then(() => __importStar(require("./config")));
174
+ const config = parseConfig(process.argv, false);
175
+ const resp = await globalThis.fetch(`${config.apiUrl}/v1/auth/status`, {
176
+ headers: { "Authorization": `Bearer ${apiKey}` },
177
+ });
178
+ if (resp.ok) {
179
+ const data = await resp.json();
180
+ process.stderr.write(` Tier: ${chalk.bold(data.tier)} \u2502 Scans: ${data.scansUsed}/${data.scansLimit} this month\n`);
181
+ }
182
+ else if (resp.status === 401) {
183
+ process.stderr.write(chalk.yellow(` Key invalid or expired. Run \`dg logout\` then \`dg login\`.\n`));
184
+ }
185
+ }
186
+ catch {
187
+ process.stderr.write(chalk.dim(` Could not reach API.\n`));
188
+ }
189
+ return;
190
+ }
191
+ if (rawCommand === "hook") {
192
+ const { handleHookCommand } = await Promise.resolve().then(() => __importStar(require("./hook")));
193
+ handleHookCommand(process.argv.slice(3));
194
+ return;
195
+ }
196
+ if (rawCommand === "update") {
197
+ await (0, update_check_1.runUpdate)(CLI_VERSION);
198
+ return;
199
+ }
200
+ if (rawCommand === "logout") {
201
+ const { getStoredApiKey, clearCredentials } = await Promise.resolve().then(() => __importStar(require("./auth")));
202
+ const chalk = (await Promise.resolve().then(() => __importStar(require("chalk")))).default;
203
+ const apiKey = getStoredApiKey();
204
+ if (apiKey) {
205
+ // Revoke the key server-side before deleting locally. Honor --api-url / DG_API_URL
206
+ // so self-hosted users don't leak test keys to production.
207
+ const { parseConfig } = await Promise.resolve().then(() => __importStar(require("./config")));
208
+ const config = parseConfig(process.argv, false);
209
+ try {
210
+ await fetch(`${config.apiUrl}/v1/auth/revoke`, {
211
+ method: "POST",
212
+ headers: { Authorization: `Bearer ${apiKey}` },
213
+ signal: AbortSignal.timeout(5000),
214
+ });
215
+ }
216
+ catch { /* ignore — best-effort, still clear locally */ }
217
+ }
218
+ clearCredentials();
219
+ process.stderr.write(chalk.green(" Logged out.\n"));
220
+ return;
221
+ }
222
+ const strictFlags = rawCommand !== "npm" && rawCommand !== "pip";
223
+ const config = (0, config_1.parseConfig)(process.argv, strictFlags);
224
+ const updatePromise = (0, update_check_1.checkForUpdate)(CLI_VERSION).catch(() => null);
225
+ // Scan command: interactive TUI in TTY, static output for CI/pipes/--json
226
+ if (rawCommand !== "npm" && rawCommand !== "pip") {
227
+ if (config.json || !isInteractive) {
228
+ const { runStatic } = await Promise.resolve().then(() => __importStar(require("./static-output")));
229
+ await runStatic(config);
230
+ }
231
+ else {
232
+ if (config.mode === "off") {
233
+ const chalk = (await Promise.resolve().then(() => __importStar(require("chalk")))).default;
234
+ process.stderr.write(chalk.dim(" Dependency Guardian: mode is off — skipping.\n"));
235
+ process.exit(0);
236
+ }
237
+ const { getStoredApiKey, maskKey } = await Promise.resolve().then(() => __importStar(require("./auth")));
238
+ const apiKey = getStoredApiKey();
239
+ let userStatus = "not signed in \u00b7 dg login";
240
+ let scanUsage = "free tier";
241
+ if (apiKey) {
242
+ try {
243
+ const resp = await globalThis.fetch(`${config.apiUrl}/v1/auth/status`, {
244
+ headers: { "Authorization": `Bearer ${apiKey}` },
245
+ signal: AbortSignal.timeout(3000),
246
+ });
247
+ if (resp.ok) {
248
+ const chalk = (await Promise.resolve().then(() => __importStar(require("chalk")))).default;
249
+ const data = await resp.json();
250
+ const name = data.name || "authenticated";
251
+ const tier = data.tier || "free";
252
+ const tierColor = tier === "free" ? chalk.yellow(tier) : chalk.green(tier);
253
+ userStatus = `${name} \u00b7 ${tierColor}`;
254
+ if (data.scansLimit === null || data.scansLimit === undefined) {
255
+ scanUsage = "unlimited scans";
256
+ }
257
+ else {
258
+ const used = data.scansUsed ?? 0;
259
+ const limit = data.scansLimit;
260
+ const remaining = limit - used;
261
+ scanUsage = `${remaining}/${limit} scans left`;
262
+ }
263
+ }
264
+ else {
265
+ userStatus = "key invalid \u00b7 dg login";
266
+ }
267
+ }
268
+ catch {
269
+ userStatus = maskKey(apiKey);
270
+ }
271
+ }
272
+ // Compute setup-incomplete state so the scan UI can surface a banner.
273
+ // Cheap (one fs check + one credential read) — fine to run every scan.
274
+ const { getSetupIssues } = await Promise.resolve().then(() => __importStar(require("./setup-status")));
275
+ const setupIssues = getSetupIssues(process.cwd());
276
+ const { render } = await Promise.resolve().then(() => __importStar(require("ink")));
277
+ const React = await Promise.resolve().then(() => __importStar(require("react")));
278
+ const { App } = await Promise.resolve().then(() => __importStar(require("./ui/App")));
279
+ const { waitUntilExit } = render(React.createElement(App, { config, userStatus, scanUsage, setupIssues }));
280
+ await waitUntilExit();
281
+ }
282
+ const updateMsg = await updatePromise;
283
+ if (updateMsg) {
284
+ const chalk = (await Promise.resolve().then(() => __importStar(require("chalk")))).default;
285
+ process.stderr.write(chalk.dim(updateMsg));
286
+ }
287
+ return;
288
+ }
289
+ // npm/pip wrappers: use Ink for spinners when TTY, static otherwise
290
+ if (config.json || !isInteractive) {
291
+ if (rawCommand === "npm") {
292
+ const { runStaticNpm } = await Promise.resolve().then(() => __importStar(require("./static-output")));
293
+ await runStaticNpm(process.argv.slice(3), config);
294
+ }
295
+ else {
296
+ const { runStaticPip } = await Promise.resolve().then(() => __importStar(require("./static-output")));
297
+ await runStaticPip(process.argv.slice(3), config);
298
+ }
299
+ const updateMsg = await updatePromise;
300
+ if (updateMsg) {
301
+ const chalk = (await Promise.resolve().then(() => __importStar(require("chalk")))).default;
302
+ process.stderr.write(chalk.dim(updateMsg));
303
+ }
304
+ return;
305
+ }
306
+ const { render } = await Promise.resolve().then(() => __importStar(require("ink")));
307
+ const React = await Promise.resolve().then(() => __importStar(require("react")));
308
+ if (rawCommand === "npm") {
309
+ const { NpmWrapperApp } = await Promise.resolve().then(() => __importStar(require("./ui/NpmWrapperApp")));
310
+ const { waitUntilExit } = render(React.createElement(NpmWrapperApp, { config, npmArgs: process.argv.slice(3) }));
311
+ await waitUntilExit();
312
+ }
313
+ else {
314
+ const { PipWrapperApp } = await Promise.resolve().then(() => __importStar(require("./ui/PipWrapperApp")));
315
+ const { waitUntilExit } = render(React.createElement(PipWrapperApp, { config, pipArgs: process.argv.slice(3) }));
316
+ await waitUntilExit();
317
+ }
318
+ const updateMsg = await updatePromise;
319
+ if (updateMsg) {
320
+ const chalk = (await Promise.resolve().then(() => __importStar(require("chalk")))).default;
321
+ process.stderr.write(chalk.dim(updateMsg));
322
+ }
323
+ }
324
+ main().catch(async (err) => {
325
+ // Report to Sentry before exiting
326
+ let isAuthed = false;
327
+ try {
328
+ const { getStoredApiKey } = await Promise.resolve().then(() => __importStar(require("./auth")));
329
+ isAuthed = !!getStoredApiKey();
330
+ }
331
+ catch { /* ignore — best-effort */ }
332
+ // Version-floor errors from the server are expected operator behavior, not crashes —
333
+ // tag them so the alerting rule can filter them out.
334
+ const { ClientOutdatedError } = await Promise.resolve().then(() => __importStar(require("./api")));
335
+ if (err instanceof ClientOutdatedError) {
336
+ (0, telemetry_1.captureError)(err, {
337
+ command: process.argv[2] || "unknown",
338
+ nodeVersion: process.version,
339
+ cliVersion: CLI_VERSION,
340
+ authenticated: isAuthed,
341
+ });
342
+ await (0, telemetry_1.flush)();
343
+ const chalk = (await Promise.resolve().then(() => __importStar(require("chalk")))).default;
344
+ const color = err.securityRelease ? chalk.bold.red : chalk.yellow;
345
+ if (process.argv.includes("--json")) {
346
+ process.stdout.write(JSON.stringify({
347
+ error: true,
348
+ code: "client_outdated",
349
+ message: err.message,
350
+ minVersion: err.minVersion,
351
+ currentVersion: err.currentVersion,
352
+ securityRelease: err.securityRelease,
353
+ recovery: "Run `dg update` to install the required version.",
354
+ }, null, 2) + "\n");
355
+ }
356
+ else {
357
+ process.stderr.write(`\n ${color(" Update required:")} ${err.message}\n` +
358
+ ` Run \`dg update\` to install ${err.minVersion} or newer.\n\n`);
359
+ }
360
+ process.exit(3);
361
+ }
362
+ (0, telemetry_1.captureError)(err, {
363
+ command: process.argv[2] || "unknown",
364
+ nodeVersion: process.version,
365
+ cliVersion: CLI_VERSION,
366
+ authenticated: isAuthed,
367
+ });
368
+ await (0, telemetry_1.flush)();
369
+ if (process.argv.includes("--json")) {
370
+ process.stdout.write(JSON.stringify({
371
+ error: true,
372
+ code: err.statusCode ? "api_error" : "internal_error",
373
+ message: err.message,
374
+ }, null, 2) + "\n");
375
+ }
376
+ else {
377
+ try {
378
+ const chalkMod = await Promise.resolve().then(() => __importStar(require("chalk")));
379
+ process.stderr.write(`\n ${chalkMod.default.bold.red("Error:")} ${err.message}\n\n`);
380
+ }
381
+ catch {
382
+ process.stderr.write(`\n Error: ${err.message}\n\n`);
383
+ }
384
+ }
385
+ process.exit(3);
386
+ });
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.USAGE = void 0;
4
+ exports.parseConfig = parseConfig;
5
+ exports.getVersion = getVersion;
6
+ const node_util_1 = require("node:util");
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
9
+ const node_url_1 = require("node:url");
10
+ const node_os_1 = require("node:os");
11
+ const auth_1 = require("./auth");
12
+ function loadDgrc() {
13
+ // Load project-level config first, then home-level.
14
+ // SECURITY: apiKey and apiUrl are ONLY read from ~/ to prevent a malicious
15
+ // repo's .dgrc.json from redirecting credentials to an attacker's server.
16
+ const cwdPath = (0, node_path_1.join)(process.cwd(), ".dgrc.json");
17
+ const homePath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".dgrc.json");
18
+ let config = {};
19
+ // Home config (trusted — may contain apiKey, apiUrl)
20
+ if ((0, node_fs_1.existsSync)(homePath)) {
21
+ try {
22
+ config = JSON.parse((0, node_fs_1.readFileSync)(homePath, "utf-8"));
23
+ }
24
+ catch {
25
+ process.stderr.write(`Warning: Failed to parse ${homePath}, ignoring.\n`);
26
+ }
27
+ }
28
+ // CWD config (untrusted — project settings only, no secrets)
29
+ if ((0, node_fs_1.existsSync)(cwdPath) && cwdPath !== homePath) {
30
+ try {
31
+ const cwd = JSON.parse((0, node_fs_1.readFileSync)(cwdPath, "utf-8"));
32
+ // Only merge non-sensitive keys from CWD config
33
+ const { apiKey: _k, apiUrl: _u, ...safeKeys } = cwd;
34
+ Object.assign(config, safeKeys);
35
+ }
36
+ catch {
37
+ process.stderr.write(`Warning: Failed to parse ${cwdPath}, ignoring.\n`);
38
+ }
39
+ }
40
+ return config;
41
+ }
42
+ const USAGE = `
43
+ Dependency Guardian — Supply chain security scanner
44
+
45
+ Usage:
46
+ dg scan [options]
47
+ dg npm install <pkg> [npm-flags]
48
+ dg pip install <pkg> [pip-flags]
49
+
50
+ Commands:
51
+ scan Scan dependencies (auto-discovers npm + Python projects)
52
+ npm Wrap npm commands — scans packages before installing
53
+ pip Wrap pip commands — scans packages before installing
54
+ hook install Install git pre-commit hook to scan lockfile changes
55
+ hook uninstall Remove the pre-commit hook
56
+ login Authenticate with your WestBayBerry account
57
+ logout Remove saved credentials
58
+ status Show auth + scan usage
59
+ update Check for and install the latest version
60
+ wrap Show instructions to alias npm to dg
61
+ kitty Re-run the guided tour with the cat
62
+ help Show this help message
63
+ version Show version number
64
+
65
+ First-run setup runs automatically the first time you invoke any
66
+ command on a fresh machine — no need to remember a setup command.
67
+ Forgot something later? Run \`dg kitty\` for the tour again.
68
+
69
+ Options:
70
+ --api-url <url> API base URL (default: https://api.westbayberry.com)
71
+ --mode <mode> block | warn | off (default: warn)
72
+ --max-packages <n> Max packages per scan (default: 10000)
73
+ --json Output JSON for CI parsing
74
+ --scan-all Scan all packages (default)
75
+ --changed-only Only scan packages changed since base lockfile
76
+ --base-lockfile <path> Path to base lockfile for explicit diff
77
+ --workspace <dir> Scan a specific workspace subdirectory
78
+ --output, -o <file> Save JSON results to file (use with --json)
79
+ --debug Show diagnostic output (discovery, batches, timing)
80
+
81
+ Config File:
82
+ Place a .dgrc.json in your project root or home directory.
83
+ Precedence: CLI flags > env vars > .dgrc.json > defaults
84
+
85
+ Environment Variables:
86
+ DG_API_URL API base URL
87
+ DG_MODE Mode (block/warn/off)
88
+ DG_DEBUG Enable debug output (set to 1)
89
+ DG_WORKSPACE Workspace subdirectory to scan
90
+ DG_TELEMETRY=0 Disable anonymous error reporting
91
+
92
+ Exit Codes:
93
+ 0 pass — No risks detected
94
+ 1 warn — Risks detected (advisory)
95
+ 2 block — High-risk packages detected
96
+ 3 error — Internal error (API failure, config error)
97
+
98
+ Examples:
99
+ dg scan
100
+ dg scan --json
101
+ dg scan --scan-all --mode block
102
+ dg scan --base-lockfile ./main-lockfile.json
103
+ dg npm install express lodash
104
+ dg npm install @scope/pkg@^2.0.0
105
+ dg npm install risky-pkg --dg-force
106
+ `.trimStart();
107
+ exports.USAGE = USAGE;
108
+ function validateApiUrl(url) {
109
+ try {
110
+ const parsed = new URL(url);
111
+ // Node's URL returns IPv6 hosts in bracketed form, e.g. "[::1]" — strip the brackets
112
+ // before range-matching so we can compare against the bare address.
113
+ const rawHost = parsed.hostname;
114
+ const host = rawHost.startsWith("[") && rawHost.endsWith("]")
115
+ ? rawHost.slice(1, -1)
116
+ : rawHost;
117
+ const isLocal = host === "localhost" ||
118
+ // IPv4: loopback, RFC 1918, CGNAT. Proper octet regex — not prefix matching,
119
+ // which would wrongly accept `192.1680.0.1` or `100.1.2.3` (public).
120
+ /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.)/.test(host) ||
121
+ // IPv6: loopback and unique local (fc00::/7 → fc** or fd**)
122
+ host === "::1" ||
123
+ /^f[cd][0-9a-f]{2}:/i.test(host);
124
+ if (parsed.protocol !== "https:" && !isLocal) {
125
+ process.stderr.write(`Error: API URL must use HTTPS (got ${parsed.protocol}). Use localhost for local testing.\n`);
126
+ process.exit(1);
127
+ }
128
+ return url;
129
+ }
130
+ catch {
131
+ process.stderr.write(`Error: Invalid API URL: ${url}\n`);
132
+ process.exit(1);
133
+ }
134
+ }
135
+ function getVersion() {
136
+ try {
137
+ const thisDir = (0, node_path_1.dirname)((0, node_url_1.fileURLToPath)(import.meta.url));
138
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(thisDir, "..", "package.json"), "utf-8"));
139
+ return pkg.version ?? "1.0.0";
140
+ }
141
+ catch {
142
+ return "1.0.0";
143
+ }
144
+ }
145
+ function parseConfig(argv, strictFlags = true) {
146
+ let values;
147
+ let positionals;
148
+ try {
149
+ const parsed = (0, node_util_1.parseArgs)({
150
+ args: argv.slice(2),
151
+ options: {
152
+ "api-url": { type: "string" },
153
+ mode: { type: "string" },
154
+ "max-packages": { type: "string" },
155
+ json: { type: "boolean", default: false },
156
+ "scan-all": { type: "boolean", default: true },
157
+ "changed-only": { type: "boolean", default: false },
158
+ "base-lockfile": { type: "string" },
159
+ workspace: { type: "string", short: "w" },
160
+ output: { type: "string", short: "o" },
161
+ debug: { type: "boolean", default: false },
162
+ help: { type: "boolean", default: false },
163
+ version: { type: "boolean", default: false },
164
+ // Recognized but handled server-side via policy dashboard
165
+ "no-config": { type: "boolean", default: false },
166
+ allowlist: { type: "string" },
167
+ "block-threshold": { type: "string" },
168
+ "warn-threshold": { type: "string" },
169
+ },
170
+ allowPositionals: true,
171
+ strict: strictFlags,
172
+ });
173
+ values = parsed.values;
174
+ positionals = parsed.positionals;
175
+ }
176
+ catch (err) {
177
+ const raw = err instanceof Error ? err.message : String(err);
178
+ // Node's parseArgs error is verbose — extract just "Unknown option '--foo'"
179
+ const match = raw.match(/Unknown option '([^']+)'/);
180
+ const msg = match ? `Unknown option '${match[1]}'.` : raw;
181
+ process.stderr.write(`\n Error: ${msg}\n`);
182
+ process.stderr.write(` Run 'dg --help' for available options.\n\n`);
183
+ process.exit(1);
184
+ }
185
+ if (values.help) {
186
+ process.stdout.write(USAGE);
187
+ process.exit(0);
188
+ }
189
+ if (values.version) {
190
+ process.stdout.write(`dependency-guardian v${getVersion()}\n`);
191
+ process.exit(0);
192
+ }
193
+ const command = positionals[0] ?? "scan";
194
+ const dgrc = loadDgrc();
195
+ const apiKey = dgrc.apiKey && typeof dgrc.apiKey === "string" && dgrc.apiKey.startsWith("dg_live_") ? dgrc.apiKey : null;
196
+ const deviceId = (0, auth_1.getOrCreateDeviceId)();
197
+ const modeRaw = values.mode ??
198
+ process.env.DG_MODE ??
199
+ dgrc.mode ??
200
+ "warn";
201
+ if (!["block", "warn", "off"].includes(modeRaw)) {
202
+ process.stderr.write(`Error: Invalid mode "${modeRaw}". Must be block, warn, or off.\n`);
203
+ process.exit(1);
204
+ }
205
+ const maxPackages = Number(values["max-packages"] ?? dgrc.maxPackages ?? "10000");
206
+ const debug = values.debug || process.env.DG_DEBUG === "1";
207
+ if (isNaN(maxPackages) || maxPackages < 1 || maxPackages > 10000) {
208
+ process.stderr.write("Error: --max-packages must be a number between 1 and 10000\n");
209
+ process.exit(1);
210
+ }
211
+ return {
212
+ apiKey,
213
+ deviceId,
214
+ apiUrl: validateApiUrl(values["api-url"] ??
215
+ process.env.DG_API_URL ??
216
+ dgrc.apiUrl ??
217
+ "https://api.westbayberry.com"),
218
+ mode: modeRaw,
219
+ maxPackages,
220
+ json: values.json,
221
+ scanAll: values["changed-only"] ? false : values["scan-all"],
222
+ baseLockfile: values["base-lockfile"] ?? null,
223
+ workspace: values.workspace ?? process.env.DG_WORKSPACE ?? null,
224
+ outputFile: values.output ?? null,
225
+ command,
226
+ debug,
227
+ };
228
+ }