@westbayberry/dg 2.0.7 → 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
|
-
...(
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|