@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,273 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parsePipArgs = parsePipArgs;
4
+ exports.parseRequirementsFile = parseRequirementsFile;
5
+ exports.parsePipSpec = parsePipSpec;
6
+ exports.resolvePipVersion = resolvePipVersion;
7
+ exports.resolvePackages = resolvePackages;
8
+ exports.detectPipBinary = detectPipBinary;
9
+ exports.runPip = runPip;
10
+ const node_child_process_1 = require("node:child_process");
11
+ const node_fs_1 = require("node:fs");
12
+ /** pip commands that install packages and should trigger a scan */
13
+ const INSTALL_COMMANDS = new Set(["install", "update"]);
14
+ /**
15
+ * Parse the argv after `dg pip ...` to extract the pip command and package specifiers.
16
+ */
17
+ function parsePipArgs(args) {
18
+ let dgForce = false;
19
+ const filtered = [];
20
+ for (const arg of args) {
21
+ if (arg === "--dg-force") {
22
+ dgForce = true;
23
+ }
24
+ else {
25
+ filtered.push(arg);
26
+ }
27
+ }
28
+ const command = filtered[0] ?? "";
29
+ const shouldScan = INSTALL_COMMANDS.has(command);
30
+ const packages = [];
31
+ let requirementsFile;
32
+ if (shouldScan) {
33
+ for (let i = 1; i < filtered.length; i++) {
34
+ const arg = filtered[i];
35
+ // -r / --requirement
36
+ if (arg === "-r" || arg === "--requirement") {
37
+ if (i + 1 < filtered.length) {
38
+ requirementsFile = filtered[i + 1];
39
+ i++;
40
+ }
41
+ continue;
42
+ }
43
+ // Skip flags and their values
44
+ if (arg.startsWith("-")) {
45
+ if (pipFlagTakesValue(arg))
46
+ i++;
47
+ continue;
48
+ }
49
+ packages.push(arg);
50
+ }
51
+ }
52
+ return {
53
+ command,
54
+ packages,
55
+ rawArgs: filtered,
56
+ dgForce,
57
+ shouldScan,
58
+ requirementsFile,
59
+ };
60
+ }
61
+ /** pip flags that consume the next argument as a value */
62
+ function pipFlagTakesValue(flag) {
63
+ const valueFlags = [
64
+ "--index-url", "-i",
65
+ "--extra-index-url",
66
+ "--constraint", "-c",
67
+ "--requirement", "-r",
68
+ "--target", "-t",
69
+ "--prefix",
70
+ "--src",
71
+ "--root",
72
+ "--config-settings",
73
+ ];
74
+ for (const f of valueFlags) {
75
+ if (flag === f)
76
+ return true;
77
+ }
78
+ return false;
79
+ }
80
+ /**
81
+ * Parse a requirements.txt file into package specifiers.
82
+ *
83
+ * Handles:
84
+ * - name==version, name>=version, name~=version, name (bare)
85
+ * - Comments (# ...)
86
+ * - Inline comments (name==1.0 # comment)
87
+ * - Environment markers (name>=1.0; python_version >= "3.6")
88
+ *
89
+ * Skips:
90
+ * - Editable installs (-e ./local)
91
+ * - VCS URLs (git+https://...)
92
+ * - Nested -r includes
93
+ * - pip options (--index-url, etc.)
94
+ * - Empty lines
95
+ */
96
+ function parseRequirementsFile(filePath) {
97
+ if (!(0, node_fs_1.existsSync)(filePath))
98
+ return [];
99
+ try {
100
+ const content = (0, node_fs_1.readFileSync)(filePath, "utf-8");
101
+ const specs = [];
102
+ for (const rawLine of content.split("\n")) {
103
+ let line = rawLine.trim();
104
+ if (!line)
105
+ continue;
106
+ // Skip comments
107
+ if (line.startsWith("#"))
108
+ continue;
109
+ // Skip editable installs
110
+ if (line.startsWith("-e") || line.startsWith("--editable"))
111
+ continue;
112
+ // Skip VCS URLs
113
+ if (/^(git|svn|hg|bzr)\+/.test(line))
114
+ continue;
115
+ // Skip nested -r includes
116
+ if (line.startsWith("-r") || line.startsWith("--requirement"))
117
+ continue;
118
+ // Skip pip options
119
+ if (line.startsWith("--"))
120
+ continue;
121
+ // Strip inline comments
122
+ const commentIdx = line.indexOf(" #");
123
+ if (commentIdx !== -1)
124
+ line = line.slice(0, commentIdx).trim();
125
+ // Strip environment markers (everything after ;)
126
+ const markerIdx = line.indexOf(";");
127
+ if (markerIdx !== -1)
128
+ line = line.slice(0, markerIdx).trim();
129
+ if (line)
130
+ specs.push(line);
131
+ }
132
+ return specs;
133
+ }
134
+ catch {
135
+ return [];
136
+ }
137
+ }
138
+ /**
139
+ * Parse a pip package specifier like "requests", "flask>=2.0", "django==4.2.1"
140
+ * into { name, versionSpec }.
141
+ */
142
+ function parsePipSpec(spec) {
143
+ // Split on version operators: ==, ===, >=, <=, ~=, !=, >, <
144
+ // pip's === is "arbitrary equality" for direct references
145
+ const match = spec.match(/^([a-zA-Z0-9][-a-zA-Z0-9._]*)((?:={2,3}|[><!~]=?).+)?$/);
146
+ if (!match)
147
+ return { name: spec, versionSpec: null };
148
+ return {
149
+ name: match[1],
150
+ versionSpec: match[2] ?? null,
151
+ };
152
+ }
153
+ /**
154
+ * Resolve version from PyPI JSON API.
155
+ * For "name==1.2.3" → verifies that exact version exists.
156
+ * For "name>=1.2.3" or bare "name" → fetches latest version.
157
+ */
158
+ async function resolvePipVersion(spec) {
159
+ const { name, versionSpec } = parsePipSpec(spec);
160
+ // Exact version pin: verify it exists
161
+ if (versionSpec?.startsWith("==")) {
162
+ const version = versionSpec.slice(2);
163
+ try {
164
+ const controller = new AbortController();
165
+ const timer = setTimeout(() => controller.abort(), 15000);
166
+ const resp = await fetch(`https://pypi.org/pypi/${name}/${version}/json`, {
167
+ signal: controller.signal,
168
+ });
169
+ clearTimeout(timer);
170
+ if (resp.ok)
171
+ return version;
172
+ return null;
173
+ }
174
+ catch {
175
+ return null;
176
+ }
177
+ }
178
+ // Any other constraint or bare name: fetch latest
179
+ try {
180
+ const controller = new AbortController();
181
+ const timer = setTimeout(() => controller.abort(), 15000);
182
+ const resp = await fetch(`https://pypi.org/pypi/${name}/json`, {
183
+ signal: controller.signal,
184
+ });
185
+ clearTimeout(timer);
186
+ if (!resp.ok)
187
+ return null;
188
+ const data = await resp.json();
189
+ return data.info?.version ?? null;
190
+ }
191
+ catch {
192
+ return null;
193
+ }
194
+ }
195
+ /**
196
+ * Build PackageInput[] from package specifiers by resolving versions in parallel.
197
+ */
198
+ async function resolvePackages(specs) {
199
+ const results = await Promise.allSettled(specs.map(async (spec) => {
200
+ const { name } = parsePipSpec(spec);
201
+ const version = await resolvePipVersion(spec);
202
+ return { spec, name, version };
203
+ }));
204
+ const resolved = [];
205
+ const failed = [];
206
+ for (const result of results) {
207
+ if (result.status === "rejected") {
208
+ failed.push("unknown");
209
+ continue;
210
+ }
211
+ const { spec, name, version } = result.value;
212
+ if (version) {
213
+ resolved.push({
214
+ name,
215
+ version,
216
+ previousVersion: null,
217
+ isNew: true,
218
+ });
219
+ }
220
+ else {
221
+ failed.push(spec);
222
+ }
223
+ }
224
+ return { resolved, failed };
225
+ }
226
+ /** Cache the detected pip binary across calls in the same process. */
227
+ let cachedPipBinary = null;
228
+ /**
229
+ * Detect the working pip binary.
230
+ * Order: python -m pip (most reliable) → pip3 → pip
231
+ */
232
+ async function detectPipBinary() {
233
+ if (cachedPipBinary)
234
+ return cachedPipBinary;
235
+ const candidates = ["python3 -m pip", "python -m pip", "pip3", "pip"];
236
+ for (const cmd of candidates) {
237
+ try {
238
+ const parts = cmd.split(" ");
239
+ const code = await new Promise((resolve) => {
240
+ const child = (0, node_child_process_1.spawn)(parts[0], [...parts.slice(1), "--version"], {
241
+ stdio: ["pipe", "pipe", "pipe"],
242
+ timeout: 5000,
243
+ });
244
+ child.on("close", (c) => resolve(c ?? 1));
245
+ child.on("error", () => resolve(1));
246
+ });
247
+ if (code === 0) {
248
+ cachedPipBinary = cmd;
249
+ return cmd;
250
+ }
251
+ }
252
+ catch {
253
+ // Try next candidate
254
+ }
255
+ }
256
+ throw new Error("pip not found. Install pip or ensure 'python -m pip' is available.");
257
+ }
258
+ /**
259
+ * Run the actual pip command, inheriting stdio.
260
+ * Returns the pip exit code.
261
+ */
262
+ async function runPip(args) {
263
+ const pipCmd = await detectPipBinary();
264
+ const parts = pipCmd.split(" ");
265
+ return new Promise((resolve) => {
266
+ const child = (0, node_child_process_1.spawn)(parts[0], [...parts.slice(1), ...args], {
267
+ stdio: "inherit",
268
+ shell: false,
269
+ });
270
+ child.on("close", (code) => resolve(code ?? 1));
271
+ child.on("error", () => resolve(1));
272
+ });
273
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitize = sanitize;
4
+ exports.sanitizeResponse = sanitizeResponse;
5
+ const node_util_1 = require("node:util");
6
+ function sanitize(s) {
7
+ return (0, node_util_1.stripVTControlCharacters)(s);
8
+ }
9
+ function sanitizeFinding(f) {
10
+ const result = { severity: f.severity };
11
+ if (f.category !== undefined)
12
+ result.category = sanitize(f.category);
13
+ if (f.title !== undefined)
14
+ result.title = sanitize(f.title);
15
+ if (f.evidence !== undefined)
16
+ result.evidence = f.evidence.map(sanitize);
17
+ return result;
18
+ }
19
+ function sanitizeResponse(response) {
20
+ const packages = response.packages.map((pkg) => ({
21
+ name: sanitize(pkg.name),
22
+ version: sanitize(pkg.version),
23
+ score: pkg.score,
24
+ findings: (pkg.findings ?? []).map(sanitizeFinding),
25
+ reasons: (pkg.reasons ?? []).map(sanitize),
26
+ recommendation: pkg.recommendation ? sanitize(pkg.recommendation) : undefined,
27
+ cached: pkg.cached,
28
+ license: pkg.license,
29
+ }));
30
+ return {
31
+ score: response.score,
32
+ action: response.action,
33
+ packages,
34
+ safeVersions: response.safeVersions,
35
+ durationMs: response.durationMs,
36
+ trialScansRemaining: response.trialScansRemaining,
37
+ };
38
+ }
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ // Shared scan primitive used by both useScan (the `dg scan` command) and
3
+ // useInit (the `dg init` wizard's verify and first-scan phases).
4
+ //
5
+ // Why this exists: useScan does a lot of multi-project / interactive logic
6
+ // (project selection, mixed npm+pypi, batched progress callbacks). useInit
7
+ // only needs the simple thing: "scan whatever's in this directory once and
8
+ // give me back the result and a wall-clock duration." Rather than copy-paste
9
+ // the discovery+analyze sequence into useInit, we extract the primitive here
10
+ // so both code paths use the same engine and the same result shape.
11
+ //
12
+ // useScan does NOT yet use this — it has more complex multi-project handling
13
+ // that doesn't fit the single-call shape. A future refactor can fold useScan's
14
+ // single-project path into this. For now this is consumed by useInit only.
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.scanProjectAtPath = scanProjectAtPath;
17
+ exports.projectAtCwd = projectAtCwd;
18
+ const lockfile_1 = require("./lockfile");
19
+ const api_1 = require("./api");
20
+ const discover_1 = require("./discover");
21
+ const EMPTY_OK_RESULT = (durationMs) => ({
22
+ result: {
23
+ score: 0,
24
+ action: "pass",
25
+ packages: [],
26
+ safeVersions: {},
27
+ durationMs,
28
+ },
29
+ durationMs,
30
+ scannedCount: 0,
31
+ skippedCount: 0,
32
+ });
33
+ async function scanProjectAtPath(cwd, config, onProgress) {
34
+ const startNs = process.hrtime.bigint();
35
+ const elapsedMs = () => Number((process.hrtime.bigint() - startNs) / 1000000n);
36
+ try {
37
+ // Step 1 — discover packages from the lockfile in cwd
38
+ let discovery;
39
+ try {
40
+ discovery = (0, lockfile_1.discoverChanges)(cwd, config);
41
+ }
42
+ catch (e) {
43
+ // discoverChanges throws if there's no lockfile at all. For the
44
+ // wizard's purposes that's a valid "nothing to scan" outcome, not
45
+ // an error — we just want to know how long the discovery took and
46
+ // report that the project has no scannable deps yet.
47
+ return {
48
+ status: "no_packages",
49
+ result: EMPTY_OK_RESULT(elapsedMs()),
50
+ message: e instanceof Error ? e.message : "no lockfile found",
51
+ };
52
+ }
53
+ const npmPackages = discovery.packages;
54
+ const pyPackages = discovery.pythonPackages ?? [];
55
+ const totalDiscovered = npmPackages.length + pyPackages.length;
56
+ if (totalDiscovered === 0) {
57
+ return {
58
+ status: "no_packages",
59
+ result: EMPTY_OK_RESULT(elapsedMs()),
60
+ };
61
+ }
62
+ // Step 2 — call the analyze API for each ecosystem present, then merge.
63
+ // Per-batch progress is surfaced to the optional onProgress callback so the
64
+ // wizard can render a progress bar; done counts cumulate across ecosystems
65
+ // so the bar fills monotonically from 0 → totalDiscovered.
66
+ const responses = [];
67
+ let cumulativeDone = 0;
68
+ if (npmPackages.length > 0) {
69
+ const npmOnProgress = onProgress
70
+ ? (done, _total, currentBatch) => {
71
+ onProgress({
72
+ done: cumulativeDone + done,
73
+ total: totalDiscovered,
74
+ current: currentBatch ?? [],
75
+ });
76
+ }
77
+ : undefined;
78
+ const npmResp = await (0, api_1.callAnalyzeAPI)(npmPackages, config, npmOnProgress);
79
+ responses.push(npmResp);
80
+ cumulativeDone += npmPackages.length;
81
+ }
82
+ if (pyPackages.length > 0) {
83
+ // The PyPI analyze API takes the same shape as npm — discovery returns
84
+ // PackageInput[] which already matches PyPIPackageInput.
85
+ const pyOnProgress = onProgress
86
+ ? (done, _total) => {
87
+ onProgress({ done: cumulativeDone + done, total: totalDiscovered, current: [] });
88
+ }
89
+ : undefined;
90
+ const pyResp = await (0, api_1.callPyPIAnalyzeAPI)(pyPackages, config, pyOnProgress);
91
+ responses.push(pyResp);
92
+ cumulativeDone += pyPackages.length;
93
+ }
94
+ // Step 3 — merge results across ecosystems
95
+ const allPackages = responses.flatMap((r) => r.packages);
96
+ const maxScore = allPackages.length > 0
97
+ ? Math.max(0, ...allPackages.map((p) => p.score))
98
+ : 0;
99
+ const action = maxScore >= 70 ? "block" : maxScore >= 60 ? "warn" : "pass";
100
+ const safeVersions = {};
101
+ for (const r of responses)
102
+ Object.assign(safeVersions, r.safeVersions);
103
+ const totalDuration = elapsedMs();
104
+ const merged = {
105
+ score: maxScore,
106
+ action,
107
+ packages: allPackages,
108
+ safeVersions,
109
+ durationMs: totalDuration,
110
+ };
111
+ return {
112
+ status: "ok",
113
+ result: {
114
+ result: merged,
115
+ durationMs: totalDuration,
116
+ scannedCount: allPackages.length,
117
+ skippedCount: discovery.skipped?.length ?? 0,
118
+ },
119
+ };
120
+ }
121
+ catch (e) {
122
+ if (e instanceof api_1.TrialExhaustedError) {
123
+ return {
124
+ status: "trial_exhausted",
125
+ result: null,
126
+ message: e.message,
127
+ };
128
+ }
129
+ return {
130
+ status: "error",
131
+ result: null,
132
+ error: e instanceof Error ? e : new Error(String(e)),
133
+ message: e instanceof Error ? e.message : String(e),
134
+ };
135
+ }
136
+ }
137
+ /** Quick lockfile presence check used by the wizard's "what does this project
138
+ * even look like" detect phase. Returns the first project found in cwd, or
139
+ * null if there's nothing scannable. Wraps discoverProjects() but with a
140
+ * cheaper "any project at all?" semantic. */
141
+ function projectAtCwd(cwd) {
142
+ const found = (0, discover_1.discoverProjects)(cwd);
143
+ return found.length > 0 ? found[0] : null;
144
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ // Compute "is the user's setup complete?" for the running command.
3
+ //
4
+ // Used by `dg scan` and other commands to surface a visible alert when
5
+ // something the wizard would have set up is missing. The aim is to make
6
+ // incomplete state obvious every time the user interacts with DG, not
7
+ // just on first run.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.getSetupIssues = getSetupIssues;
10
+ const auth_1 = require("./auth");
11
+ const hook_1 = require("./hook");
12
+ /** Inspect the user's current state and return any setup gaps worth
13
+ * surfacing. Empty array means everything's set up. Never throws. */
14
+ function getSetupIssues(cwd) {
15
+ const issues = [];
16
+ try {
17
+ const key = (0, auth_1.getStoredApiKey)();
18
+ // A key is "present and valid-looking" only if it starts with the
19
+ // expected prefix. A stale or malformed key (e.g. dgk_test) should
20
+ // surface the same "not signed in" prompt so the user re-authenticates.
21
+ const hasValidKey = key !== null &&
22
+ (key.startsWith("dg_live_") || key.startsWith("dg_test_"));
23
+ if (!hasValidKey) {
24
+ issues.push({
25
+ id: "no_api_key",
26
+ label: key ? "API key looks invalid — try signing in again" : "Not signed in — running on the free trial",
27
+ fix: "dg login",
28
+ });
29
+ }
30
+ }
31
+ catch { /* best-effort */ }
32
+ try {
33
+ const hookInfo = (0, hook_1.detectHookFramework)(cwd);
34
+ if (!hookInfo.alreadyInstalled) {
35
+ issues.push({
36
+ id: "no_hook",
37
+ label: "No pre-commit hook — commits aren't scanned",
38
+ fix: "dg kitty",
39
+ });
40
+ }
41
+ }
42
+ catch {
43
+ // Some projects aren't git repos; treat as "no info" rather than missing.
44
+ }
45
+ return issues;
46
+ }