@westbayberry/dg 2.3.2 → 2.3.4

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 (122) hide show
  1. package/README.md +95 -208
  2. package/dist/agents/copilot-cli.js +7 -2
  3. package/dist/agents/persistence.js +83 -8
  4. package/dist/agents/registry.js +5 -2
  5. package/dist/agents/routing.js +31 -14
  6. package/dist/api/analyze.js +138 -41
  7. package/dist/audit/deep.js +2 -1
  8. package/dist/audit/detectors.js +23 -2
  9. package/dist/audit/rules.js +4 -1
  10. package/dist/audit-ui/export.js +5 -4
  11. package/dist/auth/device-login.js +33 -11
  12. package/dist/auth/login-app.js +17 -12
  13. package/dist/auth/store.js +65 -9
  14. package/dist/bin/dg.js +1 -1
  15. package/dist/commands/audit.js +12 -20
  16. package/dist/commands/{guard-commit.js → commit-guard.js} +12 -12
  17. package/dist/commands/cooldown.js +2 -1
  18. package/dist/commands/decisions.js +8 -7
  19. package/dist/commands/help.js +11 -3
  20. package/dist/commands/licenses.js +17 -8
  21. package/dist/commands/login.js +58 -21
  22. package/dist/commands/router.js +12 -2
  23. package/dist/commands/sbom.js +20 -5
  24. package/dist/commands/scan.js +5 -4
  25. package/dist/commands/service.js +1 -1
  26. package/dist/commands/setup.js +12 -12
  27. package/dist/commands/status.js +4 -4
  28. package/dist/commands/uninstall.js +1 -1
  29. package/dist/commands/update.js +20 -0
  30. package/dist/commands/verify.js +4 -4
  31. package/dist/config/settings.js +81 -12
  32. package/dist/export-ui/ExportDialog.js +1 -1
  33. package/dist/gate/cooldown-request.js +22 -0
  34. package/dist/gate/egress-guard.js +185 -0
  35. package/dist/gate/host-match.js +48 -0
  36. package/dist/gate/verdict-core.js +101 -0
  37. package/dist/gate/verdict-fetch.js +214 -0
  38. package/dist/launcher/agent-check.js +1096 -42
  39. package/dist/launcher/agent-hook-exec.js +8 -1
  40. package/dist/launcher/agent-hook-io.js +5 -3
  41. package/dist/launcher/classify.js +110 -26
  42. package/dist/launcher/env.js +84 -18
  43. package/dist/launcher/install-preflight.js +26 -3
  44. package/dist/launcher/live-install.js +4 -2
  45. package/dist/launcher/manifest-screen.js +88 -7
  46. package/dist/launcher/output-redaction.js +153 -15
  47. package/dist/launcher/preflight-prompt.js +1 -1
  48. package/dist/launcher/resolve-real-binary.js +8 -3
  49. package/dist/launcher/run.js +237 -40
  50. package/dist/origin/artifact-cache.js +55 -0
  51. package/dist/origin/cargo-config.js +12 -0
  52. package/dist/origin/gate-mode.js +7 -0
  53. package/dist/origin/platform-paths.js +14 -0
  54. package/dist/origin/precedence.js +345 -0
  55. package/dist/origin/server.js +611 -0
  56. package/dist/origin/spike-worker.js +35 -0
  57. package/dist/origin/worker.js +116 -0
  58. package/dist/presentation/package-page.js +21 -1
  59. package/dist/presentation/safe-version.js +11 -0
  60. package/dist/presentation/sarif.js +109 -0
  61. package/dist/project/dgfile.js +54 -17
  62. package/dist/project/override-trust.js +0 -0
  63. package/dist/proxy/enforcement.js +27 -7
  64. package/dist/proxy/metadata-map.js +33 -7
  65. package/dist/proxy/preverified.js +0 -3
  66. package/dist/proxy/server.js +104 -446
  67. package/dist/proxy/upstream-proxy.js +23 -4
  68. package/dist/proxy/worker.js +3 -2
  69. package/dist/publish-set/collect.js +6 -0
  70. package/dist/runtime/fatal.js +2 -1
  71. package/dist/runtime/first-run.js +5 -1
  72. package/dist/runtime/node-version.js +51 -0
  73. package/dist/runtime/nudges.js +27 -1
  74. package/dist/sbom/cyclonedx.js +120 -33
  75. package/dist/sbom/graph.js +236 -0
  76. package/dist/sbom/spdx.js +53 -0
  77. package/dist/sbom-ui/SbomApp.js +5 -3
  78. package/dist/sbom-ui/inventory.js +8 -10
  79. package/dist/scan/analyze-worker.js +3 -1
  80. package/dist/scan/collect.js +38 -8
  81. package/dist/scan/command.js +77 -27
  82. package/dist/scan/discovery.js +18 -7
  83. package/dist/scan/installed.js +202 -0
  84. package/dist/scan/manifest-coverage.js +128 -0
  85. package/dist/scan/render.js +125 -40
  86. package/dist/scan/scanner-report.js +92 -22
  87. package/dist/scan/staged.js +68 -15
  88. package/dist/scan/types.js +7 -1
  89. package/dist/scan-ui/LegacyApp.js +3 -3
  90. package/dist/scan-ui/components/InteractiveResultsView.js +17 -14
  91. package/dist/scan-ui/hooks/useScan.js +37 -11
  92. package/dist/scan-ui/launch.js +2 -2
  93. package/dist/scan-ui/logo.js +6 -2
  94. package/dist/scan-ui/shims.js +6 -6
  95. package/dist/scripts/gate.js +53 -35
  96. package/dist/security/csv.js +9 -0
  97. package/dist/security/sanitize.js +11 -2
  98. package/dist/service/state.js +120 -37
  99. package/dist/service/trust-refresh.js +12 -1
  100. package/dist/service/trust-store.js +36 -5
  101. package/dist/service/worker.js +5 -7
  102. package/dist/setup/git-hook.js +2 -2
  103. package/dist/setup/install-method.js +36 -0
  104. package/dist/setup/plan.js +119 -57
  105. package/dist/setup/uninstall-standalone.js +7 -2
  106. package/dist/setup-ui/gate.js +1 -8
  107. package/dist/setup-ui/offer.js +14 -7
  108. package/dist/setup-ui/selector.js +1 -1
  109. package/dist/setup-ui/wizard.js +73 -78
  110. package/dist/standalone/uninstall.mjs +1114 -386
  111. package/dist/state/cleanup-registry.js +14 -3
  112. package/dist/state/cooldown-held.js +36 -13
  113. package/dist/state/locks.js +239 -34
  114. package/dist/state/sessions.js +2 -2
  115. package/dist/util/sh-escape.js +6 -0
  116. package/dist/util/tty-prompt.js +13 -4
  117. package/dist/verify/local.js +60 -10
  118. package/dist/verify/package-check.js +35 -49
  119. package/dist/verify/preflight.js +59 -33
  120. package/dist/verify/render.js +11 -12
  121. package/npm-shrinkwrap.json +714 -213
  122. package/package.json +4 -2
