@westbayberry/dg 2.0.6 → 2.0.8

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.
@@ -6,16 +6,19 @@ export function defaultPromptIo() {
6
6
  isTTY: Boolean(process.stdin.isTTY && process.stderr.isTTY)
7
7
  };
8
8
  }
9
- export async function promptYesNo(question, io) {
9
+ export async function promptYesNo(question, io, defaultYes = false) {
10
10
  if (!io.isTTY) {
11
11
  return false;
12
12
  }
13
13
  const rl = createInterface({ input: io.input, output: io.output });
14
14
  try {
15
15
  const answer = await new Promise((resolve) => {
16
- rl.question(`${question} [y/N] `, resolve);
16
+ rl.question(`${question} ${defaultYes ? "[Y/n]" : "[y/N]"} `, resolve);
17
17
  });
18
18
  const normalized = answer.trim().toLowerCase();
19
+ if (normalized === "") {
20
+ return defaultYes;
21
+ }
19
22
  return normalized === "y" || normalized === "yes";
20
23
  }
21
24
  finally {
@@ -0,0 +1,89 @@
1
+ import { spawn } from "node:child_process";
2
+ import { analyzePackages } from "../api/analyze.js";
3
+ import { defaultPromptIo, promptYesNo } from "../install-ui/prompt.js";
4
+ import { parsePipReportInstallSet } from "./pip-report.js";
5
+ const PROCEED = { proceed: true };
6
+ function resolvePipInstallSet(binary, args, env) {
7
+ if (!binary)
8
+ return Promise.resolve(undefined);
9
+ return new Promise((resolve) => {
10
+ let stdout = "";
11
+ let settled = false;
12
+ let timer;
13
+ const finish = (value) => {
14
+ if (settled)
15
+ return;
16
+ settled = true;
17
+ if (timer)
18
+ clearTimeout(timer);
19
+ resolve(value);
20
+ };
21
+ let child;
22
+ try {
23
+ child = spawn(binary, [...args, "--dry-run", "--report", "-", "--quiet"], {
24
+ env,
25
+ stdio: ["ignore", "pipe", "ignore"]
26
+ });
27
+ }
28
+ catch {
29
+ finish(undefined);
30
+ return;
31
+ }
32
+ timer = setTimeout(() => {
33
+ try {
34
+ child.kill();
35
+ }
36
+ catch { /* already exited */ }
37
+ finish(undefined);
38
+ }, 30_000);
39
+ child.stdout?.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
40
+ child.on("error", () => finish(undefined));
41
+ child.on("close", (code) => finish(code === 0 ? parsePipReportInstallSet(stdout) : undefined));
42
+ });
43
+ }
44
+ function findingSummary(pkg) {
45
+ return pkg.reasons[0] ?? pkg.findings[0]?.title ?? pkg.findings[0]?.id ?? "flagged";
46
+ }
47
+ function renderPreflight(flagged, out) {
48
+ const blocks = flagged.filter((pkg) => pkg.action === "block").length;
49
+ const noun = flagged.length === 1 ? "package" : "packages";
50
+ const tail = blocks > 0 ? ` (${blocks} blocked)` : "";
51
+ out.write(`\n DG flagged ${flagged.length} ${noun} before install${tail}:\n`);
52
+ for (const pkg of flagged) {
53
+ const tag = pkg.action === "block" ? "block" : "warn";
54
+ out.write(` ${pkg.name}@${pkg.version} ${tag} ${findingSummary(pkg)}\n`);
55
+ }
56
+ out.write("\n");
57
+ }
58
+ export async function runInstallPreflight(manager, binary, childArgs, env) {
59
+ if (manager !== "pip" || !binary) {
60
+ return PROCEED;
61
+ }
62
+ const set = await resolvePipInstallSet(binary, childArgs, env);
63
+ if (!set || set.length === 0) {
64
+ return PROCEED;
65
+ }
66
+ let verdicts;
67
+ try {
68
+ verdicts = await analyzePackages(set.map((pkg) => ({ name: pkg.name, version: pkg.version })), { ecosystem: "pypi", env });
69
+ }
70
+ catch {
71
+ return PROCEED;
72
+ }
73
+ return decideFromVerdicts(verdicts.packages, defaultPromptIo());
74
+ }
75
+ export async function decideFromVerdicts(packages, io) {
76
+ const flagged = packages.filter((pkg) => pkg.action === "warn" || pkg.action === "block");
77
+ if (flagged.length === 0 || !io.isTTY) {
78
+ return PROCEED;
79
+ }
80
+ renderPreflight(flagged, io.output);
81
+ const hasBlock = flagged.some((pkg) => pkg.action === "block");
82
+ const accepted = hasBlock
83
+ ? await promptYesNo(" Override and install anyway?", io, false)
84
+ : await promptYesNo(" Proceed?", io, true);
85
+ if (!accepted) {
86
+ return { proceed: false };
87
+ }
88
+ return hasBlock ? { proceed: true, forceOverride: { force: true } } : PROCEED;
89
+ }
@@ -1,4 +1,5 @@
1
- import { createLaunchPlan, runWithProductionProxyLive } from "./run.js";
1
+ import { createLaunchPlan, runWithProductionProxyLive, EXIT_INSTALL_BLOCKED } from "./run.js";
2
+ import { runInstallPreflight } from "./install-preflight.js";
2
3
  import { isSupportedPackageManager } from "./classify.js";
3
4
  import { isCiEnv, resolvePresentation } from "../presentation/mode.js";
4
5
  const FALL_THROUGH = { handled: false };
@@ -16,9 +17,17 @@ export async function maybeRunLiveInstall(args, options = {}) {
16
17
  if (plan.classification.kind !== "protected" || !plan.realBinary.path) {
17
18
  return FALL_THROUGH;
18
19
  }
20
+ let effectiveOverride = forceOverride;
21
+ if (!forceOverride) {
22
+ const preflight = await runInstallPreflight(manager, plan.realBinary.path, childArgs, env);
23
+ if (!preflight.proceed) {
24
+ return { handled: true, result: { exitCode: EXIT_INSTALL_BLOCKED, stdout: "", stderr: " Install cancelled.\n" } };
25
+ }
26
+ effectiveOverride = preflight.forceOverride;
27
+ }
19
28
  const runOptions = {
20
29
  env,
21
- ...(forceOverride ? { forceOverride } : {})
30
+ ...(effectiveOverride ? { forceOverride: effectiveOverride } : {})
22
31
  };
23
32
  const { renderLiveInstall } = await import("../install-ui/live-install-app.js");
24
33
  try {
@@ -1,4 +1,4 @@
1
- export function parsePipReportInstallCount(stdout) {
1
+ function parseInstallArray(stdout) {
2
2
  const trimmed = stdout.trim();
3
3
  if (!trimmed)
4
4
  return undefined;
@@ -10,7 +10,7 @@ export function parsePipReportInstallCount(stdout) {
10
10
  try {
11
11
  const parsed = JSON.parse(candidate);
12
12
  if (Array.isArray(parsed.install)) {
13
- return parsed.install.length;
13
+ return parsed.install;
14
14
  }
15
15
  }
16
16
  catch {
@@ -19,3 +19,19 @@ export function parsePipReportInstallCount(stdout) {
19
19
  }
20
20
  return undefined;
21
21
  }
22
+ export function parsePipReportInstallCount(stdout) {
23
+ return parseInstallArray(stdout)?.length;
24
+ }
25
+ export function parsePipReportInstallSet(stdout) {
26
+ const install = parseInstallArray(stdout);
27
+ if (!install)
28
+ return undefined;
29
+ const set = [];
30
+ for (const entry of install) {
31
+ const metadata = entry.metadata;
32
+ if (metadata && typeof metadata.name === "string" && typeof metadata.version === "string") {
33
+ set.push({ name: metadata.name, version: metadata.version });
34
+ }
35
+ }
36
+ return set;
37
+ }
@@ -792,7 +792,7 @@ function scanTarballUploadPolicy(env) {
792
792
  };
793
793
  }
794
794
  function installVerdictTimeoutMs(env) {
795
- return parsePositiveInteger(env.DG_INSTALL_VERDICT_TIMEOUT_MS, 180_000);
795
+ return parsePositiveInteger(env.DG_INSTALL_VERDICT_TIMEOUT_MS, 240_000);
796
796
  }
797
797
  function parsePositiveInteger(value, fallback) {
798
798
  if (!value) {
@@ -1088,7 +1088,7 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
1088
1088
  const dpAbove = dpScroll;
1089
1089
  const dpBelow = Math.max(0, detailLines.length - dpScroll - detailContentRows);
1090
1090
  const dpVisible = detailLines.slice(dpScroll, dpScroll + detailContentRows);
1091
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, userStatus: userStatus, scanUsage: scanUsage, usageNearLimit: usageNearLimit }), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { bold: true, children: [groupNames(dpGroup), dpRep.license ? chalk.dim(" \u00B7 ") + (dpRep.license.riskCategory === "permissive" ? chalk.green(dpRep.license.spdx ?? dpRep.license.raw ?? "") : dpRep.license.riskCategory === "no-license" || dpRep.license.riskCategory === "network-copyleft" ? chalk.red(dpRep.license.spdx ?? dpRep.license.raw ?? "No license") : chalk.yellow(dpRep.license.spdx ?? dpRep.license.raw ?? "")) : ""] }), _jsx(Text, { children: dpColor(`score ${dpRep.score}`) })] }), dpAbove > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2191"), " ", dpAbove, " more above"] })), _jsx(Box, { flexDirection: "column", marginLeft: 2, children: dpVisible }), dpBelow > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2193"), " ", dpBelow, " more below"] }))] }), _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), _jsx(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: _jsxs(Box, { justifyContent: "space-between", children: [clean.length > 0 ? (_jsxs(Text, { children: [chalk.green("\u2713"), " ", chalk.green.bold(String(clean.length)), " ", chalk.dim(`package${clean.length !== 1 ? "s" : ""} passed with score 0`), " ", chalk.dim(`\u00b7 ${(durationMs / 1000).toFixed(1)}s`)] })) : (_jsxs(Text, { dimColor: true, children: [(durationMs / 1000).toFixed(1), "s"] })), result.freeScansRemaining !== undefined && (_jsx(Text, { dimColor: true, children: "Free tier \u00B7 dg login for higher scan limits" }))] }) }), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Text, { children: [" ", chalk.bold.cyan("\u2191\u2193"), " ", chalk.dim("scroll"), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("back"), " ", chalk.bold.cyan("q"), " ", chalk.dim("quit")] })] }));
1091
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, userStatus: userStatus, scanUsage: scanUsage, usageNearLimit: usageNearLimit }), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { bold: true, children: [groupNames(dpGroup), dpRep.license ? chalk.dim(" \u00B7 ") + (dpRep.license.riskCategory === "permissive" ? chalk.green(dpRep.license.spdx ?? dpRep.license.raw ?? "") : dpRep.license.riskCategory === "no-license" || dpRep.license.riskCategory === "network-copyleft" ? chalk.red(dpRep.license.spdx ?? dpRep.license.raw ?? "No license") : chalk.yellow(dpRep.license.spdx ?? dpRep.license.raw ?? "")) : ""] }), _jsx(Text, { children: dpColor(`score ${dpRep.score}`) })] }), dpAbove > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2191"), " ", dpAbove, " more above"] })), _jsx(Box, { flexDirection: "column", marginLeft: 2, children: dpVisible }), dpBelow > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2193"), " ", dpBelow, " more below"] }))] }), _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), _jsx(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: _jsxs(Box, { justifyContent: "space-between", children: [clean.length > 0 ? (_jsxs(Text, { children: [chalk.green("\u2713"), " ", chalk.green.bold(String(clean.length)), " ", chalk.dim(`package${clean.length !== 1 ? "s" : ""} passed`), " ", chalk.dim(`\u00b7 ${(durationMs / 1000).toFixed(1)}s`)] })) : (_jsxs(Text, { dimColor: true, children: [(durationMs / 1000).toFixed(1), "s"] })), result.freeScansRemaining !== undefined && (_jsx(Text, { dimColor: true, children: "Free tier \u00B7 dg login for higher scan limits" }))] }) }), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Text, { children: [" ", chalk.bold.cyan("\u2191\u2193"), " ", chalk.dim("scroll"), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("back"), " ", chalk.bold.cyan("q"), " ", chalk.dim("quit")] })] }));
1092
1092
  }
