@westbayberry/dg 1.0.52 → 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 +349 -168
- 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,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.usePipWrapper = usePipWrapper;
|
|
37
|
+
const react_1 = require("react");
|
|
38
|
+
const pip_wrapper_1 = require("../../pip-wrapper");
|
|
39
|
+
const api_1 = require("../../api");
|
|
40
|
+
const static_output_1 = require("../../static-output");
|
|
41
|
+
function reducer(_state, action) {
|
|
42
|
+
switch (action.type) {
|
|
43
|
+
case "PASSTHROUGH":
|
|
44
|
+
return { phase: "passthrough" };
|
|
45
|
+
case "RESOLVING":
|
|
46
|
+
return { phase: "resolving", count: action.count };
|
|
47
|
+
case "SCANNING":
|
|
48
|
+
return { phase: "scanning", count: action.count };
|
|
49
|
+
case "PASS":
|
|
50
|
+
return { phase: "pass", count: action.count };
|
|
51
|
+
case "WARN":
|
|
52
|
+
return { phase: "warn", result: action.result };
|
|
53
|
+
case "BLOCKED":
|
|
54
|
+
return { phase: "blocked", result: action.result, dgForce: action.dgForce };
|
|
55
|
+
case "INSTALLING":
|
|
56
|
+
return { phase: "installing" };
|
|
57
|
+
case "DONE":
|
|
58
|
+
return { phase: "done", exitCode: action.exitCode };
|
|
59
|
+
case "ERROR":
|
|
60
|
+
return { phase: "error", message: action.message, proceed: action.proceed };
|
|
61
|
+
case "TRIAL_EXHAUSTED":
|
|
62
|
+
return { phase: "trial_exhausted" };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function usePipWrapper(pipArgs, config, exit) {
|
|
66
|
+
const [state, dispatch] = (0, react_1.useReducer)(reducer, { phase: "resolving", count: 0 });
|
|
67
|
+
const started = (0, react_1.useRef)(false);
|
|
68
|
+
const parsedRef = (0, react_1.useRef)((0, pip_wrapper_1.parsePipArgs)(pipArgs));
|
|
69
|
+
const pendingInstall = (0, react_1.useRef)(null);
|
|
70
|
+
const rejectRef = (0, react_1.useRef)(null);
|
|
71
|
+
const confirmInstall = () => {
|
|
72
|
+
if (pendingInstall.current) {
|
|
73
|
+
pendingInstall.current();
|
|
74
|
+
pendingInstall.current = null;
|
|
75
|
+
rejectRef.current = null;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const rejectInstall = () => {
|
|
79
|
+
if (rejectRef.current) {
|
|
80
|
+
rejectRef.current();
|
|
81
|
+
pendingInstall.current = null;
|
|
82
|
+
rejectRef.current = null;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
(0, react_1.useEffect)(() => {
|
|
86
|
+
if (started.current)
|
|
87
|
+
return;
|
|
88
|
+
started.current = true;
|
|
89
|
+
const parsed = parsedRef.current;
|
|
90
|
+
(async () => {
|
|
91
|
+
try {
|
|
92
|
+
// Non-install commands: passthrough
|
|
93
|
+
if (!parsed.shouldScan) {
|
|
94
|
+
dispatch({ type: "PASSTHROUGH" });
|
|
95
|
+
const code = await (0, pip_wrapper_1.runPip)(parsed.rawArgs);
|
|
96
|
+
dispatch({ type: "DONE", exitCode: code });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Determine packages to resolve
|
|
100
|
+
let specs;
|
|
101
|
+
if (parsed.packages.length === 0 && parsed.requirementsFile) {
|
|
102
|
+
// -r requirements.txt
|
|
103
|
+
specs = (0, pip_wrapper_1.parseRequirementsFile)(parsed.requirementsFile);
|
|
104
|
+
if (specs.length === 0) {
|
|
105
|
+
dispatch({ type: "PASSTHROUGH" });
|
|
106
|
+
const code = await (0, pip_wrapper_1.runPip)(parsed.rawArgs);
|
|
107
|
+
dispatch({ type: "DONE", exitCode: code });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (parsed.packages.length === 0) {
|
|
112
|
+
// No packages specified and no requirements file — passthrough
|
|
113
|
+
dispatch({ type: "PASSTHROUGH" });
|
|
114
|
+
const code = await (0, pip_wrapper_1.runPip)(parsed.rawArgs);
|
|
115
|
+
dispatch({ type: "DONE", exitCode: code });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
specs = parsed.packages;
|
|
120
|
+
}
|
|
121
|
+
dispatch({ type: "RESOLVING", count: specs.length });
|
|
122
|
+
// Resolve versions via PyPI API
|
|
123
|
+
const { resolved } = await (0, pip_wrapper_1.resolvePackages)(specs);
|
|
124
|
+
if (resolved.length === 0) {
|
|
125
|
+
// Could not resolve any packages — passthrough
|
|
126
|
+
dispatch({ type: "PASSTHROUGH" });
|
|
127
|
+
const code = await (0, pip_wrapper_1.runPip)(parsed.rawArgs);
|
|
128
|
+
dispatch({ type: "DONE", exitCode: code });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
dispatch({ type: "SCANNING", count: resolved.length });
|
|
132
|
+
// Call PyPI analyze API
|
|
133
|
+
const result = await (0, api_1.callPyPIAnalyzeAPI)(resolved, config);
|
|
134
|
+
// Route based on action
|
|
135
|
+
if (result.action === "pass") {
|
|
136
|
+
if (exit)
|
|
137
|
+
exit();
|
|
138
|
+
const chalk = (await Promise.resolve().then(() => __importStar(require("chalk")))).default;
|
|
139
|
+
process.stderr.write(chalk.green(` \u2713 ${resolved.length} package${resolved.length !== 1 ? "s" : ""} scanned \u2014 all clear\n`));
|
|
140
|
+
process.stderr.write("\n");
|
|
141
|
+
const code = await (0, pip_wrapper_1.runPip)(parsed.rawArgs);
|
|
142
|
+
process.exit(code);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (result.action === "warn") {
|
|
146
|
+
if (exit)
|
|
147
|
+
exit();
|
|
148
|
+
process.stdout.write((0, static_output_1.renderResultStatic)(result, config) + "\n");
|
|
149
|
+
const chalk = (await Promise.resolve().then(() => __importStar(require("chalk")))).default;
|
|
150
|
+
process.stderr.write(chalk.yellow(" Warnings detected. Proceeding with install.\n\n"));
|
|
151
|
+
const code = await (0, pip_wrapper_1.runPip)(parsed.rawArgs);
|
|
152
|
+
process.exit(code);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (result.action === "block") {
|
|
156
|
+
process.stdout.write((0, static_output_1.renderResultStatic)(result, config) + "\n");
|
|
157
|
+
if (parsed.dgForce) {
|
|
158
|
+
dispatch({ type: "BLOCKED", result, dgForce: true });
|
|
159
|
+
dispatch({ type: "INSTALLING" });
|
|
160
|
+
const code = await (0, pip_wrapper_1.runPip)(parsed.rawArgs);
|
|
161
|
+
dispatch({ type: "DONE", exitCode: code });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// Prompt user
|
|
165
|
+
dispatch({ type: "BLOCKED", result, dgForce: false });
|
|
166
|
+
const shouldProceed = await new Promise((resolve) => {
|
|
167
|
+
pendingInstall.current = () => resolve(true);
|
|
168
|
+
rejectRef.current = () => resolve(false);
|
|
169
|
+
});
|
|
170
|
+
if (shouldProceed) {
|
|
171
|
+
dispatch({ type: "INSTALLING" });
|
|
172
|
+
const code = await (0, pip_wrapper_1.runPip)(parsed.rawArgs);
|
|
173
|
+
dispatch({ type: "DONE", exitCode: code });
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
dispatch({ type: "DONE", exitCode: 2 });
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (error instanceof api_1.TrialExhaustedError) {
|
|
183
|
+
dispatch({ type: "TRIAL_EXHAUSTED" });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
187
|
+
dispatch({ type: "ERROR", message, proceed: true });
|
|
188
|
+
dispatch({ type: "INSTALLING" });
|
|
189
|
+
const code = await (0, pip_wrapper_1.runPip)(parsedRef.current.rawArgs);
|
|
190
|
+
dispatch({ type: "DONE", exitCode: code });
|
|
191
|
+
}
|
|
192
|
+
})();
|
|
193
|
+
}, [pipArgs, config]);
|
|
194
|
+
return { state, confirmInstall, rejectInstall };
|
|
195
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useScan = useScan;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const lockfile_1 = require("../../lockfile");
|
|
6
|
+
const api_1 = require("../../api");
|
|
7
|
+
const discover_1 = require("../../discover");
|
|
8
|
+
const telemetry_1 = require("../../telemetry");
|
|
9
|
+
const lockfile_2 = require("../../lockfile");
|
|
10
|
+
function reducer(_state, action) {
|
|
11
|
+
switch (action.type) {
|
|
12
|
+
case "PROJECTS_FOUND":
|
|
13
|
+
return { phase: "selecting", projects: action.projects };
|
|
14
|
+
case "RESTART_SELECTION":
|
|
15
|
+
return { phase: "selecting", projects: action.projects };
|
|
16
|
+
case "DISCOVERY_COMPLETE":
|
|
17
|
+
return { phase: "scanning", done: 0, total: action.packages.length, currentBatch: [] };
|
|
18
|
+
case "DISCOVERY_EMPTY":
|
|
19
|
+
return { phase: "empty", message: action.message };
|
|
20
|
+
case "SCAN_PROGRESS":
|
|
21
|
+
return { phase: "scanning", done: action.done, total: action.total, currentBatch: action.currentBatch };
|
|
22
|
+
case "SCAN_COMPLETE":
|
|
23
|
+
return { phase: "results", result: action.result, durationMs: action.durationMs, skippedCount: action.skippedCount, discoveredTotal: action.discoveredTotal };
|
|
24
|
+
case "ERROR":
|
|
25
|
+
return { phase: "error", error: action.error };
|
|
26
|
+
case "TRIAL_EXHAUSTED":
|
|
27
|
+
return { phase: "trial_exhausted", scansUsed: action.scansUsed, maxScans: action.maxScans };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function useScan(config) {
|
|
31
|
+
const [state, dispatch] = (0, react_1.useReducer)(reducer, { phase: "discovering" });
|
|
32
|
+
const started = (0, react_1.useRef)(false);
|
|
33
|
+
const [multiProjects, setMultiProjects] = (0, react_1.useState)(null);
|
|
34
|
+
(0, react_1.useEffect)(() => {
|
|
35
|
+
if (started.current)
|
|
36
|
+
return;
|
|
37
|
+
started.current = true;
|
|
38
|
+
// Always discover projects for the back button
|
|
39
|
+
const projects = (0, discover_1.discoverProjects)(process.cwd());
|
|
40
|
+
if (projects.length > 1)
|
|
41
|
+
setMultiProjects(projects);
|
|
42
|
+
// Multiple projects — always show the project selector first
|
|
43
|
+
if (projects.length > 1) {
|
|
44
|
+
dispatch({ type: "PROJECTS_FOUND", projects });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
// Single project or none — try lockfile-based discovery
|
|
49
|
+
const discovery = (0, lockfile_1.discoverChanges)(process.cwd(), config);
|
|
50
|
+
const packages = discovery.packages;
|
|
51
|
+
if (packages.length === 0) {
|
|
52
|
+
dispatch({ type: "DISCOVERY_EMPTY", message: "No package changes detected." });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// Single project with packages — scan directly
|
|
56
|
+
dispatch({ type: "DISCOVERY_COMPLETE", packages, skippedCount: discovery.skipped.length });
|
|
57
|
+
runNpmScan(packages, discovery.skipped.length, config, dispatch);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
if (projects.length === 0) {
|
|
61
|
+
dispatch({ type: "DISCOVERY_EMPTY", message: "No dependency files found." });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Single project — skip selection, go straight to scan
|
|
65
|
+
dispatch({ type: "DISCOVERY_COMPLETE", packages: [], skippedCount: 0 });
|
|
66
|
+
scanProjects(projects, config, dispatch);
|
|
67
|
+
}
|
|
68
|
+
}, [config]);
|
|
69
|
+
const scanSelectedProjects = (0, react_1.useCallback)((projects) => {
|
|
70
|
+
dispatch({ type: "DISCOVERY_COMPLETE", packages: [], skippedCount: 0 });
|
|
71
|
+
scanProjects(projects, config, dispatch);
|
|
72
|
+
}, [config]);
|
|
73
|
+
const restartSelection = (0, react_1.useCallback)(() => {
|
|
74
|
+
if (!multiProjects)
|
|
75
|
+
return;
|
|
76
|
+
dispatch({ type: "RESTART_SELECTION", projects: multiProjects });
|
|
77
|
+
}, [multiProjects]);
|
|
78
|
+
return {
|
|
79
|
+
state,
|
|
80
|
+
scanSelectedProjects,
|
|
81
|
+
restartSelection: multiProjects ? restartSelection : null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function runNpmScan(packages, skippedCount, config, dispatch) {
|
|
85
|
+
try {
|
|
86
|
+
const startMs = Date.now();
|
|
87
|
+
const result = await (0, api_1.callAnalyzeAPI)(packages, config, (done, total) => {
|
|
88
|
+
const batchStart = Math.max(0, done - 15);
|
|
89
|
+
const currentBatch = packages.slice(batchStart, done).map((p) => p.name);
|
|
90
|
+
dispatch({ type: "SCAN_PROGRESS", done, total, currentBatch });
|
|
91
|
+
});
|
|
92
|
+
dispatch({ type: "SCAN_COMPLETE", result, durationMs: Date.now() - startMs, skippedCount });
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof api_1.TrialExhaustedError) {
|
|
96
|
+
dispatch({ type: "TRIAL_EXHAUSTED", scansUsed: error.scansUsed, maxScans: error.maxScans });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
100
|
+
(0, telemetry_1.captureError)(err, { command: "scan", nodeVersion: process.version, cliVersion: "", authenticated: !!config.apiKey });
|
|
101
|
+
dispatch({ type: "ERROR", error: err });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function scanProjects(projects, config, dispatch) {
|
|
105
|
+
try {
|
|
106
|
+
const npmProjects = projects.filter((p) => p.ecosystem === "npm");
|
|
107
|
+
const pypiProjects = projects.filter((p) => p.ecosystem === "pypi");
|
|
108
|
+
// Multi-project selector always scans all packages (not just git-changed)
|
|
109
|
+
const fullScanConfig = { ...config, scanAll: true };
|
|
110
|
+
// Collect all packages across projects, deduplicating by name@version
|
|
111
|
+
const npmPackages = [];
|
|
112
|
+
const pypiPackages = [];
|
|
113
|
+
const seenNpm = new Set();
|
|
114
|
+
const seenPypi = new Set();
|
|
115
|
+
for (const proj of npmProjects) {
|
|
116
|
+
try {
|
|
117
|
+
const discovery = (0, lockfile_1.discoverChanges)(proj.path, fullScanConfig);
|
|
118
|
+
for (const pkg of discovery.packages) {
|
|
119
|
+
const key = `${pkg.name}@${pkg.version}`;
|
|
120
|
+
if (!seenNpm.has(key)) {
|
|
121
|
+
seenNpm.add(key);
|
|
122
|
+
npmPackages.push(pkg);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch { /* ignore — skip unparseable project */ }
|
|
127
|
+
}
|
|
128
|
+
for (const proj of pypiProjects) {
|
|
129
|
+
try {
|
|
130
|
+
const packages = (0, lockfile_2.parsePythonDepFile)(proj.path, proj.depFile);
|
|
131
|
+
for (const pkg of packages) {
|
|
132
|
+
const key = `${pkg.name}@${pkg.version}`;
|
|
133
|
+
if (!seenPypi.has(key)) {
|
|
134
|
+
seenPypi.add(key);
|
|
135
|
+
pypiPackages.push(pkg);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch { /* ignore — skip unparseable project */ }
|
|
140
|
+
}
|
|
141
|
+
const discoveredTotal = npmPackages.length + pypiPackages.length;
|
|
142
|
+
// Batching is handled by the API client — no client-side cap needed
|
|
143
|
+
const totalPackages = npmPackages.length + pypiPackages.length;
|
|
144
|
+
if (totalPackages === 0) {
|
|
145
|
+
dispatch({ type: "DISCOVERY_EMPTY", message: "No packages to scan." });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const startMs = Date.now();
|
|
149
|
+
const results = [];
|
|
150
|
+
let completed = 0;
|
|
151
|
+
if (npmPackages.length > 0) {
|
|
152
|
+
const npmResult = await (0, api_1.callAnalyzeAPI)(npmPackages, config, (done) => {
|
|
153
|
+
completed = done;
|
|
154
|
+
dispatch({
|
|
155
|
+
type: "SCAN_PROGRESS",
|
|
156
|
+
done: completed,
|
|
157
|
+
total: totalPackages,
|
|
158
|
+
currentBatch: npmPackages.slice(Math.max(0, done - 15), done).map((p) => p.name),
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
completed = npmPackages.length;
|
|
162
|
+
results.push(npmResult);
|
|
163
|
+
}
|
|
164
|
+
if (pypiPackages.length > 0) {
|
|
165
|
+
const pypiResult = await (0, api_1.callPyPIAnalyzeAPI)(pypiPackages, config, (done) => {
|
|
166
|
+
dispatch({
|
|
167
|
+
type: "SCAN_PROGRESS",
|
|
168
|
+
done: completed + done,
|
|
169
|
+
total: totalPackages,
|
|
170
|
+
currentBatch: pypiPackages.slice(Math.max(0, done - 15), done).map((p) => p.name),
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
results.push(pypiResult);
|
|
174
|
+
}
|
|
175
|
+
// Merge results
|
|
176
|
+
const allPackages = results.flatMap((r) => r.packages);
|
|
177
|
+
const maxScore = Math.max(0, ...allPackages.map((p) => p.score));
|
|
178
|
+
const action = maxScore >= 70 ? "block"
|
|
179
|
+
: maxScore >= 60 ? "warn"
|
|
180
|
+
: "pass";
|
|
181
|
+
const safeVersions = {};
|
|
182
|
+
for (const r of results)
|
|
183
|
+
Object.assign(safeVersions, r.safeVersions);
|
|
184
|
+
const merged = {
|
|
185
|
+
score: maxScore,
|
|
186
|
+
action,
|
|
187
|
+
packages: allPackages,
|
|
188
|
+
safeVersions,
|
|
189
|
+
durationMs: Date.now() - startMs,
|
|
190
|
+
};
|
|
191
|
+
dispatch({ type: "SCAN_COMPLETE", result: merged, durationMs: merged.durationMs, skippedCount: 0, discoveredTotal });
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (error instanceof api_1.TrialExhaustedError) {
|
|
195
|
+
dispatch({ type: "TRIAL_EXHAUSTED", scansUsed: error.scansUsed, maxScans: error.maxScans });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
199
|
+
(0, telemetry_1.captureError)(err, { command: "scan", nodeVersion: process.version, cliVersion: "", authenticated: !!config.apiKey });
|
|
200
|
+
dispatch({ type: "ERROR", error: err });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useTerminalSize = useTerminalSize;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const ink_1 = require("ink");
|
|
6
|
+
function useTerminalSize() {
|
|
7
|
+
const { stdout } = (0, ink_1.useStdout)();
|
|
8
|
+
const [size, setSize] = (0, react_1.useState)({
|
|
9
|
+
rows: stdout?.rows ?? process.stdout.rows ?? 24,
|
|
10
|
+
cols: stdout?.columns ?? process.stdout.columns ?? 80,
|
|
11
|
+
});
|
|
12
|
+
(0, react_1.useEffect)(() => {
|
|
13
|
+
const handle = () => {
|
|
14
|
+
const rows = process.stdout.rows ?? 24;
|
|
15
|
+
const cols = process.stdout.columns ?? 80;
|
|
16
|
+
if (process.stdout.isTTY) {
|
|
17
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
18
|
+
}
|
|
19
|
+
setSize({ rows, cols });
|
|
20
|
+
};
|
|
21
|
+
process.stdout.setMaxListeners(process.stdout.getMaxListeners() + 1);
|
|
22
|
+
process.stdout.on("resize", handle);
|
|
23
|
+
return () => {
|
|
24
|
+
process.stdout.off("resize", handle);
|
|
25
|
+
process.stdout.setMaxListeners(Math.max(0, process.stdout.getMaxListeners() - 1));
|
|
26
|
+
};
|
|
27
|
+
}, []);
|
|
28
|
+
return size;
|
|
29
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.isNewer = isNewer;
|
|
37
|
+
exports.checkForUpdate = checkForUpdate;
|
|
38
|
+
exports.runUpdate = runUpdate;
|
|
39
|
+
const node_fs_1 = require("node:fs");
|
|
40
|
+
const node_os_1 = require("node:os");
|
|
41
|
+
const node_path_1 = require("node:path");
|
|
42
|
+
const node_child_process_1 = require("node:child_process");
|
|
43
|
+
const PKG_NAME = "@westbayberry/dg";
|
|
44
|
+
const CACHE_FILE = (0, node_path_1.join)((0, node_os_1.homedir)(), ".dg-update-check.json");
|
|
45
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
46
|
+
function readCache() {
|
|
47
|
+
try {
|
|
48
|
+
const raw = (0, node_fs_1.readFileSync)(CACHE_FILE, "utf-8");
|
|
49
|
+
const data = JSON.parse(raw);
|
|
50
|
+
if (typeof data.latest === "string" && typeof data.checkedAt === "number") {
|
|
51
|
+
return data;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch { /* ignore — cache is best-effort */ }
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function writeCache(entry) {
|
|
58
|
+
try {
|
|
59
|
+
(0, node_fs_1.writeFileSync)(CACHE_FILE, JSON.stringify(entry), "utf-8");
|
|
60
|
+
}
|
|
61
|
+
catch { /* ignore — cache is best-effort */ }
|
|
62
|
+
}
|
|
63
|
+
async function fetchLatestVersion() {
|
|
64
|
+
try {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
67
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}/latest`, { headers: { Accept: "application/json" }, signal: controller.signal });
|
|
68
|
+
clearTimeout(timeoutId);
|
|
69
|
+
if (!res.ok)
|
|
70
|
+
return null;
|
|
71
|
+
const data = (await res.json());
|
|
72
|
+
return typeof data.version === "string" ? data.version : null;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function parseVersion(v) {
|
|
79
|
+
return v.replace(/^v/, "").split(".").map(Number);
|
|
80
|
+
}
|
|
81
|
+
// isNewer(a, b) returns true iff `a` is strictly newer than `b`.
|
|
82
|
+
// Used by the server-driven version floor (api.ts): isNewer(minClientVersion, currentVersion)
|
|
83
|
+
// means "the required minimum is newer than what's running" — the condition we must throw on.
|
|
84
|
+
// Argument order matters: reversing = silent security-gate failure.
|
|
85
|
+
function isNewer(a, b) {
|
|
86
|
+
const aParts = parseVersion(a);
|
|
87
|
+
const bParts = parseVersion(b);
|
|
88
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
89
|
+
const x = aParts[i] ?? 0;
|
|
90
|
+
const y = bParts[i] ?? 0;
|
|
91
|
+
if (x > y)
|
|
92
|
+
return true;
|
|
93
|
+
if (x < y)
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
function formatBanner(current, latest) {
|
|
99
|
+
return `\n \u26A0 Update available: ${current} \u2192 ${latest}\n Run \`dg update\` to upgrade\n\n`;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Check npm registry for a newer version. Returns a plain banner string (no color),
|
|
103
|
+
* or null if no update / if running in CI or --json mode.
|
|
104
|
+
* The caller is responsible for wrapping the string in chalk.dim() when printing.
|
|
105
|
+
*
|
|
106
|
+
* Does NOT spawn `npm install -g` — users must run `dg update` explicitly.
|
|
107
|
+
* Silent auto-update was removed as a self-inflicted supply-chain vector.
|
|
108
|
+
*/
|
|
109
|
+
async function checkForUpdate(currentVersion) {
|
|
110
|
+
// Skip in CI and --json mode — no interactive banner makes sense there,
|
|
111
|
+
// and CI pipelines should not reach out to the npm registry on every scan.
|
|
112
|
+
if (process.env.CI || process.argv.includes("--json"))
|
|
113
|
+
return null;
|
|
114
|
+
const cached = readCache();
|
|
115
|
+
if (cached && Date.now() - cached.checkedAt < CHECK_INTERVAL_MS) {
|
|
116
|
+
if (isNewer(cached.latest, currentVersion)) {
|
|
117
|
+
return formatBanner(currentVersion, cached.latest);
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const latest = await fetchLatestVersion();
|
|
122
|
+
if (!latest)
|
|
123
|
+
return null;
|
|
124
|
+
writeCache({ latest, checkedAt: Date.now() });
|
|
125
|
+
if (isNewer(latest, currentVersion)) {
|
|
126
|
+
return formatBanner(currentVersion, latest);
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
async function runUpdate(currentVersion) {
|
|
131
|
+
const chalk = (await Promise.resolve().then(() => __importStar(require("chalk")))).default;
|
|
132
|
+
process.stderr.write(chalk.dim(" Checking for updates...\n"));
|
|
133
|
+
const latest = await fetchLatestVersion();
|
|
134
|
+
if (!latest) {
|
|
135
|
+
process.stderr.write(chalk.red(" Could not reach npm registry.\n"));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
if (!isNewer(latest, currentVersion)) {
|
|
139
|
+
process.stderr.write(chalk.green(` Already on latest version (${currentVersion}).\n`));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
process.stderr.write(chalk.dim(` Installing ${PKG_NAME}@${latest}...\n`));
|
|
143
|
+
try {
|
|
144
|
+
(0, node_child_process_1.execFileSync)("npm", ["install", "-g", `${PKG_NAME}@${latest}`], { stdio: "inherit" });
|
|
145
|
+
writeCache({ latest, checkedAt: Date.now() });
|
|
146
|
+
process.stderr.write(chalk.green(`\n Updated ${currentVersion} → ${latest}\n`));
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
process.stderr.write(chalk.red(`\n Update failed. Try manually: npm i -g ${PKG_NAME}@${latest}\n`));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Demo fixture for the first-run wizard. Shape mirrors APIPackageResult so
|
|
3
|
+
// ScanResultCard renders demo and real results identically.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.DEMO_SCAN_TOTAL_MS = exports.DEMO_SCAN_LABELS = exports.DEMO_MALICIOUS_PACKAGE = void 0;
|
|
6
|
+
exports.friendlyCategory = friendlyCategory;
|
|
7
|
+
exports.DEMO_MALICIOUS_PACKAGE = {
|
|
8
|
+
isDemo: true,
|
|
9
|
+
score: 91,
|
|
10
|
+
action: "block",
|
|
11
|
+
package: {
|
|
12
|
+
name: "malicious-demo",
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
score: 91,
|
|
15
|
+
cached: false,
|
|
16
|
+
findings: [
|
|
17
|
+
{
|
|
18
|
+
category: "Steals your passwords and API keys",
|
|
19
|
+
severity: 5,
|
|
20
|
+
title: "reads your environment variables on install",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
category: "Hides what it's doing",
|
|
24
|
+
severity: 5,
|
|
25
|
+
title: "the code is jumbled so it's hard to read",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
category: "Phones home",
|
|
29
|
+
severity: 4,
|
|
30
|
+
title: "downloads more code from the internet and runs it",
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
reasons: [
|
|
34
|
+
"reads env vars on install",
|
|
35
|
+
"three layers of obfuscation",
|
|
36
|
+
"postinstall pipes curl to a shell",
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
exports.DEMO_SCAN_LABELS = [
|
|
41
|
+
"looking at malicious-demo@1.0.0...",
|
|
42
|
+
"reading the code...",
|
|
43
|
+
"checking 78 patterns...",
|
|
44
|
+
"scoring...",
|
|
45
|
+
];
|
|
46
|
+
exports.DEMO_SCAN_TOTAL_MS = 500;
|
|
47
|
+
// Engine category → plain-English label for the wizard.
|
|
48
|
+
const FRIENDLY_CATEGORIES = {
|
|
49
|
+
"Credential Exfiltration": "Steals passwords and API keys",
|
|
50
|
+
"Network Exfiltration": "Sends your data to the internet",
|
|
51
|
+
"Obfuscated Code": "Hides what it's doing",
|
|
52
|
+
"Code Execution": "Runs arbitrary code",
|
|
53
|
+
"Install Hook": "Runs code during install",
|
|
54
|
+
"Postinstall Curl Pipe": "Downloads code and runs it",
|
|
55
|
+
"Suspicious Network": "Talks to suspicious servers",
|
|
56
|
+
"Typosquatting": "Pretending to be a popular package",
|
|
57
|
+
"Sandbox Escape": "Tries to break out of safety checks",
|
|
58
|
+
};
|
|
59
|
+
function friendlyCategory(category) {
|
|
60
|
+
if (!category)
|
|
61
|
+
return "Finding";
|
|
62
|
+
return FRIENDLY_CATEGORIES[category] ?? category;
|
|
63
|
+
}
|