@@ -16,7 +16,7 @@ import { createTheme } from "../presentation/theme.js";
16
16
  import { activateShell, activationOffer } from "../setup/activate-shell.js";
17
17
  import { commitGuardOffer } from "../setup/git-hook.js";
18
18
  import { activationCommand, tildifyPath } from "../setup/plan.js";
19
- import { markSecurityNotesShown, markWizardSkipped, securityNotesShown } from "./gate.js";
19
+ import { markSecurityNotesShown, securityNotesShown } from "./gate.js";
20
20
  import { Selector } from "./selector.js";
21
21
  import { buildCommitGuardTask, buildWizardTasks } from "./tasks.js";
22
22
  export function protectsSentence(agentLabels) {
@@ -30,31 +30,30 @@ export function protectsSentence(agentLabels) {
30
30
  }
31
31
  const Header = ({ version }) => (_jsxs(Box, { flexDirection: "column", children: [renderLogo("pass").map((line, i) => (_jsx(Text, { children: line }, i))), _jsx(Text, { children: " " }), _jsxs(Text, { bold: true, children: ["Dependency Guardian v", version] }), _jsx(Text, { children: " " })] }));
32
32
  const LoginFlow = ({ webBase, env, onDone }) => {
33
- const { state, openAndPoll } = useLogin(webBase, env);
34
- useEffect(() => {
35
- if (state.phase === "ready") {
36
- openAndPoll();
37
- }
38
- }, [state.phase, openAndPoll]);
33
+ const { state, openInBrowser } = useLogin(webBase, env);
39
34
  useInput((_input, key) => {
40
- if (key.return && (state.phase === "success" || state.phase === "expired" || state.phase === "error")) {
41
- onDone(state.phase === "success" ? state.email : "");
42
- }
43
- else if (key.escape) {
35
+ if (key.escape) {
44
36
  onDone("");
45
37
  }
38
+ else if (key.return) {
39
+ if (state.phase === "ready") {
40
+ openInBrowser();
41
+ }
42
+ else if (state.phase === "success" || state.phase === "expired" || state.phase === "error") {
43
+ onDone(state.phase === "success" ? state.email : "");
44
+ }
45
+ }
46
46
  });
47
47
  switch (state.phase) {
48
48
  case "creating":
49
49
  return _jsx(Spinner, { label: "Creating login session\u2026" });
50
50
  case "ready":
51
- case "waiting":
52
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Sign in at:" }), _jsx(Text, { color: "cyan", children: state.verifyUrl }), _jsx(Text, { children: " " }), _jsx(Spinner, { label: "Waiting for you to approve in the browser\u2026" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Esc to skip" })] }));
51
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Sign in at:" }), _jsx(Text, { color: "cyan", children: state.verifyUrl }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Press Enter to open it in your browser, or open the link yourself." }), _jsx(Text, { children: " " }), _jsx(Spinner, { label: "Waiting for you to approve\u2026" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Esc to cancel setup" })] }));
53
52
  case "success":
54
53
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: ["\u2713 Logged in", state.email ? ` as ${state.email}` : "", state.plan ? ` (${displayTier(state.plan)} plan)` : ""] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Login successful. Press Enter to continue\u2026" })] }));
55
54
  case "expired":
56
55
  case "error":
57
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: state.phase === "expired" ? "That login link expired." : `Login failed: ${state.message}.` }), _jsx(Text, { dimColor: true, children: "You can run dg login later. Press Enter to continue\u2026" })] }));
56
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: state.phase === "expired" ? "That login link expired." : `Login failed: ${state.message}.` }), _jsx(Text, { dimColor: true, children: "Run dg login any time. Press Enter to exit setup\u2026" })] }));
58
57
  }