1093
1093
  }
1094
1094
  // ── List mode ──
@@ -1108,7 +1108,7 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
1108
1108
  : chalk.yellow;
1109
1109
  const arrow = level === "summary" ? "\u25BE" : "\u25B8"; // ▾ expanded, ▸ collapsed
1110
1110
  return (_jsxs(Box, { flexDirection: "column", children: [isCursor ? (_jsxs(Text, { backgroundColor: "#1a1a2e", children: [chalk.cyan("\u258C"), " ", chalk.cyan(arrow), " ", ` `, color(pad(label, 8)), chalk.bold(pad(truncate(names, nameCol - 2), nameCol)), lcColor(pad(lcStr, lcCol)), color(scoreStr.padStart(3)), " "] })) : (_jsxs(Text, { children: [` ${chalk.dim(arrow)} `, color(pad(label, 8)), pad(truncate(names, nameCol - 2), nameCol), lcColor(pad(lcStr, lcCol)), color(scoreStr.padStart(3))] })), level === "summary" && (_jsx(FindingsSummary, { group: group, maxWidth: innerWidth - 8, maxLines: globalIdx === view.expandedIndex ? animVisibleLines : undefined }))] }, group.key));
1111
- }), belowCount > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2193"), " ", belowCount, " more below"] }))] })] })), searchQuery && groups.length === 0 && (_jsx(Text, { dimColor: true, children: ` No flagged packages match "${searchQuery}"` })), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: [discoveredTotal !== undefined && discoveredTotal > total && (_jsxs(Text, { dimColor: true, children: ["Scanned ", total, " of ", discoveredTotal, " packages"] })), _jsxs(Box, { justifyContent: "space-between", children: [clean.length > 0 ? (_jsxs(Text, { children: [chalk.green("\u2713"), " ", chalk.green.bold(String(clean.length)), " ", chalk.dim(`package${clean.length !== 1 ? "s" : ""} passed with score 0`), " ", chalk.dim(`\u00b7 ${(durationMs / 1000).toFixed(1)}s`)] })) : (_jsxs(Text, { dimColor: true, children: [(durationMs / 1000).toFixed(1), "s"] })), result.freeScansRemaining !== undefined && (_jsx(Text, { dimColor: true, children: "Free tier \u00B7 dg login for higher scan limits" }))] })] }), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), searchMode ? (_jsxs(Text, { children: [" ", chalk.bold.cyan("/"), " ", searchQuery, chalk.cyan("\u2588"), " ", chalk.dim("Esc clear")] })) : (_jsxs(Text, { children: [" ", allGroupCount > 0 && (_jsxs(_Fragment, { children: [chalk.bold.cyan("\u2191\u2193"), " ", chalk.dim("navigate"), " ", chalk.bold.cyan("\u23CE"), " ", chalk.dim("expand"), " ", chalk.bold.cyan("/"), " ", chalk.dim("search"), " "] })), chalk.bold.cyan("l"), " ", chalk.dim("licenses"), " ", chalk.bold.cyan("e"), " ", chalk.dim("export"), " ", onBack && _jsxs(_Fragment, { children: [chalk.bold.cyan("Esc"), " ", chalk.dim("back"), " "] }), chalk.bold.cyan("q"), " ", chalk.dim("quit"), " ", chalk.dim("\u00B7 Ctrl+C or q to exit"), exportMsg && _jsxs(_Fragment, { children: [" ", chalk.green(exportMsg)] })] }))] }));
1111
+ }), belowCount > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2193"), " ", belowCount, " more below"] }))] })] })), searchQuery && groups.length === 0 && (_jsx(Text, { dimColor: true, children: ` No flagged packages match "${searchQuery}"` })), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: [discoveredTotal !== undefined && discoveredTotal > total && (_jsxs(Text, { dimColor: true, children: ["Scanned ", total, " of ", discoveredTotal, " packages"] })), _jsxs(Box, { justifyContent: "space-between", children: [clean.length > 0 ? (_jsxs(Text, { children: [chalk.green("\u2713"), " ", chalk.green.bold(String(clean.length)), " ", chalk.dim(`package${clean.length !== 1 ? "s" : ""} passed`), " ", chalk.dim(`\u00b7 ${(durationMs / 1000).toFixed(1)}s`)] })) : (_jsxs(Text, { dimColor: true, children: [(durationMs / 1000).toFixed(1), "s"] })), result.freeScansRemaining !== undefined && (_jsx(Text, { dimColor: true, children: "Free tier \u00B7 dg login for higher scan limits" }))] })] }), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), searchMode ? (_jsxs(Text, { children: [" ", chalk.bold.cyan("/"), " ", searchQuery, chalk.cyan("\u2588"), " ", chalk.dim("Esc clear")] })) : (_jsxs(Text, { children: [" ", allGroupCount > 0 && (_jsxs(_Fragment, { children: [chalk.bold.cyan("\u2191\u2193"), " ", chalk.dim("navigate"), " ", chalk.bold.cyan("\u23CE"), " ", chalk.dim("expand"), " ", chalk.bold.cyan("/"), " ", chalk.dim("search"), " "] })), chalk.bold.cyan("l"), " ", chalk.dim("licenses"), " ", chalk.bold.cyan("e"), " ", chalk.dim("export"), " ", onBack && _jsxs(_Fragment, { children: [chalk.bold.cyan("Esc"), " ", chalk.dim("back"), " "] }), chalk.bold.cyan("q"), " ", chalk.dim("quit"), " ", chalk.dim("\u00B7 Ctrl+C or q to exit"), exportMsg && _jsxs(_Fragment, { children: [" ", chalk.green(exportMsg)] })] }))] }));
1112
1112
  };
1113
1113
  const T = {
1114
1114
  branch: chalk.dim("\u251C\u2500\u2500"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@westbayberry/dg",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "Dependency Guardian supply-chain firewall CLI",
5
5
  "type": "module",
6
6
  "bin": {