@westbayberry/dg 2.2.0 → 2.3.1
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/dist/agents/claude-code.js +10 -0
- package/dist/agents/codex.js +1 -1
- package/dist/agents/cursor.js +6 -1
- package/dist/agents/gate-posture.js +21 -0
- package/dist/agents/persistence.js +68 -2
- package/dist/agents/registry.js +4 -3
- package/dist/agents/routing.js +118 -0
- package/dist/agents/windsurf.js +1 -1
- package/dist/audit/rules.js +6 -2
- package/dist/auth/store.js +9 -2
- package/dist/commands/agents.js +45 -1
- package/dist/commands/audit.js +6 -0
- package/dist/commands/setup.js +1 -2
- package/dist/commands/uninstall.js +2 -1
- package/dist/config/settings.js +35 -4
- package/dist/install-ui/LiveInstall.js +3 -2
- package/dist/install-ui/block-render.js +17 -13
- package/dist/launcher/agent-check.js +455 -25
- package/dist/launcher/agent-hook-exec.js +1 -1
- package/dist/launcher/agent-hook-io.js +12 -4
- package/dist/launcher/classify.js +27 -1
- package/dist/launcher/env.js +39 -7
- package/dist/launcher/install-preflight.js +15 -6
- package/dist/launcher/live-install.js +4 -3
- package/dist/launcher/manifest-screen.js +171 -0
- package/dist/launcher/output-redaction.js +4 -1
- package/dist/launcher/preflight-prompt.js +4 -3
- package/dist/launcher/run.js +90 -18
- package/dist/project/dgfile.js +2 -1
- package/dist/project/override-trust.js +0 -0
- package/dist/proxy/metadata-map.js +29 -13
- package/dist/proxy/server.js +130 -14
- package/dist/runtime/first-run.js +2 -1
- package/dist/sbom-ui/inventory.js +5 -1
- package/dist/scan/staged.js +31 -8
- package/dist/scan-ui/hooks/useScan.js +4 -1
- package/dist/security/sanitize.js +8 -4
- package/dist/service/state.js +1 -1
- package/dist/service/trust-store.js +5 -9
- package/dist/service/worker.js +3 -3
- package/dist/setup/plan.js +156 -12
- package/dist/setup/uninstall-standalone.js +25 -0
- package/dist/setup-ui/wizard.js +9 -1
- package/dist/standalone/uninstall.mjs +2123 -0
- package/dist/verify/local.js +2 -2
- package/dist/verify/package-check.js +3 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
package/dist/setup/plan.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, delimiter, dirname, join, resolve, sep } from "node:path";
|
|
3
3
|
import { chmodSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { randomBytes } from "node:crypto";
|
|
5
6
|
import { homedir } from "node:os";
|
|
6
7
|
import { createTheme } from "../presentation/theme.js";
|
|
7
|
-
import { reverseAgentHookEntry } from "../agents/registry.js";
|
|
8
|
+
import { AGENTS, AGENT_IDS, agentLabel, resolveAgentHookContext, reverseAgentHookEntry } from "../agents/registry.js";
|
|
9
|
+
import { GATE_ENABLE_HINT, readNetworkGatePosture } from "../agents/gate-posture.js";
|
|
8
10
|
import { acquireLockSync, findStaleSessionsSync, preserveCorruptCleanupRegistrySync, resolveDgPaths, sweepStaleSessionsSync, CLEANUP_REGISTRY_LOCK } from "../state/index.js";
|
|
9
11
|
import { currentNodeVersion, isSupportedNode } from "../runtime/node-version.js";
|
|
10
12
|
import { dgVersion } from "../commands/version.js";
|
|
11
13
|
import { compareVersions, readLatestVersion } from "../commands/update.js";
|
|
12
14
|
import { AuthError, authStatus, displayTier, readAuthState } from "../auth/store.js";
|
|
13
|
-
import { ConfigError, loadUserConfig } from "../config/settings.js";
|
|
15
|
+
import { ConfigError, DEFAULT_CONFIG, loadUserConfig } from "../config/settings.js";
|
|
14
16
|
import { describeCooldownSettings } from "../policy/cooldown.js";
|
|
15
17
|
import { resolveRealBinary } from "../launcher/resolve-real-binary.js";
|
|
16
18
|
import { packageManagerNames } from "../launcher/classify.js";
|
|
17
19
|
import { readServiceState } from "../service/state.js";
|
|
18
20
|
import { OPTIONAL_SUPPORT_GATES } from "./optional-support.js";
|
|
19
|
-
export const SHIM_COMMANDS = Object.freeze(["npm", "npx", "pnpm", "pnpx", "yarn", "pip", "pipx", "uv", "uvx", "cargo"]);
|
|
21
|
+
export const SHIM_COMMANDS = Object.freeze(["npm", "npx", "pnpm", "pnpx", "yarn", "pip", "pip3", "pipx", "uv", "uvx", "cargo"]);
|
|
20
22
|
export const SHIM_SENTINEL = "dg-shim-v1";
|
|
21
23
|
export const RC_SENTINEL = "dg-shell-rc-v1";
|
|
22
24
|
export const RC_FUNCTIONS_SENTINEL = "dg-shim-functions-v1";
|
|
@@ -35,6 +37,14 @@ const LEGACY_PYTHON_HOOK_PTH_MARKER = "import dg_pip_hook";
|
|
|
35
37
|
export const SETUP_UNINSTALL_LOCK = "setup-uninstall";
|
|
36
38
|
export const SETUP_UNINSTALL_LOCK_STALE_MS = 30 * 60 * 1000;
|
|
37
39
|
export const STALE_SESSION_OLDER_THAN_MS = 24 * 60 * 60 * 1000;
|
|
40
|
+
function readShimFailClosed(env) {
|
|
41
|
+
try {
|
|
42
|
+
return loadUserConfig(env).policy.shimFailClosed;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
38
48
|
const DOCTOR_GROUP_BY_NAME = {
|
|
39
49
|
node: "environment",
|
|
40
50
|
package: "environment",
|
|
@@ -56,6 +66,7 @@ const DOCTOR_GROUP_BY_NAME = {
|
|
|
56
66
|
"commit-guard": "setup",
|
|
57
67
|
"stale-sessions": "setup",
|
|
58
68
|
service: "setup",
|
|
69
|
+
"agent-gate": "setup",
|
|
59
70
|
auth: "account",
|
|
60
71
|
api: "account"
|
|
61
72
|
};
|
|
@@ -102,6 +113,7 @@ export function buildSetupPlan(options) {
|
|
|
102
113
|
shell,
|
|
103
114
|
shimDir,
|
|
104
115
|
rcPath,
|
|
116
|
+
failClosed: readShimFailClosed(env),
|
|
105
117
|
writes: [
|
|
106
118
|
{
|
|
107
119
|
kind: "directory",
|
|
@@ -147,13 +159,14 @@ export function applySetupPlan(plan, now = new Date()) {
|
|
|
147
159
|
});
|
|
148
160
|
for (const command of SHIM_COMMANDS) {
|
|
149
161
|
const path = join(plan.shimDir, command);
|
|
150
|
-
writeFileSync(path, shimSource(command), {
|
|
162
|
+
writeFileSync(path, shimSource(command, { failClosed: plan.failClosed }), {
|
|
151
163
|
encoding: "utf8",
|
|
152
164
|
mode: 0o755
|
|
153
165
|
});
|
|
154
166
|
chmodSync(path, 0o755);
|
|
155
167
|
entries.push(cleanupEntry("shim", path, "mode1", now, SHIM_SENTINEL));
|
|
156
168
|
}
|
|
169
|
+
installStandaloneUninstaller(plan.paths.homeDir);
|
|
157
170
|
mkdirSync(dirname(plan.rcPath), {
|
|
158
171
|
recursive: true,
|
|
159
172
|
mode: 0o700
|
|
@@ -182,6 +195,45 @@ export function applySetupPlanWithLock(plan, now = new Date()) {
|
|
|
182
195
|
lock.release();
|
|
183
196
|
}
|
|
184
197
|
}
|
|
198
|
+
// Shims written by a dg that predates the self-cleaning template carry no
|
|
199
|
+
// auto-cleanup, so a plain `npm uninstall -g` leaves them stranded forever.
|
|
200
|
+
// Rewriting the installed shims and reinstalling the standalone uninstaller on
|
|
201
|
+
// the first run after a version bump propagates the current template to setups
|
|
202
|
+
// created by older releases.
|
|
203
|
+
export function refreshSetupOnUpgrade(env = process.env) {
|
|
204
|
+
const paths = resolveDgPaths(env);
|
|
205
|
+
const shimDir = join(paths.homeDir, ".dg", "shims");
|
|
206
|
+
if (!isShimFile(join(shimDir, "npm"))) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const failClosed = readShimFailClosed(env);
|
|
210
|
+
try {
|
|
211
|
+
const lock = acquireLockSync(paths, SETUP_UNINSTALL_LOCK, {
|
|
212
|
+
staleMs: SETUP_UNINSTALL_LOCK_STALE_MS
|
|
213
|
+
});
|
|
214
|
+
try {
|
|
215
|
+
for (const command of SHIM_COMMANDS) {
|
|
216
|
+
const shimPath = join(shimDir, command);
|
|
217
|
+
if (!isShimFile(shimPath)) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
writeFileSync(shimPath, shimSource(command, { failClosed }), {
|
|
221
|
+
encoding: "utf8",
|
|
222
|
+
mode: 0o755
|
|
223
|
+
});
|
|
224
|
+
chmodSync(shimPath, 0o755);
|
|
225
|
+
}
|
|
226
|
+
installStandaloneUninstaller(paths.homeDir);
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
lock.release();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
185
237
|
export function uninstallSetup(options) {
|
|
186
238
|
const paths = resolveDgPaths(options.env ?? process.env);
|
|
187
239
|
const hadCacheDirBeforeLock = existsSync(paths.cacheDir);
|
|
@@ -328,6 +380,7 @@ export function doctorReport(options = {}) {
|
|
|
328
380
|
});
|
|
329
381
|
checks.push(...unavailableDoctorChecks());
|
|
330
382
|
checks.push(serviceCheck(env));
|
|
383
|
+
checks.push(agentGateCheck(env, paths.homeDir));
|
|
331
384
|
return {
|
|
332
385
|
version: dgVersion(),
|
|
333
386
|
checks: checks.map(enrichDoctorCheck)
|
|
@@ -596,10 +649,10 @@ function reloadInstructions(shell) {
|
|
|
596
649
|
// The baked absolute dg path stops a PATH-shadowing dg from hijacking the shim;
|
|
597
650
|
// the fallbacks make it fail open — a removed or moved dg (uninstall, mid-upgrade)
|
|
598
651
|
// runs the real manager instead of bricking it.
|
|
599
|
-
export function shimSource(command) {
|
|
652
|
+
export function shimSource(command, options = {}) {
|
|
600
653
|
const dg = escapeDoubleQuotedSh(dgEntrypoint());
|
|
601
654
|
const nonce = '"${DG_SHIM_ACTIVE:+$DG_SHIM_ACTIVE,}' + `${command}:$$"`;
|
|
602
|
-
|
|
655
|
+
const head = [
|
|
603
656
|
"#!/bin/sh",
|
|
604
657
|
`# ${SHIM_SENTINEL}`,
|
|
605
658
|
`if [ -x "${dg}" ]; then`,
|
|
@@ -610,6 +663,27 @@ export function shimSource(command) {
|
|
|
610
663
|
`dg_bin=$(PATH="$dg_path" command -v dg 2>/dev/null)`,
|
|
611
664
|
`if [ -n "$dg_bin" ]; then`,
|
|
612
665
|
` DG_SHIM_ACTIVE=${nonce} exec "$dg_bin" ${command} "$@"`,
|
|
666
|
+
"fi"
|
|
667
|
+
];
|
|
668
|
+
// dg is gone from this machine. fail-closed (managed/CI) refuses the command
|
|
669
|
+
// so an unscanned install can't slip through; the operator removes the guard
|
|
670
|
+
// explicitly via `dg uninstall` while dg is present. fail-open (default)
|
|
671
|
+
// self-cleans dg's footprint and falls back to the real binary, because a
|
|
672
|
+
// plain `npm uninstall -g dg` cannot run cleanup and must not brick npm.
|
|
673
|
+
const failClosedTail = [
|
|
674
|
+
`echo "dg: install firewall required by policy (policy.shimFailClosed) but dg is unavailable; refusing to run ${command}" >&2`,
|
|
675
|
+
`echo "dg: reinstall dg, or run 'dg uninstall' while dg is present, to remove this guard" >&2`,
|
|
676
|
+
"exit 127",
|
|
677
|
+
""
|
|
678
|
+
];
|
|
679
|
+
const failOpenTail = [
|
|
680
|
+
`dg_home=$(dirname "$shim_dir")`,
|
|
681
|
+
`if [ -f "$dg_home/uninstall.mjs" ] && command -v node >/dev/null 2>&1; then`,
|
|
682
|
+
` ( sleep 10`,
|
|
683
|
+
` if [ ! -x "${dg}" ] && ! PATH="$dg_path" command -v dg >/dev/null 2>&1; then`,
|
|
684
|
+
` node "$dg_home/uninstall.mjs" --quiet >/dev/null 2>&1`,
|
|
685
|
+
` fi`,
|
|
686
|
+
` ) >/dev/null 2>&1 &`,
|
|
613
687
|
"fi",
|
|
614
688
|
`real_bin=$(PATH="$dg_path" command -v ${command} 2>/dev/null)`,
|
|
615
689
|
`if [ -n "$real_bin" ]; then`,
|
|
@@ -618,7 +692,8 @@ export function shimSource(command) {
|
|
|
618
692
|
`echo "dg: protection unavailable and no real ${command} found on PATH" >&2`,
|
|
619
693
|
"exit 127",
|
|
620
694
|
""
|
|
621
|
-
]
|
|
695
|
+
];
|
|
696
|
+
return [...head, ...(options.failClosed ? failClosedTail : failOpenTail)].join("\n");
|
|
622
697
|
}
|
|
623
698
|
function escapeDoubleQuotedSh(value) {
|
|
624
699
|
return value.replace(/[\\"$`]/g, "\\$&");
|
|
@@ -630,6 +705,25 @@ function dgEntrypoint() {
|
|
|
630
705
|
const argv1 = process.argv[1];
|
|
631
706
|
return argv1 ? resolve(argv1) : "dg";
|
|
632
707
|
}
|
|
708
|
+
function standaloneUninstallerSource() {
|
|
709
|
+
try {
|
|
710
|
+
return fileURLToPath(new URL("../standalone/uninstall.mjs", import.meta.url));
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
return undefined;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function installStandaloneUninstaller(homeDir) {
|
|
717
|
+
const source = standaloneUninstallerSource();
|
|
718
|
+
if (!source || !existsSync(source)) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const dgRoot = join(homeDir, ".dg");
|
|
722
|
+
mkdirSync(dgRoot, { recursive: true, mode: 0o700 });
|
|
723
|
+
const dest = join(dgRoot, "uninstall.mjs");
|
|
724
|
+
copyFileSync(source, dest);
|
|
725
|
+
chmodSync(dest, 0o600);
|
|
726
|
+
}
|
|
633
727
|
function withRcBlock(existing, plan) {
|
|
634
728
|
const withoutExisting = stripRcBlock(existing);
|
|
635
729
|
const block = plan.shell === "fish" ? fishRcBlock(plan.shimDir) : posixRcBlock(plan.shimDir);
|
|
@@ -846,7 +940,19 @@ function configCheck(paths, env) {
|
|
|
846
940
|
}
|
|
847
941
|
try {
|
|
848
942
|
accessSync(paths.configDir, constants.R_OK);
|
|
849
|
-
loadUserConfig(env);
|
|
943
|
+
const config = loadUserConfig(env);
|
|
944
|
+
if (config.api.baseUrl !== DEFAULT_CONFIG.api.baseUrl) {
|
|
945
|
+
// The verdict API is the firewall's source of truth. A non-default
|
|
946
|
+
// endpoint is legitimate for enterprise self-host, but a silent repoint
|
|
947
|
+
// is also how an attacker would make every verdict come back clean, so
|
|
948
|
+
// surface it on the standard health check rather than trusting it quietly.
|
|
949
|
+
return {
|
|
950
|
+
name: "config",
|
|
951
|
+
status: "warn",
|
|
952
|
+
message: `dg is fetching verdicts from a non-default API endpoint: ${config.api.baseUrl} (default ${DEFAULT_CONFIG.api.baseUrl})`,
|
|
953
|
+
fix: `if you did not set this, your verdict source may be tampered — restore with: dg config set api.baseUrl ${DEFAULT_CONFIG.api.baseUrl}`
|
|
954
|
+
};
|
|
955
|
+
}
|
|
850
956
|
return {
|
|
851
957
|
name: "config",
|
|
852
958
|
status: "pass",
|
|
@@ -962,6 +1068,45 @@ function serviceCheck(env) {
|
|
|
962
1068
|
message: `Service mode is running at ${state.proxy.proxyUrl}; trust installed: ${state.trustInstalled ? "yes" : "no"}`
|
|
963
1069
|
};
|
|
964
1070
|
}
|
|
1071
|
+
function hookedAgentLabels(env, home) {
|
|
1072
|
+
const labels = [];
|
|
1073
|
+
for (const agent of AGENT_IDS) {
|
|
1074
|
+
const integration = AGENTS[agent];
|
|
1075
|
+
if (!integration.detect(home)) {
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
const ctx = resolveAgentHookContext(agent, { env, home });
|
|
1079
|
+
const hooked = integration.verify(ctx).find((check) => check.name === integration.isInstalledCheckName)?.ok ?? false;
|
|
1080
|
+
if (hooked) {
|
|
1081
|
+
labels.push(agentLabel(agent));
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return labels;
|
|
1085
|
+
}
|
|
1086
|
+
function agentGateCheck(env, home) {
|
|
1087
|
+
const hooked = hookedAgentLabels(env, home);
|
|
1088
|
+
if (hooked.length === 0) {
|
|
1089
|
+
return {
|
|
1090
|
+
name: "agent-gate",
|
|
1091
|
+
status: "unavailable",
|
|
1092
|
+
message: "No AI agent has the dg install hook; run 'dg agents on' to protect agent-run installs"
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
const posture = readNetworkGatePosture(env);
|
|
1096
|
+
if (posture.live) {
|
|
1097
|
+
return {
|
|
1098
|
+
name: "agent-gate",
|
|
1099
|
+
status: "pass",
|
|
1100
|
+
message: `${hooked.join(", ")} hooked; network gate live at ${posture.proxyUrl} (installs screened at fetch by artifact hash)`
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
return {
|
|
1104
|
+
name: "agent-gate",
|
|
1105
|
+
status: "warn",
|
|
1106
|
+
message: `${hooked.join(", ")} hooked but the fetch-time network gate is OFF — static pre-screen only; absolute-path, manifest-only, dynamic, and unsupported-manager installs are NOT screened`,
|
|
1107
|
+
fix: GATE_ENABLE_HINT
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
965
1110
|
function pathPrecedenceCheck(env, shimDir, functionsPresent) {
|
|
966
1111
|
const pathEntries = (env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
967
1112
|
const shimIndex = pathEntries.indexOf(shimDir);
|
|
@@ -1148,9 +1293,8 @@ function scriptGateCheck(env) {
|
|
|
1148
1293
|
if (mode === "enforce") {
|
|
1149
1294
|
return {
|
|
1150
1295
|
name: "script-gate",
|
|
1151
|
-
status: "
|
|
1152
|
-
message: "
|
|
1153
|
-
fix: "enforcement ships in an upcoming release; dg config set scriptGate.mode observe clears this warning"
|
|
1296
|
+
status: "pass",
|
|
1297
|
+
message: "Install-script gate enforces: protected npm/yarn installs run with --ignore-scripts so packages cannot run lifecycle scripts (pnpm blocks them natively)"
|
|
1154
1298
|
};
|
|
1155
1299
|
}
|
|
1156
1300
|
return {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { uninstallSetup } from "./plan.js";
|
|
4
|
+
import { LockBusyError, resolveDgPaths } from "../state/index.js";
|
|
5
|
+
function run() {
|
|
6
|
+
const quiet = process.argv.includes("--quiet");
|
|
7
|
+
try {
|
|
8
|
+
const result = uninstallSetup({ keepConfig: false, all: true });
|
|
9
|
+
if (!quiet) {
|
|
10
|
+
process.stderr.write(`Dependency Guardian cleaned up ${result.removed.length} leftover item(s) after the package was removed.\n`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (!quiet && !(error instanceof LockBusyError)) {
|
|
15
|
+
process.stderr.write(`dg self-cleanup could not finish: ${error.message}\n`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
rmSync(join(resolveDgPaths().homeDir, ".dg"), { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
run();
|
package/dist/setup-ui/wizard.js
CHANGED
|
@@ -175,7 +175,8 @@ export const SetupWizardApp = (props) => {
|
|
|
175
175
|
export async function runSetupWizard(plan, options = {}) {
|
|
176
176
|
const env = options.env ?? process.env;
|
|
177
177
|
const ci = process.env.CI;
|
|
178
|
-
|
|
178
|
+
const restoreCi = ci === "" || ci === "0" || ci === "false";
|
|
179
|
+
if (restoreCi) {
|
|
179
180
|
delete process.env.CI;
|
|
180
181
|
}
|
|
181
182
|
const agents = collectAgentOffers();
|
|
@@ -193,11 +194,18 @@ export async function runSetupWizard(plan, options = {}) {
|
|
|
193
194
|
} }), { exitOnCtrlC: true });
|
|
194
195
|
await instance.waitUntilExit();
|
|
195
196
|
leaveTui();
|
|
197
|
+
if (restoreCi) {
|
|
198
|
+
process.env.CI = ci;
|
|
199
|
+
}
|
|
196
200
|
const theme = createTheme(resolvePresentation().color);
|
|
197
201
|
const accent = (text) => theme.paint("accent", text);
|
|
198
202
|
const muted = (text) => theme.paint("muted", text);
|
|
199
203
|
const outcome = state.outcome;
|
|
200
204
|
if (outcome === null) {
|
|
205
|
+
// Aborted (e.g. Ctrl-C). Record the skip so the wizard does not re-intercept
|
|
206
|
+
// every subsequent dg command until the user explicitly declines or applies it.
|
|
207
|
+
markWizardSkipped(env);
|
|
208
|
+
markFirstRunShown(env);
|
|
201
209
|
return { exitCode: 0, stdout: "", stderr: "" };
|
|
202
210
|
}
|
|
203
211
|
if (outcome.kind === "declined") {
|