59
58
  };
60
59
  function hyperlink(label, url) {
@@ -70,7 +69,7 @@ const NotesStep = ({ termsUrl, privacyUrl, onAgree }) => {
70
69
  };
71
70
  export const SetupWizardApp = (props) => {
72
71
  const { exit } = useApp();
73
- const [step, setStep] = useState(props.loginNeeded ? "login" : props.notesNeeded ? "notes" : "choose");
72
+ const [step, setStep] = useState(props.notesNeeded ? "notes" : props.loginNeeded ? "login" : "choose");
74
73
  const [results, setResults] = useState([]);
75
74
  const [running, setRunning] = useState(null);
76
75
  const [fatalDetail, setFatalDetail] = useState("");
@@ -79,7 +78,7 @@ export const SetupWizardApp = (props) => {
79
78
  const applyStarted = useRef(false);
80
79
  const Login = props.LoginFlowComponent ?? LoginFlow;
81
80
  const afterLogin = () => {
82
- setStep(props.notesNeeded ? "notes" : "choose");
81
+ setStep("choose");
83
82
  };
84
83
  useInput((_input, key) => {
85
84
  if (key.return) {
@@ -136,30 +135,25 @@ export const SetupWizardApp = (props) => {
136
135
  cancelled = true;
137
136
  };
138
137
  }, [step, props]);
139
- return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingTop: 1, children: [_jsx(Header, { version: props.version }), step === "login" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Connect your account" }), _jsx(Text, { children: " " }), _jsx(Selector, { options: [{ label: "Connect in your browser" }, { label: "Skip for now" }], onSelect: (index) => {
140
- if (index === 0) {
141
- setStep("login-flow");
142
- }
143
- else {
144
- props.onLoginSkipped();
145
- afterLogin();
146
- }
147
- }, onCancel: () => {
138
+ return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingTop: 1, children: [_jsx(Header, { version: props.version }), step === "login" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Connect your account" }), _jsx(Text, { dimColor: true, children: "Sign in to use dg. Esc cancels setup." }), _jsx(Text, { children: " " }), _jsx(Selector, { options: [{ label: "Connect in your browser" }], onSelect: () => setStep("login-flow"), onCancel: () => {
148
139
  props.onLoginSkipped();
149
- afterLogin();
140
+ props.onOutcome({ kind: "declined" });
141
+ setStep("declined");
150
142
  } })] })), step === "login-flow" && (_jsx(Login, { webBase: props.webBase, env: props.env, onDone: (loggedInEmail) => {
151
143
  if (loggedInEmail) {
152
144
  setEmail(loggedInEmail);
153
145
  emailRef.current = loggedInEmail;
146
+ afterLogin();
154
147
  }
155
148
  else {
156
149
  props.onLoginSkipped();
150
+ props.onOutcome({ kind: "declined" });
151
+ setStep("declined");
157
152
  }
158
- afterLogin();
159
153
  } })), step === "notes" && (_jsx(NotesStep, { termsUrl: `${props.webBase}/terms`, privacyUrl: `${props.webBase}/privacy`, onAgree: () => {
160
154
  props.onNotesAgreed();
161
- setStep("choose");
162
- } })), step === "choose" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Use recommended settings?" }), _jsx(Text, { children: " " }), _jsx(Text, { children: props.sentence }), props.guardsRepo && _jsx(Text, { children: "Also scans this repo's commits before they land." }), props.skips.map((skip) => (_jsxs(Text, { dimColor: true, children: [skip.label, " found but skipped \u2014 ", skip.detail] }, skip.label))), _jsx(Text, { children: " " }), _jsx(Selector, { options: [{ label: "Yes, use recommended settings" }, { label: "No, maybe later with dg setup" }], onSelect: (index) => {
155
+ setStep(props.loginNeeded ? "login" : "choose");
156
+ } })), step === "choose" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Use recommended settings?" }), _jsx(Text, { children: " " }), _jsx(Text, { children: props.sentence }), props.guardsRepo && _jsx(Text, { children: "Also scans this repo's commits before they land." }), props.skips.map((skip) => (_jsxs(Text, { dimColor: true, children: [skip.label, " found but skipped \u2014 ", skip.detail] }, skip.label))), _jsx(Text, { children: " " }), _jsx(Selector, { options: [{ label: "Yes, use recommended settings" }, { label: "Not now exit setup" }], onSelect: (index) => {
163
157
  if (index === 0) {
164
158
  setStep("applying");
165
159
  }
