@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,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
|
+
}
|