@westbayberry/dg 2.1.0 → 2.3.0
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 +216 -226
- package/dist/agents/claude-code.js +123 -0
- package/dist/agents/codex.js +65 -0
- package/dist/agents/copilot-cli.js +115 -0
- package/dist/agents/cursor.js +118 -0
- package/dist/agents/gate-posture.js +21 -0
- package/dist/agents/gemini.js +107 -0
- package/dist/agents/persistence.js +351 -0
- package/dist/agents/registry.js +128 -0
- package/dist/agents/routing.js +118 -0
- package/dist/agents/types.js +1 -0
- package/dist/agents/windsurf.js +93 -0
- package/dist/api/analyze.js +1 -1
- package/dist/audit/detectors.js +0 -11
- package/dist/audit/events.js +5 -21
- package/dist/audit/rules.js +6 -2
- package/dist/audit-ui/AuditApp.js +2 -0
- package/dist/audit-ui/components/AuditResultsView.js +55 -92
- package/dist/audit-ui/export.js +0 -4
- package/dist/audit-ui/format.js +0 -3
- package/dist/audit-ui/launch.js +10 -1
- package/dist/auth/device-login.js +4 -5
- package/dist/auth/login-app.js +7 -7
- package/dist/auth/store.js +17 -5
- package/dist/bin/dg.js +59 -52
- package/dist/commands/agents.js +275 -0
- package/dist/commands/audit.js +28 -3
- package/dist/commands/config.js +15 -7
- package/dist/commands/cooldown.js +389 -0
- package/dist/commands/decisions.js +23 -9
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/help.js +1 -1
- package/dist/commands/licenses.js +10 -22
- package/dist/commands/logout.js +4 -11
- package/dist/commands/router.js +6 -4
- package/dist/commands/sbom.js +206 -0
- package/dist/commands/scan.js +1 -1
- package/dist/commands/service.js +32 -13
- package/dist/commands/setup.js +196 -27
- package/dist/commands/types.js +1 -0
- package/dist/commands/uninstall.js +2 -1
- package/dist/commands/update.js +17 -8
- package/dist/commands/verify.js +8 -5
- package/dist/config/settings.js +49 -48
- package/dist/decisions/remember-prompt.js +5 -10
- package/dist/export-ui/ExportDialog.js +198 -0
- package/dist/install-ui/LiveInstall.js +5 -4
- package/dist/install-ui/block-render.js +18 -14
- package/dist/install-ui/prep-spinner.js +32 -0
- package/dist/launcher/agent-check.js +896 -0
- package/dist/launcher/agent-hook-exec.js +70 -0
- package/dist/launcher/agent-hook-io.js +39 -0
- package/dist/launcher/cargo-cache.js +40 -0
- package/dist/launcher/classify.js +44 -7
- package/dist/launcher/env.js +105 -26
- package/dist/launcher/install-preflight.js +64 -18
- package/dist/launcher/live-install.js +29 -8
- package/dist/launcher/manifest-screen.js +171 -0
- package/dist/launcher/output-redaction.js +10 -4
- package/dist/launcher/preflight-prompt.js +18 -4
- package/dist/launcher/run.js +178 -103
- package/dist/launcher/spawn-invocation.js +21 -0
- package/dist/policy/cooldown.js +18 -5
- package/dist/policy/evaluate.js +5 -6
- package/dist/policy/pypi-name.js +17 -0
- package/dist/presentation/mode.js +3 -2
- package/dist/presentation/package-page.js +9 -0
- package/dist/presentation/theme.js +7 -7
- package/dist/project/dgfile.js +146 -6
- package/dist/project/override-trust.js +0 -0
- package/dist/proxy/auth.js +42 -0
- package/dist/proxy/ca.js +29 -9
- package/dist/proxy/cooldown-exemptions-file.js +33 -0
- package/dist/proxy/enforcement.js +55 -16
- package/dist/proxy/metadata-map.js +66 -12
- package/dist/proxy/preverified.js +55 -0
- package/dist/proxy/server.js +568 -53
- package/dist/proxy/worker.js +16 -1
- package/dist/publish-set/collect.js +1 -4
- package/dist/publish-set/npm.js +8 -5
- package/dist/publish-set/pack.js +9 -3
- package/dist/runtime/cli.js +0 -4
- package/dist/runtime/fatal.js +31 -0
- package/dist/runtime/first-run.js +14 -12
- package/dist/runtime/node-version.js +43 -6
- package/dist/runtime/nudges.js +35 -2
- package/dist/sbom/cyclonedx.js +211 -0
- package/dist/sbom-ui/SbomApp.js +158 -0
- package/dist/sbom-ui/components/SbomHeader.js +32 -0
- package/dist/sbom-ui/components/SbomList.js +52 -0
- package/dist/sbom-ui/inventory.js +132 -0
- package/dist/sbom-ui/launch.js +51 -0
- package/dist/sbom-ui/run.js +55 -0
- package/dist/sbom-ui/store.js +26 -0
- package/dist/scan/collect.js +10 -6
- package/dist/scan/command.js +17 -10
- package/dist/scan/discovery.js +11 -2
- package/dist/scan/render.js +28 -4
- package/dist/scan/scanner-report.js +15 -9
- package/dist/scan/staged.js +33 -9
- package/dist/scan-ui/LegacyApp.js +9 -13
- package/dist/scan-ui/alt-screen.js +5 -8
- package/dist/scan-ui/components/InteractiveResultsView.js +133 -120
- package/dist/scan-ui/components/ProgressBar.js +3 -14
- package/dist/scan-ui/components/ProjectSelector.js +1 -1
- package/dist/scan-ui/components/ScoreHeader.js +2 -3
- package/dist/scan-ui/components/SetupBanner.js +0 -6
- package/dist/scan-ui/format-helpers.js +61 -5
- package/dist/scan-ui/hooks/useResizeRepaint.js +25 -0
- package/dist/scan-ui/hooks/useScan.js +19 -2
- package/dist/scan-ui/launch.js +7 -4
- package/dist/scan-ui/shims.js +11 -4
- package/dist/scripts/detect.js +7 -2
- package/dist/scripts/gate.js +1 -1
- package/dist/security/sanitize.js +8 -4
- package/dist/service/state.js +28 -9
- package/dist/service/trust-refresh.js +92 -0
- package/dist/service/trust-store.js +5 -9
- package/dist/service/worker.js +23 -1
- package/dist/setup/activate-shell.js +28 -0
- package/dist/setup/git-hook.js +49 -4
- package/dist/setup/plan.js +227 -49
- package/dist/setup/uninstall-standalone.js +25 -0
- package/dist/setup-ui/gate.js +39 -0
- package/dist/setup-ui/offer.js +42 -0
- package/dist/setup-ui/selector.js +27 -0
- package/dist/setup-ui/tasks.js +56 -0
- package/dist/setup-ui/wizard.js +233 -0
- package/dist/standalone/uninstall.mjs +2123 -0
- package/dist/state/cooldown-held.js +66 -0
- package/dist/state/index.js +1 -0
- package/dist/state/locks.js +4 -2
- package/dist/state/store.js +2 -1
- package/dist/util/external-tool.js +25 -0
- package/dist/util/git.js +10 -3
- package/dist/util/report-writer.js +57 -0
- package/dist/verify/local.js +242 -44
- package/dist/verify/package-check.js +87 -29
- package/dist/verify/preflight.js +242 -49
- package/dist/verify/render.js +15 -1
- package/npm-shrinkwrap.json +2383 -0
- package/package.json +7 -8
- package/NOTICE +0 -5
- package/dist/commands/completion.js +0 -117
- package/dist/commands/explain.js +0 -236
- package/dist/commands/unavailable.js +0 -11
- package/dist/scripts/rebuild.js +0 -28
- package/dist/telemetry/events.js +0 -40
|
@@ -4,11 +4,7 @@ import { Text, Box } from "ink";
|
|
|
4
4
|
import InkSpinner from "ink-spinner";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { useTerminalSize } from "../hooks/useTerminalSize.js";
|
|
7
|
-
//
|
|
8
|
-
// `value` and `total` are the same numbers the bar renders; `elapsedSeconds` is
|
|
9
|
-
// the wall-clock since the bar mounted. Returns null fields when there's not
|
|
10
|
-
// enough signal yet (value == 0 or elapsed == 0) so the UI can show
|
|
11
|
-
// "calculating…" instead of a misleading number.
|
|
7
|
+
// Null fields until value/elapsed are non-zero so the UI shows "calculating…".
|
|
12
8
|
export function computeEta(value, total, elapsedSeconds) {
|
|
13
9
|
if (value <= 0 || elapsedSeconds <= 0 || total <= 0) {
|
|
14
10
|
return { remainingSeconds: null, ratePerSec: null };
|
|
@@ -68,17 +64,10 @@ export const ProgressBar = ({ value, total, label, }) => {
|
|
|
68
64
|
timeInfo = `${formatTime(elapsed)} elapsed · ~${formatTime(eta.remainingSeconds)} left${rateInline}`;
|
|
69
65
|
}
|
|
70
66
|
const counter = `${value}/${total} ${percent}%`;
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
// break Ink's line-count tracker, leaving stale lines
|
|
74
|
-
// above on the next redraw).
|
|
75
|
-
// 30-59 → narrow bar (no elapsed-time tail beside the spinner).
|
|
76
|
-
// >= 60 → full bar + tail.
|
|
67
|
+
// Below 30 cols the bar would soft-wrap and break Ink's line-count
|
|
68
|
+
// tracker, leaving stale lines above on the next redraw.
|
|
77
69
|
const compact = termWidth < 30;
|
|
78
70
|
const narrow = !compact && termWidth < 60;
|
|
79
|
-
// Reserve: 4 indent + counter length + 2 padding. Floor at 6 cols so
|
|
80
|
-
// even on a borderline-narrow terminal we draw SOMETHING; if there's
|
|
81
|
-
// no room at all we fall through to compact mode above.
|
|
82
71
|
const barWidth = Math.max(6, termWidth - counter.length - 8);
|
|
83
72
|
const fraction = total > 0 ? Math.min(1, value / total) : 0;
|
|
84
73
|
const filled = Math.round(fraction * barWidth);
|
|
@@ -42,7 +42,7 @@ export const ProjectSelector = ({ projects, onConfirm, onCancel, userStatus }) =
|
|
|
42
42
|
}
|
|
43
43
|
});
|
|
44
44
|
const ecosystemLabel = (eco) => eco === "npm" ? chalk.magenta("npm") : chalk.blue("pip");
|
|
45
|
-
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsxs(Text, { children: [chalk.bold("Dependency Guardian"), " ", userStatus
|
|
45
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsxs(Text, { children: [chalk.bold("Dependency Guardian"), " ", userStatus ?? ""] }), _jsx(Text, { children: "" }), _jsxs(Text, { bold: true, children: ["Found ", projects.length, " project", projects.length !== 1 ? "s" : ""] }), _jsx(Text, { children: "" }), projects.map((proj, i) => {
|
|
46
46
|
const isCursor = i === cursor;
|
|
47
47
|
const isSelected = selected.has(i);
|
|
48
48
|
const prefix = isCursor ? chalk.cyan("\u258C") : " ";
|
|
@@ -13,10 +13,9 @@ function scoreColor(score, action) {
|
|
|
13
13
|
chalk.green.bold;
|
|
14
14
|
return colorFn(String(score));
|
|
15
15
|
}
|
|
16
|
-
export const ScoreHeader = ({ score, action, total, flagged, clean, userStatus, scanUsage, usageNearLimit, }) => {
|
|
16
|
+
export const ScoreHeader = ({ score, action, compact, total, flagged, clean, userStatus, scanUsage, usageNearLimit, }) => {
|
|
17
17
|
const logo = renderLogo(action);
|
|
18
|
-
const { cols
|
|
19
|
-
const compact = rows < COMPACT_ROWS;
|
|
18
|
+
const { cols } = useTerminalSize();
|
|
20
19
|
const showLogo = cols >= LOGO_MIN_COLS && !compact;
|
|
21
20
|
return (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: action === "block" ? "red" : action === "warn" ? "yellow" : action === "analysis_incomplete" ? "cyan" : "green", paddingLeft: 1, paddingRight: 1, width: "100%", children: _jsxs(Box, { flexDirection: "row", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Text, { bold: true, children: ["Dependency Guardian ", userStatus ?? ""] }), !compact && _jsx(Text, { children: " " }), _jsxs(Text, { children: [chalk.dim("Score"), " ", scoreColor(score, action)] }), total !== undefined && (_jsxs(_Fragment, { children: [!compact && _jsx(Text, { children: " " }), _jsxs(Text, { children: [chalk.dim(`${total} package${total !== 1 ? "s" : ""} scanned`), flagged !== undefined && flagged > 0 ? (_jsxs(_Fragment, { children: [" ", chalk.yellow(`${flagged} flagged`), " ", chalk.green(`${clean ?? 0} clean`)] })) : (_jsxs(_Fragment, { children: [" ", chalk.green("all clean")] }))] }), scanUsage && (usageNearLimit
|
|
22
21
|
? _jsxs(Text, { color: "yellow", children: [scanUsage, " \u2191 Pro: westbayberry.com/pricing"] })
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
// Visible alert shown above the scan results when the user's DG setup has
|
|
4
|
-
// gaps — missing api key, missing pre-commit hook, etc. Renders nothing
|
|
5
|
-
// when issues is empty so it's free to drop in unconditionally.
|
|
6
|
-
//
|
|
7
|
-
// Yellow border, single line per issue, fix command in cyan so the user
|
|
8
|
-
// can copy-paste it.
|
|
9
3
|
export const SetupBanner = ({ issues }) => {
|
|
10
4
|
if (issues.length === 0)
|
|
11
5
|
return null;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { sanitize } from "../security/sanitize.js";
|
|
1
3
|
export const USAGE_NEAR_LIMIT_RATIO = 0.8;
|
|
2
|
-
// Renders the monthly package-usage readout shown on every scan. `nearLimit`
|
|
3
|
-
// drives the dim → yellow + upgrade-nudge escalation; it's only ever true for
|
|
4
|
-
// a finite limit at/above 80% used.
|
|
5
4
|
export function formatUsage(usage) {
|
|
6
5
|
const used = usage.used.toLocaleString();
|
|
7
6
|
if (usage.limit === null) {
|
|
@@ -12,11 +11,68 @@ export function formatUsage(usage) {
|
|
|
12
11
|
nearLimit: usage.used / usage.limit >= USAGE_NEAR_LIMIT_RATIO,
|
|
13
12
|
};
|
|
14
13
|
}
|
|
14
|
+
export function formatAccountStatus(tier, loggedIn, name) {
|
|
15
|
+
const cleaned = sanitize(tier).trim().slice(0, 24).toLowerCase();
|
|
16
|
+
const label = cleaned ? cleaned.charAt(0).toUpperCase() + cleaned.slice(1) : loggedIn ? "Account" : "Free";
|
|
17
|
+
if (!loggedIn) {
|
|
18
|
+
return `${chalk.dim(`${label} · `)}${chalk.cyan.bold("dg login")}`;
|
|
19
|
+
}
|
|
20
|
+
const plan = (cleaned === "free" ? chalk.yellow : chalk.green)(`${label} plan`);
|
|
21
|
+
const who = name ? sanitize(name).trim().slice(0, 40) : "";
|
|
22
|
+
return who ? `${chalk.dim(`${who} · `)}${plan}` : plan;
|
|
23
|
+
}
|
|
24
|
+
function isWideCodePoint(cp) {
|
|
25
|
+
return (cp >= 0x1100 &&
|
|
26
|
+
(cp <= 0x115f ||
|
|
27
|
+
cp === 0x2329 ||
|
|
28
|
+
cp === 0x232a ||
|
|
29
|
+
(cp >= 0x2e80 && cp <= 0xa4cf && cp !== 0x303f) ||
|
|
30
|
+
(cp >= 0xac00 && cp <= 0xd7a3) ||
|
|
31
|
+
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
32
|
+
(cp >= 0xfe10 && cp <= 0xfe19) ||
|
|
33
|
+
(cp >= 0xfe30 && cp <= 0xfe6b) ||
|
|
34
|
+
(cp >= 0xff00 && cp <= 0xff60) ||
|
|
35
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
36
|
+
(cp >= 0x1f300 && cp <= 0x1f64f) ||
|
|
37
|
+
(cp >= 0x1f900 && cp <= 0x1f9ff) ||
|
|
38
|
+
(cp >= 0x20000 && cp <= 0x3fffd)));
|
|
39
|
+
}
|
|
40
|
+
function isZeroWidthCodePoint(cp) {
|
|
41
|
+
return ((cp >= 0x0300 && cp <= 0x036f) ||
|
|
42
|
+
(cp >= 0x1ab0 && cp <= 0x1aff) ||
|
|
43
|
+
(cp >= 0x20d0 && cp <= 0x20ff) ||
|
|
44
|
+
(cp >= 0xfe00 && cp <= 0xfe0f) ||
|
|
45
|
+
(cp >= 0x200b && cp <= 0x200f) ||
|
|
46
|
+
cp === 0xfeff);
|
|
47
|
+
}
|
|
48
|
+
function codePointWidth(cp) {
|
|
49
|
+
if (isZeroWidthCodePoint(cp))
|
|
50
|
+
return 0;
|
|
51
|
+
return isWideCodePoint(cp) ? 2 : 1;
|
|
52
|
+
}
|
|
53
|
+
export function displayWidth(s) {
|
|
54
|
+
let width = 0;
|
|
55
|
+
for (const ch of s) {
|
|
56
|
+
width += codePointWidth(ch.codePointAt(0) ?? 0);
|
|
57
|
+
}
|
|
58
|
+
return width;
|
|
59
|
+
}
|
|
15
60
|
export function pad(s, len) {
|
|
16
|
-
return s + " ".repeat(Math.max(0, len - s
|
|
61
|
+
return s + " ".repeat(Math.max(0, len - displayWidth(s)));
|
|
17
62
|
}
|
|
18
63
|
export function truncate(s, max) {
|
|
19
|
-
|
|
64
|
+
if (displayWidth(s) <= max)
|
|
65
|
+
return s;
|
|
66
|
+
let out = "";
|
|
67
|
+
let width = 0;
|
|
68
|
+
for (const ch of s) {
|
|
69
|
+
const w = codePointWidth(ch.codePointAt(0) ?? 0);
|
|
70
|
+
if (width + w > max - 1)
|
|
71
|
+
break;
|
|
72
|
+
out += ch;
|
|
73
|
+
width += w;
|
|
74
|
+
}
|
|
75
|
+
return out + "…";
|
|
20
76
|
}
|
|
21
77
|
export function groupPackages(packages, keyBy = "name") {
|
|
22
78
|
const map = new Map();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useStdout } from "ink";
|
|
3
|
+
import { CLEAR_HOME } from "../alt-screen.js";
|
|
4
|
+
// Ink skips repainting when output matches its memoized last frame, so a bare
|
|
5
|
+
// screen clear on resize leaves the TUI blank until the next state change.
|
|
6
|
+
// useStdout().write is the only public path that erases and repaints the
|
|
7
|
+
// memoized frame unconditionally; CLEAR_HOME first wipes resize artifacts.
|
|
8
|
+
export function useResizeRepaint() {
|
|
9
|
+
const { stdout, write } = useStdout();
|
|
10
|
+
const [resizes, setResizes] = useState(0);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const handle = () => setResizes((count) => count + 1);
|
|
13
|
+
stdout.setMaxListeners(stdout.getMaxListeners() + 1);
|
|
14
|
+
stdout.on("resize", handle);
|
|
15
|
+
return () => {
|
|
16
|
+
stdout.off("resize", handle);
|
|
17
|
+
stdout.setMaxListeners(Math.max(0, stdout.getMaxListeners() - 1));
|
|
18
|
+
};
|
|
19
|
+
}, [stdout]);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (resizes === 0)
|
|
22
|
+
return;
|
|
23
|
+
write(CLEAR_HOME);
|
|
24
|
+
}, [resizes, write]);
|
|
25
|
+
}
|
|
@@ -53,9 +53,10 @@ export function useScan(config) {
|
|
|
53
53
|
}
|
|
54
54
|
started.current = true;
|
|
55
55
|
void (async () => {
|
|
56
|
-
const
|
|
56
|
+
const discovered = await discoverScanProjectsAsync(process.cwd(), (progress) => {
|
|
57
57
|
dispatch({ type: "DISCOVERY_PROGRESS", path: progress.path, found: progress.found });
|
|
58
58
|
});
|
|
59
|
+
const projects = discovered.filter((project) => project.ecosystem === "npm" || project.ecosystem === "pypi");
|
|
59
60
|
if (projects.length === 0) {
|
|
60
61
|
dispatch({ type: "DISCOVERY_EMPTY", message: "No dependency lockfiles found." });
|
|
61
62
|
return;
|
|
@@ -159,7 +160,23 @@ async function scanProjects(projects, dispatch, signal) {
|
|
|
159
160
|
const firstFailure = outcomes.find((outcome) => "error" in outcome);
|
|
160
161
|
if (responses.length > 0) {
|
|
161
162
|
const merged = mergeAnalyzeResponses(responses);
|
|
162
|
-
|
|
163
|
+
// A failed ecosystem means its verdict is unknown; escalate anything short of
|
|
164
|
+
// a confirmed block to analysis_incomplete so a warn from the succeeded
|
|
165
|
+
// ecosystem does not present as the whole verdict while the other is unscanned.
|
|
166
|
+
const base = firstFailure && merged.action !== "block" ? { ...merged, action: "analysis_incomplete" } : merged;
|
|
167
|
+
const ecoByKey = new Map();
|
|
168
|
+
for (const [ecosystem, packages] of entries) {
|
|
169
|
+
for (const pkg of packages) {
|
|
170
|
+
ecoByKey.set(packageKey(pkg.name, pkg.version), ecosystem);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const result = {
|
|
174
|
+
...base,
|
|
175
|
+
packages: base.packages.map((pkg) => {
|
|
176
|
+
const ecosystem = ecoByKey.get(packageKey(pkg.name, pkg.version));
|
|
177
|
+
return ecosystem ? { ...pkg, ecosystem } : pkg;
|
|
178
|
+
})
|
|
179
|
+
};
|
|
163
180
|
const decisions = computeProjectDecisions(result, entries);
|
|
164
181
|
dispatch({
|
|
165
182
|
type: "SCAN_COMPLETE",
|
package/dist/scan-ui/launch.js
CHANGED
|
@@ -12,11 +12,15 @@ export function shouldLaunchScanTui(options) {
|
|
|
12
12
|
if (process.env.TERM === "dumb") {
|
|
13
13
|
return false;
|
|
14
14
|
}
|
|
15
|
+
if (!process.stdin.isTTY) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
15
18
|
return resolvePresentation().mode === "rich";
|
|
16
19
|
}
|
|
17
20
|
export async function launchScanTui(initialView = "results") {
|
|
18
21
|
const ci = process.env.CI;
|
|
19
|
-
|
|
22
|
+
const ciCleared = ci === "" || ci === "0" || ci === "false";
|
|
23
|
+
if (ciCleared) {
|
|
20
24
|
delete process.env.CI;
|
|
21
25
|
}
|
|
22
26
|
const [{ render }, react, app] = await Promise.all([
|
|
@@ -32,13 +36,12 @@ export async function launchScanTui(initialView = "results") {
|
|
|
32
36
|
: undefined;
|
|
33
37
|
enterTui();
|
|
34
38
|
const instance = render(react.default.createElement(app.App, { config, initialView, updateAvailable }), { exitOnCtrlC: true });
|
|
35
|
-
const clearStaleFrameOnResize = () => instance.clear();
|
|
36
|
-
process.stdout.on("resize", clearStaleFrameOnResize);
|
|
37
39
|
try {
|
|
38
40
|
await instance.waitUntilExit();
|
|
39
41
|
}
|
|
40
42
|
finally {
|
|
41
|
-
|
|
43
|
+
if (ciCleared)
|
|
44
|
+
process.env.CI = ci;
|
|
42
45
|
leaveTui();
|
|
43
46
|
}
|
|
44
47
|
}
|
package/dist/scan-ui/shims.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { authStatus } from "../auth/store.js";
|
|
2
|
+
import { formatAccountStatus } from "./format-helpers.js";
|
|
2
3
|
export function isLoggedIn() {
|
|
3
4
|
try {
|
|
4
5
|
return authStatus().authenticated;
|
|
@@ -7,14 +8,20 @@ export function isLoggedIn() {
|
|
|
7
8
|
return false;
|
|
8
9
|
}
|
|
9
10
|
}
|
|
10
|
-
export function
|
|
11
|
+
export function accountHeaderLine(usageTier, env = process.env) {
|
|
12
|
+
let loggedIn = false;
|
|
13
|
+
let name;
|
|
14
|
+
let storedTier;
|
|
11
15
|
try {
|
|
12
|
-
const status = authStatus();
|
|
13
|
-
|
|
16
|
+
const status = authStatus(env);
|
|
17
|
+
loggedIn = status.authenticated;
|
|
18
|
+
name = status.name ?? status.email;
|
|
19
|
+
storedTier = status.tier;
|
|
14
20
|
}
|
|
15
21
|
catch {
|
|
16
|
-
|
|
22
|
+
loggedIn = false;
|
|
17
23
|
}
|
|
24
|
+
return formatAccountStatus(usageTier ?? storedTier ?? "", loggedIn, name);
|
|
18
25
|
}
|
|
19
26
|
export function effectiveScanAction(raw, effective, mode) {
|
|
20
27
|
return mode === "strict" || effective === undefined ? raw : effective;
|
package/dist/scripts/detect.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
const LIFECYCLE_HOOKS = ["preinstall", "install", "postinstall"];
|
|
5
5
|
export function computeScriptsHash(scripts, hasGyp) {
|
|
6
6
|
const canonical = JSON.stringify({
|
|
@@ -74,7 +74,12 @@ function wantersFromHiddenLockfile(projectDir, nodeModules) {
|
|
|
74
74
|
if (!isPlainObject(entry) || entry.hasInstallScript !== true || !packagePath.startsWith("node_modules/")) {
|
|
75
75
|
continue;
|
|
76
76
|
}
|
|
77
|
-
const
|
|
77
|
+
const manifestDir = resolve(projectDir, packagePath);
|
|
78
|
+
const containment = relative(resolve(nodeModules), manifestDir);
|
|
79
|
+
if (containment === "" || containment.startsWith("..") || isAbsolute(containment)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const wanter = wanterFromManifestDir(manifestDir);
|
|
78
83
|
if (wanter) {
|
|
79
84
|
wanters.push(wanter);
|
|
80
85
|
}
|
package/dist/scripts/gate.js
CHANGED
|
@@ -123,7 +123,7 @@ export function scriptGateReportLine(options) {
|
|
|
123
123
|
}
|
|
124
124
|
const names = wanters.map((wanter) => (wanter.version ? `${wanter.name}@${wanter.version}` : wanter.name));
|
|
125
125
|
const noun = wanters.length === 1 ? "package ran" : "packages ran";
|
|
126
|
-
return `\n ${theme.paint("muted", `dg scripts: ${wanters.length} ${noun} install scripts (${formatNames(names)}) —
|
|
126
|
+
return `\n ${theme.paint("muted", `dg scripts: ${wanters.length} ${noun} install scripts (${formatNames(names)}) — observed, not blocked · silence: dg config set scriptGate.mode off`)}\n`;
|
|
127
127
|
}
|
|
128
128
|
function formatNames(names) {
|
|
129
129
|
if (names.length <= REPORTED_NAME_LIMIT) {
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
const CTRL_KEEP_NEWLINE = /[\x00-\x08\x0B-\x1F\x7F-\x9F]/g;
|
|
3
|
+
const CTRL_ALL = /[\x00-\x1F\x7F-\x9F]/g;
|
|
2
4
|
export function sanitize(s) {
|
|
3
|
-
return stripVTControlCharacters(s);
|
|
5
|
+
return stripVTControlCharacters(s).replace(CTRL_KEEP_NEWLINE, "");
|
|
6
|
+
}
|
|
7
|
+
export function sanitizeLine(s) {
|
|
8
|
+
return stripVTControlCharacters(s).replace(/[\r\n]+/g, " ").replace(CTRL_ALL, "");
|
|
4
9
|
}
|
|
5
10
|
export function sanitizeDeep(value) {
|
|
6
11
|
if (typeof value === "string") {
|
|
7
|
-
return
|
|
12
|
+
return sanitize(value);
|
|
8
13
|
}
|
|
9
14
|
if (value === null || value === undefined) {
|
|
10
15
|
return value;
|
|
@@ -16,8 +21,7 @@ export function sanitizeDeep(value) {
|
|
|
16
21
|
const src = value;
|
|
17
22
|
const out = {};
|
|
18
23
|
for (const k of Object.keys(src)) {
|
|
19
|
-
|
|
20
|
-
out[cleanKey] = sanitizeDeep(src[k]);
|
|
24
|
+
out[sanitizeLine(k)] = sanitizeDeep(src[k]);
|
|
21
25
|
}
|
|
22
26
|
return out;
|
|
23
27
|
}
|
package/dist/service/state.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { loadUserConfig } from "../config/settings.js";
|
|
7
7
|
import { applyTrustInstall, applyTrustUninstall, readCertificateFingerprints, readServiceTrustRecord, renderTrustStorePlanLines, resolveTrustInstallPlan, TrustStoreError, TrustToolMissingError, writeServiceTrustRecord } from "./trust-store.js";
|
|
8
|
+
import { clearTrustRefreshError, readTrustRefreshError } from "./trust-refresh.js";
|
|
8
9
|
import { acquireLockSync, cleanupSessionSync, createSessionSync, preserveCorruptCleanupRegistrySync, resolveDgPaths, CLEANUP_REGISTRY_LOCK } from "../state/index.js";
|
|
9
10
|
export const SERVICE_SENTINEL = "dg-service-mode-v1";
|
|
10
11
|
export const TRUST_SENTINEL = "dg-service-trust-v1";
|
|
@@ -292,6 +293,7 @@ export function installServiceTrust(env, now = new Date()) {
|
|
|
292
293
|
const plan = trustStoreOperation(() => resolveTrustInstallPlan(runtime.caPath, env ?? process.env));
|
|
293
294
|
const existing = readServiceTrustRecord(paths.trustRecordPath, TRUST_SENTINEL);
|
|
294
295
|
if (existing && existing.fingerprintSha256 === plan.fingerprintSha256 && existing.provider === plan.provider && existing.target === plan.target) {
|
|
296
|
+
clearTrustRefreshError(paths.serviceDir);
|
|
295
297
|
const state = {
|
|
296
298
|
...current,
|
|
297
299
|
trustInstalled: true,
|
|
@@ -311,6 +313,7 @@ export function installServiceTrust(env, now = new Date()) {
|
|
|
311
313
|
trustStoreOperation(() => applyTrustUninstall(existing));
|
|
312
314
|
}
|
|
313
315
|
const record = trustStoreOperation(() => applyTrustInstall(plan, now, TRUST_SENTINEL));
|
|
316
|
+
clearTrustRefreshError(paths.serviceDir);
|
|
314
317
|
const state = {
|
|
315
318
|
...current,
|
|
316
319
|
trustInstalled: true,
|
|
@@ -339,6 +342,7 @@ export function uninstallServiceTrust(env, now = new Date()) {
|
|
|
339
342
|
if (record) {
|
|
340
343
|
trustStoreOperation(() => applyTrustUninstall(record));
|
|
341
344
|
}
|
|
345
|
+
clearTrustRefreshError(paths.serviceDir);
|
|
342
346
|
const removedTrust = removeFile(paths.trustRecordPath);
|
|
343
347
|
if (!current.configured && !removedTrust) {
|
|
344
348
|
return {
|
|
@@ -462,7 +466,7 @@ function trustStoreOperation(operation) {
|
|
|
462
466
|
throw error;
|
|
463
467
|
}
|
|
464
468
|
}
|
|
465
|
-
function withServiceLock(paths, run) {
|
|
469
|
+
export function withServiceLock(paths, run) {
|
|
466
470
|
const lock = acquireLockSync(paths.paths, SERVICE_LOCK, {
|
|
467
471
|
staleMs: SERVICE_LOCK_STALE_MS
|
|
468
472
|
});
|
|
@@ -518,31 +522,42 @@ function readState(paths) {
|
|
|
518
522
|
function applyRuntimeDiagnostics(paths, state) {
|
|
519
523
|
const runtime = readRuntime(paths) ?? state.proxy;
|
|
520
524
|
if (!runtime) {
|
|
521
|
-
return state;
|
|
525
|
+
return withTrustRefreshError(paths, state);
|
|
522
526
|
}
|
|
523
527
|
const drift = detectTrustDrift(state.trust, runtime);
|
|
524
528
|
if (!state.running) {
|
|
525
|
-
return {
|
|
529
|
+
return withTrustRefreshError(paths, {
|
|
526
530
|
...state,
|
|
527
531
|
proxy: runtime,
|
|
528
532
|
trustDrift: drift,
|
|
529
533
|
lastError: "stale service runtime state: runtime file exists while service is stopped"
|
|
530
|
-
};
|
|
534
|
+
});
|
|
531
535
|
}
|
|
532
536
|
const health = runtimeHealth(runtime);
|
|
533
537
|
if (!health.healthy) {
|
|
534
|
-
return {
|
|
538
|
+
return withTrustRefreshError(paths, {
|
|
535
539
|
...state,
|
|
536
540
|
running: false,
|
|
537
541
|
proxy: runtime,
|
|
538
542
|
trustDrift: drift,
|
|
539
543
|
lastError: `stale service runtime state: ${health.reason}`
|
|
540
|
-
};
|
|
544
|
+
});
|
|
541
545
|
}
|
|
542
|
-
return {
|
|
546
|
+
return withTrustRefreshError(paths, {
|
|
543
547
|
...state,
|
|
544
548
|
proxy: runtime,
|
|
545
549
|
trustDrift: drift
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
function withTrustRefreshError(paths, state) {
|
|
553
|
+
const refreshError = readTrustRefreshError(paths.serviceDir);
|
|
554
|
+
if (!refreshError) {
|
|
555
|
+
return state;
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
...state,
|
|
559
|
+
lastError: state.lastError
|
|
560
|
+
?? `service CA trust refresh failed at ${refreshError.at}: ${refreshError.message}. Run 'dg service trust install --yes' to repair OS trust for the rotated CA.`
|
|
546
561
|
};
|
|
547
562
|
}
|
|
548
563
|
function runtimeHealth(runtime) {
|
|
@@ -711,7 +726,7 @@ function cleanupRuntime(paths, runtime) {
|
|
|
711
726
|
if (runtime?.pid && processIsAlive(runtime.pid)) {
|
|
712
727
|
killProcess(runtime.pid);
|
|
713
728
|
}
|
|
714
|
-
if (runtime?.sessionDir &&
|
|
729
|
+
if (runtime?.sessionDir && isContainedIn(paths.paths.sessionsDir, runtime.sessionDir)) {
|
|
715
730
|
rmSync(runtime.sessionDir, {
|
|
716
731
|
force: true,
|
|
717
732
|
recursive: true
|
|
@@ -719,6 +734,10 @@ function cleanupRuntime(paths, runtime) {
|
|
|
719
734
|
}
|
|
720
735
|
removeFile(paths.runtimePath);
|
|
721
736
|
}
|
|
737
|
+
function isContainedIn(parent, child) {
|
|
738
|
+
const related = relative(parent, child);
|
|
739
|
+
return related.length > 0 && !related.startsWith("..") && !isAbsolute(related);
|
|
740
|
+
}
|
|
722
741
|
function processIsAlive(pid) {
|
|
723
742
|
if (pid <= 0) {
|
|
724
743
|
return false;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { applyTrustInstall, applyTrustUninstall, readServiceTrustRecord, resolveTrustInstallPlan, writeServiceTrustRecord } from "./trust-store.js";
|
|
4
|
+
export const TRUST_REFRESH_ERROR_FILENAME = "trust-refresh-error.json";
|
|
5
|
+
export function trustRefreshErrorPath(serviceDir) {
|
|
6
|
+
return join(serviceDir, TRUST_REFRESH_ERROR_FILENAME);
|
|
7
|
+
}
|
|
8
|
+
export function readTrustRefreshError(serviceDir) {
|
|
9
|
+
try {
|
|
10
|
+
const path = trustRefreshErrorPath(serviceDir);
|
|
11
|
+
if (!existsSync(path)) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
15
|
+
if (typeof parsed.at !== "string" || typeof parsed.message !== "string") {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
at: parsed.at,
|
|
20
|
+
message: parsed.message
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function clearTrustRefreshError(serviceDir) {
|
|
28
|
+
rmSync(trustRefreshErrorPath(serviceDir), {
|
|
29
|
+
force: true
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function refreshServiceTrustAfterCaRotation(options) {
|
|
33
|
+
const record = readServiceTrustRecord(options.trustRecordPath, options.sentinel);
|
|
34
|
+
if (!record) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const plan = resolveTrustInstallPlan(options.caPath, options.env);
|
|
39
|
+
applyTrustUninstall(record);
|
|
40
|
+
const next = applyTrustInstall(plan, options.now ?? new Date(), options.sentinel);
|
|
41
|
+
sweepStaleDgTrustFiles(next.provider, next.target);
|
|
42
|
+
writeServiceTrustRecord(options.trustRecordPath, next);
|
|
43
|
+
clearTrustRefreshError(options.serviceDir);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
writeTrustRefreshError(options.serviceDir, error, options.now ?? new Date());
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function sweepStaleDgTrustFiles(provider, targetPath) {
|
|
50
|
+
if (provider === "darwin-user-keychain") {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const dir = dirname(targetPath);
|
|
54
|
+
let entries;
|
|
55
|
+
try {
|
|
56
|
+
entries = readdirSync(dir);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (!/^dependency-guardian-[0-9a-f]{16}\.(pem|crt)$/.test(entry)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const path = join(dir, entry);
|
|
66
|
+
if (path === targetPath) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
rmSync(path, {
|
|
70
|
+
force: true
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function writeTrustRefreshError(serviceDir, error, now) {
|
|
75
|
+
try {
|
|
76
|
+
mkdirSync(serviceDir, {
|
|
77
|
+
recursive: true,
|
|
78
|
+
mode: 0o700
|
|
79
|
+
});
|
|
80
|
+
const record = {
|
|
81
|
+
at: now.toISOString(),
|
|
82
|
+
message: error instanceof Error ? error.message : String(error)
|
|
83
|
+
};
|
|
84
|
+
writeFileSync(trustRefreshErrorPath(serviceDir), `${JSON.stringify(record, null, 2)}\n`, {
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
mode: 0o600
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { X509Certificate } from "node:crypto";
|
|
2
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { writeJsonAtomic } from "../util/json-file.js";
|
|
6
7
|
export class TrustStoreError extends Error {
|
|
7
8
|
constructor(message) {
|
|
8
9
|
super(message);
|
|
@@ -191,14 +192,9 @@ export function readCertificateFingerprints(path) {
|
|
|
191
192
|
return readCertificateInfo(path);
|
|
192
193
|
}
|
|
193
194
|
export function writeServiceTrustRecord(path, record) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
});
|
|
198
|
-
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, {
|
|
199
|
-
encoding: "utf8",
|
|
200
|
-
mode: 0o600
|
|
201
|
-
});
|
|
195
|
+
// Atomic write so a torn write can't drop the trustInstalled state while the OS
|
|
196
|
+
// still trusts the CA.
|
|
197
|
+
writeJsonAtomic(path, record, { fileMode: 0o600, dirMode: 0o700 });
|
|
202
198
|
}
|
|
203
199
|
function readCertificateInfo(path) {
|
|
204
200
|
try {
|
package/dist/service/worker.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { startProductionHttpProxy } from "../proxy/server.js";
|
|
4
|
+
import { resolveServicePaths, TRUST_SENTINEL, withServiceLock } from "./state.js";
|
|
5
|
+
import { refreshServiceTrustAfterCaRotation } from "./trust-refresh.js";
|
|
4
6
|
const sessionPath = process.argv[2];
|
|
5
7
|
const apiBaseUrl = process.argv[3];
|
|
6
8
|
const runtimePath = process.argv[4];
|
|
@@ -37,11 +39,31 @@ process.on("SIGTERM", () => {
|
|
|
37
39
|
process.on("SIGINT", () => {
|
|
38
40
|
close().finally(() => process.exit(0));
|
|
39
41
|
});
|
|
42
|
+
function exitOnFatal(error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
try {
|
|
45
|
+
process.stderr.write(`dg service worker: unexpected error — ${message}\n`);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// stderr is gone; nothing left to report to.
|
|
49
|
+
}
|
|
50
|
+
close().finally(() => process.exit(1));
|
|
51
|
+
}
|
|
52
|
+
process.on("uncaughtException", exitOnFatal);
|
|
53
|
+
process.on("unhandledRejection", exitOnFatal);
|
|
54
|
+
const servicePaths = resolveServicePaths(process.env);
|
|
40
55
|
proxy = await startProductionHttpProxy({
|
|
41
56
|
session,
|
|
42
57
|
apiBaseUrl: requiredApiBaseUrl,
|
|
43
58
|
classification,
|
|
44
|
-
env: process.env
|
|
59
|
+
env: process.env,
|
|
60
|
+
onCaRotate: () => withServiceLock(servicePaths, () => refreshServiceTrustAfterCaRotation({
|
|
61
|
+
serviceDir: servicePaths.serviceDir,
|
|
62
|
+
trustRecordPath: servicePaths.trustRecordPath,
|
|
63
|
+
sentinel: TRUST_SENTINEL,
|
|
64
|
+
caPath: session.files.ca,
|
|
65
|
+
env: process.env
|
|
66
|
+
}))
|
|
45
67
|
});
|
|
46
68
|
healthServer = await startHealthServer(proxy.port);
|
|
47
69
|
const healthAddress = healthServer.address();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { isCiEnv } from "../presentation/mode.js";
|
|
4
|
+
export function activationOffer(options = {}) {
|
|
5
|
+
const env = options.env ?? process.env;
|
|
6
|
+
if (env.DG_ACTIVATED_SHELL) {
|
|
7
|
+
return "none";
|
|
8
|
+
}
|
|
9
|
+
const stdin = options.stdin ?? process.stdin;
|
|
10
|
+
const stdout = options.stdout ?? process.stdout;
|
|
11
|
+
if (!stdin.isTTY || !stdout.isTTY || isCiEnv(env)) {
|
|
12
|
+
return "hint";
|
|
13
|
+
}
|
|
14
|
+
return "prompt";
|
|
15
|
+
}
|
|
16
|
+
// dg cannot mutate the parent shell's environment, so activation nests a protected interactive child shell.
|
|
17
|
+
export function activateShell(options = {}) {
|
|
18
|
+
const env = options.env ?? process.env;
|
|
19
|
+
const spawner = options.spawner ?? defaultShellSpawner;
|
|
20
|
+
const binary = env.SHELL || "/bin/sh";
|
|
21
|
+
const name = basename(binary);
|
|
22
|
+
return spawner({
|
|
23
|
+
binary,
|
|
24
|
+
args: name === "bash" || name === "zsh" ? ["-i"] : [],
|
|
25
|
+
env: { ...env, DG_ACTIVATED_SHELL: "1" }
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const defaultShellSpawner = (request) => spawnSync(request.binary, request.args, { stdio: "inherit", env: request.env }).status ?? 0;
|