@@ -170,7 +164,7 @@ export const SetupWizardApp = (props) => {
170
164
  }, onCancel: () => {
171
165
  props.onOutcome({ kind: "declined" });
172
166
  setStep("declined");
173
- } })] })), (step === "applying" || step === "done" || step === "fatal") && (_jsxs(Box, { flexDirection: "column", children: [results.map((line) => (_jsxs(Text, { color: line.ok ? "green" : "yellow", children: [line.ok ? "✓" : "✗", " ", line.label, !line.ok && line.detail ? ` — ${line.detail}` : ""] }, line.label))), running !== null && _jsx(Spinner, { label: running }), step === "fatal" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "red", children: fatalDetail })] })), step === "done" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Setup complete." }), " ", "Activate now: ", _jsx(Text, { bold: true, children: props.activateHint })] }), email ? _jsxs(Text, { dimColor: true, children: ["Logged in as ", email] }) : null, _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Press Enter to continue\u2026" })] }))] })), step === "declined" && _jsx(Text, { dimColor: true, children: "Run dg setup any time to turn this on." })] }));
167
+ } })] })), (step === "applying" || step === "done" || step === "fatal") && (_jsxs(Box, { flexDirection: "column", children: [results.map((line) => (_jsxs(Text, { color: line.ok ? "green" : "yellow", children: [line.ok ? "✓" : "✗", " ", line.label, !line.ok && line.detail ? ` — ${line.detail}` : ""] }, line.label))), running !== null && _jsx(Spinner, { label: running }), step === "fatal" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "red", children: fatalDetail })] })), step === "done" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Setup complete." }), " ", "Activate now: ", _jsx(Text, { bold: true, children: props.activateHint })] }), email ? _jsxs(Text, { dimColor: true, children: ["Logged in as ", email] }) : null, _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Press Enter to continue\u2026" })] }))] })), step === "declined" && _jsx(Text, { dimColor: true, children: "Setup not finished \u2014 dg is unavailable until setup completes." })] }));
174
168
  };
175
169
  export async function runSetupWizard(plan, options = {}) {
176
170
  const env = options.env ?? process.env;
@@ -179,55 +173,56 @@ export async function runSetupWizard(plan, options = {}) {
179
173
  if (restoreCi) {
180
174
  delete process.env.CI;
181
175
  }
182
- const agents = collectAgentOffers();
183
- const repo = commitGuardOffer();
184
- const auth = authStatus(env);
185
- const state = { outcome: null };
186
- const tasks = buildWizardTasks(plan, agents);
187
- if (repo) {
188
- tasks.push(buildCommitGuardTask(repo));
189
- }
190
- const { render } = await import("ink");
191
- enterTui();
192
- const instance = render(_jsx(SetupWizardApp, { version: dgVersion(), notesNeeded: !securityNotesShown(env), loginNeeded: !auth.authenticated, authedEmail: auth.email ?? "", sentence: protectsSentence(agents.map((offer) => offer.label)), guardsRepo: repo !== null, skips: collectAgentSkips(), tasks: tasks, webBase: resolveWebBase(env), env: env, activateHint: activationCommand(plan.shell, tildifyPath(plan.rcPath)), onNotesAgreed: () => markSecurityNotesShown(env), onLoginSkipped: () => recordLoginNudge(env), onOutcome: (outcome) => {
193
- state.outcome = outcome;
194
- } }), { exitOnCtrlC: true });
195
- await instance.waitUntilExit();
196
- leaveTui();
197
- if (restoreCi) {
198
- process.env.CI = ci;
199
- }
200
- const theme = createTheme(resolvePresentation().color);
201
- const accent = (text) => theme.paint("accent", text);
202
- const muted = (text) => theme.paint("muted", text);
203
- const outcome = state.outcome;
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);
209
- return { exitCode: 0, stdout: "", stderr: "" };
210
- }
211
- if (outcome.kind === "declined") {
212
- markWizardSkipped(env);
176
+ try {
177
+ const agents = collectAgentOffers();
178
+ const repo = commitGuardOffer();
179
+ const auth = authStatus(env);
180
+ const state = { outcome: null };
181
+ const tasks = buildWizardTasks(plan, agents);
182
+ if (repo) {
183
+ tasks.push(buildCommitGuardTask(repo));
184
+ }
185
+ const { render } = await import("ink");
186
+ enterTui();
187
+ const instance = render(_jsx(SetupWizardApp, { version: dgVersion(), notesNeeded: !securityNotesShown(env), loginNeeded: !auth.authenticated, authedEmail: auth.email ?? "", sentence: protectsSentence(agents.map((offer) => offer.label)), guardsRepo: repo !== null, skips: collectAgentSkips(), tasks: tasks, webBase: resolveWebBase(env), env: env, activateHint: activationCommand(plan.shell, tildifyPath(plan.rcPath)), onNotesAgreed: () => markSecurityNotesShown(env), onLoginSkipped: () => recordLoginNudge(env), onOutcome: (outcome) => {
188
+ state.outcome = outcome;
189
+ } }), { exitOnCtrlC: true });
190
+ await instance.waitUntilExit();
191
+ leaveTui();
192
+ const theme = createTheme(resolvePresentation().color);
193
+ const accent = (text) => theme.paint("accent", text);
194
+ const muted = (text) => theme.paint("muted", text);
195
+ const outcome = state.outcome;
196
+ if (outcome === null || outcome.kind === "declined") {
197
+ return {
198
+ exitCode: 1,
199
+ stdout: "",
200
+ stderr: ` ${muted("Setup not finished — dg is unavailable until setup completes. Run")} ${accent("dg setup")} ${muted("to finish.")}\n`
201
+ };
202
+ }
203
+ if (outcome.kind === "fatal") {
204
+ return { exitCode: 1, stdout: "", stderr: ` ${outcome.detail}\n` };
205
+ }
213
206
  markFirstRunShown(env);
214
- return { exitCode: 0, stdout: "", stderr: ` ${muted("Run")} ${accent("dg setup")} ${muted("any time to turn this on.")}\n` };
215
- }
216
- if (outcome.kind === "fatal") {
217
- return { exitCode: 1, stdout: "", stderr: ` ${outcome.detail}\n` };
218
- }
219
- markFirstRunShown(env);
220
- const lines = outcome.results.map((line) => line.ok
221
- ? ` ${theme.paint("pass", `✓ ${line.label}`)}`
222
- : ` ${theme.paint("warn", `✗ ${line.label}`)}${line.detail ? ` ${muted(`— ${line.detail}`)}` : ""}`);
223
- lines.push(` ${theme.paint("pass", "✓ dg setup complete — installs are protected in new terminals.")}`);
224
- if (outcome.email) {
225
- lines.push(` ${muted(`Logged in as ${outcome.email}`)}`);
207
+ const lines = outcome.results.map((line) => line.ok
208
+ ? ` ${theme.paint("pass", `✓ ${line.label}`)}`
209
+ : ` ${theme.paint("warn", `✗ ${line.label}`)}${line.detail ? ` ${muted(`— ${line.detail}`)}` : ""}`);
210
+ lines.push(` ${theme.paint("pass", "✓ dg setup complete installs are protected in new terminals.")}`);
211
+ if (outcome.email) {
212
+ lines.push(` ${muted(`Logged in as ${outcome.email}`)}`);
213
+ }
214
+ lines.push(` ${muted("Next: run")} ${accent("dg scan")} ${muted("in a project for a dependency-risk overview")}`);
215
+ lines.push(` ${muted("Docs:")} ${accent(`${resolveWebBase(env)}/docs`)}`);
216
+ if (options.autoActivate && activationOffer() === "prompt") {
217
+ process.stderr.write(`${lines.join("\n")}\n ${muted("Starting a protected shell — type")} ${accent("exit")} ${muted("to return to your previous one.")}\n`);
218
+ return { exitCode: activateShell(), stdout: "", stderr: "" };
219
+ }
220
+ lines.push(` ${muted("Activate now:")} ${accent(activationCommand(plan.shell, tildifyPath(plan.rcPath)))}`);
221
+ return { exitCode: 0, stdout: "", stderr: `${lines.join("\n")}\n` };
226
222
  }
227
- if (options.autoActivate && activationOffer() === "prompt") {
228
- process.stderr.write(`${lines.join("\n")}\n ${muted("Starting a protected shell — type")} ${accent("exit")} ${muted("to return to your previous one.")}\n`);
229
- return { exitCode: activateShell(), stdout: "", stderr: "" };
223
+ finally {
224
+ if (restoreCi) {
225
+ process.env.CI = ci;
226
+ }
230
227
  }
231
- lines.push(` ${muted("Activate now:")} ${accent(activationCommand(plan.shell, tildifyPath(plan.rcPath)))}`);
232
- return { exitCode: 0, stdout: "", stderr: `${lines.join("\n")}\n` };
233
228
  }