@westbayberry/dg 2.1.0 → 2.2.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.
Files changed (138) hide show
  1. package/README.md +216 -226
  2. package/dist/agents/claude-code.js +113 -0
  3. package/dist/agents/codex.js +65 -0
  4. package/dist/agents/copilot-cli.js +115 -0
  5. package/dist/agents/cursor.js +113 -0
  6. package/dist/agents/gemini.js +107 -0
  7. package/dist/agents/persistence.js +285 -0
  8. package/dist/agents/registry.js +127 -0
  9. package/dist/agents/types.js +1 -0
  10. package/dist/agents/windsurf.js +93 -0
  11. package/dist/api/analyze.js +1 -1
  12. package/dist/audit/detectors.js +0 -11
  13. package/dist/audit/events.js +5 -21
  14. package/dist/audit-ui/AuditApp.js +2 -0
  15. package/dist/audit-ui/components/AuditResultsView.js +55 -92
  16. package/dist/audit-ui/export.js +0 -4
  17. package/dist/audit-ui/format.js +0 -3
  18. package/dist/audit-ui/launch.js +10 -1
  19. package/dist/auth/device-login.js +4 -5
  20. package/dist/auth/login-app.js +7 -7
  21. package/dist/auth/store.js +8 -3
  22. package/dist/bin/dg.js +59 -52
  23. package/dist/commands/agents.js +231 -0
  24. package/dist/commands/audit.js +22 -3
  25. package/dist/commands/config.js +15 -7
  26. package/dist/commands/cooldown.js +389 -0
  27. package/dist/commands/decisions.js +23 -9
  28. package/dist/commands/doctor.js +1 -1
  29. package/dist/commands/help.js +1 -1
  30. package/dist/commands/licenses.js +10 -22
  31. package/dist/commands/logout.js +4 -11
  32. package/dist/commands/router.js +6 -4
  33. package/dist/commands/sbom.js +206 -0
  34. package/dist/commands/scan.js +1 -1
  35. package/dist/commands/service.js +32 -13
  36. package/dist/commands/setup.js +197 -27
  37. package/dist/commands/types.js +1 -0
  38. package/dist/commands/update.js +17 -8
  39. package/dist/commands/verify.js +8 -5
  40. package/dist/config/settings.js +14 -44
  41. package/dist/decisions/remember-prompt.js +5 -10
  42. package/dist/export-ui/ExportDialog.js +198 -0
  43. package/dist/install-ui/LiveInstall.js +2 -2
  44. package/dist/install-ui/block-render.js +1 -1
  45. package/dist/install-ui/prep-spinner.js +32 -0
  46. package/dist/launcher/agent-check.js +466 -0
  47. package/dist/launcher/agent-hook-exec.js +70 -0
  48. package/dist/launcher/agent-hook-io.js +31 -0
  49. package/dist/launcher/cargo-cache.js +40 -0
  50. package/dist/launcher/classify.js +17 -6
  51. package/dist/launcher/env.js +71 -24
  52. package/dist/launcher/install-preflight.js +51 -14
  53. package/dist/launcher/live-install.js +25 -5
  54. package/dist/launcher/output-redaction.js +7 -4
  55. package/dist/launcher/preflight-prompt.js +14 -1
  56. package/dist/launcher/run.js +89 -86
  57. package/dist/launcher/spawn-invocation.js +21 -0
  58. package/dist/policy/cooldown.js +18 -5
  59. package/dist/policy/evaluate.js +5 -6
  60. package/dist/policy/pypi-name.js +17 -0
  61. package/dist/presentation/mode.js +3 -2
  62. package/dist/presentation/package-page.js +9 -0
  63. package/dist/presentation/theme.js +7 -7
  64. package/dist/project/dgfile.js +145 -6
  65. package/dist/proxy/auth.js +42 -0
  66. package/dist/proxy/ca.js +29 -9
  67. package/dist/proxy/cooldown-exemptions-file.js +33 -0
  68. package/dist/proxy/enforcement.js +55 -16
  69. package/dist/proxy/metadata-map.js +41 -3
  70. package/dist/proxy/preverified.js +55 -0
  71. package/dist/proxy/server.js +448 -49
  72. package/dist/proxy/worker.js +16 -1
  73. package/dist/publish-set/collect.js +1 -4
  74. package/dist/publish-set/npm.js +8 -5
  75. package/dist/publish-set/pack.js +9 -3
  76. package/dist/runtime/cli.js +0 -4
  77. package/dist/runtime/fatal.js +31 -0
  78. package/dist/runtime/first-run.js +12 -11
  79. package/dist/runtime/node-version.js +43 -6
  80. package/dist/runtime/nudges.js +35 -2
  81. package/dist/sbom/cyclonedx.js +211 -0
  82. package/dist/sbom-ui/SbomApp.js +158 -0
  83. package/dist/sbom-ui/components/SbomHeader.js +32 -0
  84. package/dist/sbom-ui/components/SbomList.js +52 -0
  85. package/dist/sbom-ui/inventory.js +128 -0
  86. package/dist/sbom-ui/launch.js +51 -0
  87. package/dist/sbom-ui/run.js +55 -0
  88. package/dist/sbom-ui/store.js +26 -0
  89. package/dist/scan/collect.js +10 -6
  90. package/dist/scan/command.js +17 -10
  91. package/dist/scan/discovery.js +11 -2
  92. package/dist/scan/render.js +28 -4
  93. package/dist/scan/scanner-report.js +15 -9
  94. package/dist/scan/staged.js +2 -1
  95. package/dist/scan-ui/LegacyApp.js +9 -13
  96. package/dist/scan-ui/alt-screen.js +5 -8
  97. package/dist/scan-ui/components/InteractiveResultsView.js +133 -120
  98. package/dist/scan-ui/components/ProgressBar.js +3 -14
  99. package/dist/scan-ui/components/ProjectSelector.js +1 -1
  100. package/dist/scan-ui/components/ScoreHeader.js +2 -3
  101. package/dist/scan-ui/components/SetupBanner.js +0 -6
  102. package/dist/scan-ui/format-helpers.js +61 -5
  103. package/dist/scan-ui/hooks/useResizeRepaint.js +25 -0
  104. package/dist/scan-ui/hooks/useScan.js +16 -2
  105. package/dist/scan-ui/launch.js +7 -4
  106. package/dist/scan-ui/shims.js +11 -4
  107. package/dist/scripts/detect.js +7 -2
  108. package/dist/scripts/gate.js +1 -1
  109. package/dist/service/state.js +27 -8
  110. package/dist/service/trust-refresh.js +92 -0
  111. package/dist/service/worker.js +23 -1
  112. package/dist/setup/activate-shell.js +28 -0
  113. package/dist/setup/git-hook.js +49 -4
  114. package/dist/setup/plan.js +73 -39
  115. package/dist/setup-ui/gate.js +39 -0
  116. package/dist/setup-ui/offer.js +42 -0
  117. package/dist/setup-ui/selector.js +27 -0
  118. package/dist/setup-ui/tasks.js +56 -0
  119. package/dist/setup-ui/wizard.js +225 -0
  120. package/dist/state/cooldown-held.js +66 -0
  121. package/dist/state/index.js +1 -0
  122. package/dist/state/locks.js +4 -2
  123. package/dist/state/store.js +2 -1
  124. package/dist/util/external-tool.js +25 -0
  125. package/dist/util/git.js +10 -3
  126. package/dist/util/report-writer.js +57 -0
  127. package/dist/verify/local.js +240 -42
  128. package/dist/verify/package-check.js +84 -28
  129. package/dist/verify/preflight.js +242 -49
  130. package/dist/verify/render.js +15 -1
  131. package/npm-shrinkwrap.json +2383 -0
  132. package/package.json +6 -8
  133. package/NOTICE +0 -5
  134. package/dist/commands/completion.js +0 -117
  135. package/dist/commands/explain.js +0 -236
  136. package/dist/commands/unavailable.js +0 -11
  137. package/dist/scripts/rebuild.js +0 -28
  138. package/dist/telemetry/events.js +0 -40
@@ -1,53 +1,100 @@
1
+ import { dirname } from "node:path";
2
+ import { proxyUrlWithAuth, readProxyAuthToken } from "../proxy/auth.js";
3
+ // The client read timeout must exceed the proxy's per-artifact verdict ceiling
4
+ // (DG_INSTALL_VERDICT_TIMEOUT_MS, default 240s) plus download headroom, so the
5
+ // package manager never gives up on a legitimately-slow verify. Overridable for
6
+ // constrained environments via DG_CLIENT_READ_TIMEOUT_S.
7
+ function clientReadTimeoutSeconds(env) {
8
+ const override = Number(env.DG_CLIENT_READ_TIMEOUT_S);
9
+ if (Number.isFinite(override) && override > 0) {
10
+ return Math.ceil(override);
11
+ }
12
+ const verdictMs = Number(env.DG_INSTALL_VERDICT_TIMEOUT_MS);
13
+ const verdictSeconds = Number.isFinite(verdictMs) && verdictMs > 0 ? verdictMs / 1000 : 240;
14
+ return Math.ceil(verdictSeconds + 60);
15
+ }
1
16
  export function buildProxyChildEnv(options) {
17
+ const authToken = readProxyAuthToken(dirname(options.caBundlePath));
18
+ const proxyUrl = authToken ? proxyUrlWithAuth(options.proxyUrl, authToken) : options.proxyUrl;
2
19
  const env = {
3
20
  ...options.baseEnv,
4
21
  DG_PROXY_ACTIVE: "1"
5
22
  };
6
- const noProxy = mergeNoProxy(options.baseEnv.NO_PROXY ?? options.baseEnv.no_proxy, options.noProxyHosts ?? ["127.0.0.1", "localhost"]);
23
+ // The wrapped package manager and every lifecycle/postinstall script it runs
24
+ // are untrusted. The dg account credential must never reach them; only the
25
+ // trusted proxy worker (which makes the authenticated API calls) keeps it.
26
+ delete env.DG_API_KEY;
27
+ delete env.DG_API_TOKEN;
28
+ // dg fully controls NO_PROXY (loopback only). An inherited NO_PROXY that names
29
+ // the registry (or a `*`/`.org` glob) would route the manager straight past the
30
+ // firewall, so the inherited value is dropped, not merged.
31
+ const noProxy = (options.noProxyHosts ?? ["127.0.0.1", "localhost"]).join(",");
32
+ env.NO_PROXY = noProxy;
33
+ env.no_proxy = noProxy;
34
+ const readTimeoutSeconds = clientReadTimeoutSeconds(options.baseEnv);
7
35
  if (["npm", "npx", "pnpm", "pnpx", "yarn"].includes(options.manager)) {
8
- env.HTTP_PROXY = options.proxyUrl;
9
- env.HTTPS_PROXY = options.proxyUrl;
10
- env.npm_config_proxy = options.proxyUrl;
11
- env.npm_config_https_proxy = options.proxyUrl;
12
- env.NO_PROXY = noProxy;
36
+ env.HTTP_PROXY = proxyUrl;
37
+ env.HTTPS_PROXY = proxyUrl;
38
+ env.npm_config_proxy = proxyUrl;
39
+ env.npm_config_https_proxy = proxyUrl;
13
40
  env.NODE_EXTRA_CA_CERTS = options.caBundlePath;
41
+ // The proxy withholds every artifact byte until its verdict resolves, so the
42
+ // package manager sees no response data until then. Its own network read
43
+ // timeout must outlast the proxy's worst-case verdict wait or it aborts a
44
+ // legitimate install mid-verify (pip's 15s default is what made large cold
45
+ // installs fail). Set it explicitly so a user-lowered global config can't
46
+ // undercut the proxy.
47
+ env.npm_config_fetch_timeout = String(readTimeoutSeconds * 1000);
48
+ if (options.manager === "yarn") {
49
+ env.YARN_NETWORK_TIMEOUT = String(readTimeoutSeconds * 1000);
50
+ }
51
+ if (options.manager === "yarn") {
52
+ // Yarn Berry (2+) ignores HTTP(S)_PROXY/npm_config_*; without its own
53
+ // proxy + CA config it fetches straight from the registry, bypassing the
54
+ // firewall. Classic Yarn ignores these YARN_ vars, so set them for both.
55
+ env.YARN_HTTP_PROXY = proxyUrl;
56
+ env.YARN_HTTPS_PROXY = proxyUrl;
57
+ env.YARN_HTTPS_CA_FILE_PATH = options.caBundlePath;
58
+ }
14
59
  if (options.cacheDir) {
15
60
  env.npm_config_cache = options.cacheDir;
16
61
  if (options.manager === "yarn") {
62
+ // Both: YARN_CACHE_FOLDER is Berry's per-project cache, but Berry 2+
63
+ // defaults to a shared global cache (~/.yarn/berry) that a warm install
64
+ // serves from without ever fetching, so redirect that too.
17
65
  env.YARN_CACHE_FOLDER = options.cacheDir;
66
+ env.YARN_GLOBAL_FOLDER = options.cacheDir;
18
67
  }
19
68
  }
20
69
  return env;
21
70
  }
22
71
  if (options.manager === "pip" || options.manager === "pipx") {
23
- env.http_proxy = options.proxyUrl;
24
- env.https_proxy = options.proxyUrl;
25
- env.no_proxy = noProxy;
72
+ env.http_proxy = proxyUrl;
73
+ env.https_proxy = proxyUrl;
26
74
  env.REQUESTS_CA_BUNDLE = options.caBundlePath;
27
75
  env.PIP_CERT = options.caBundlePath;
28
76
  env.PIP_NO_CACHE_DIR = "1";
77
+ env.PIP_DEFAULT_TIMEOUT = String(readTimeoutSeconds);
29
78
  return env;
30
79
  }
31
80
  if (options.manager === "uv" || options.manager === "uvx") {
32
- env.HTTP_PROXY = options.proxyUrl;
33
- env.HTTPS_PROXY = options.proxyUrl;
34
- env.ALL_PROXY = options.proxyUrl;
35
- env.NO_PROXY = noProxy;
81
+ env.HTTP_PROXY = proxyUrl;
82
+ env.HTTPS_PROXY = proxyUrl;
83
+ env.ALL_PROXY = proxyUrl;
36
84
  env.SSL_CERT_FILE = options.caBundlePath;
37
85
  env.UV_NO_CACHE = "1";
86
+ env.UV_HTTP_TIMEOUT = String(readTimeoutSeconds);
38
87
  return env;
39
88
  }
40
- env.HTTPS_PROXY = options.proxyUrl;
41
- env.https_proxy = options.proxyUrl;
42
- env.http_proxy = options.proxyUrl;
43
- env.NO_PROXY = noProxy;
89
+ env.HTTPS_PROXY = proxyUrl;
90
+ env.https_proxy = proxyUrl;
91
+ env.http_proxy = proxyUrl;
44
92
  env.CARGO_HTTP_CAINFO = options.caBundlePath;
45
- return env;
46
- }
47
- function mergeNoProxy(existing, required) {
48
- const values = new Set((existing ?? "").split(",").map((part) => part.trim()).filter(Boolean));
49
- for (const host of required) {
50
- values.add(host);
93
+ env.CARGO_HTTP_TIMEOUT = String(readTimeoutSeconds);
94
+ if (options.cacheDir) {
95
+ // cargo serves a cached crate from ~/.cargo with zero network, skipping the
96
+ // firewall; pointing CARGO_HOME at the throwaway dir forces a re-fetch.
97
+ env.CARGO_HOME = options.cacheDir;
51
98
  }
52
- return [...values].join(",");
99
+ return env;
53
100
  }
@@ -6,24 +6,26 @@ import { matchDecision, packageKey } from "../decisions/apply.js";
6
6
  import { offerRememberOnIo } from "../decisions/remember-prompt.js";
7
7
  import { provenanceDowngradeLine } from "../presentation/provenance.js";
8
8
  import { defaultPromptIo } from "../install-ui/prompt.js";
9
- import { cooldownRequestParam, formatCooldownDuration, formatPackageAge, isCooldownExempt } from "../policy/cooldown.js";
9
+ import { cooldownRequestParam, formatCooldownDuration, formatPackageAge, isCooldownExempt, isCooldownExemptByDgFile } from "../policy/cooldown.js";
10
10
  import { findProjectRoot, loadDgFile, resolveAcceptedBy, warnUnreadableDgFile } from "../project/dgfile.js";
11
11
  import { parsePipReportInstallCount, parsePipReportInstallSet } from "./pip-report.js";
12
- export function resolvePreflightCooldown(env) {
12
+ import { resolveSpawnInvocation } from "./spawn-invocation.js";
13
+ export function resolvePreflightCooldown(env, ecosystem) {
13
14
  try {
14
15
  const config = loadUserConfig(env);
15
- const param = cooldownRequestParam(config, env, "pypi", "");
16
- return param ? { param, exempt: config.cooldown.exempt } : undefined;
16
+ const param = cooldownRequestParam(config, env, ecosystem, "");
17
+ return param ? { param, exempt: config.cooldown.exempt, ecosystem } : undefined;
17
18
  }
18
19
  catch {
19
20
  return undefined;
20
21
  }
21
22
  }
22
- function isQuarantined(pkg, context) {
23
+ function isQuarantined(pkg, context, dgExemptions = []) {
23
24
  if (!context || !pkg.cooldown) {
24
25
  return false;
25
26
  }
26
- if (isCooldownExempt(pkg.name, context.exempt, "pypi")) {
27
+ if (isCooldownExempt(pkg.name, context.exempt, context.ecosystem)
28
+ || isCooldownExemptByDgFile(pkg.name, context.ecosystem, dgExemptions)) {
27
29
  return false;
28
30
  }
29
31
  return pkg.cooldown.status === "quarantine"
@@ -70,6 +72,15 @@ export function cachedPipResolution(binary, args) {
70
72
  function pipResolutionKey(binary, args) {
71
73
  return JSON.stringify([binary, ...args]);
72
74
  }
75
+ // pip's dry-run resolve downloads sdists/wheels it cannot get PEP 658
76
+ // metadata for, so big dependency trees legitimately take minutes on a cold
77
+ // pip cache. A short timeout here silently skips the preflight (and with it
78
+ // the batched analyze + preverified handoff), pushing every package onto the
79
+ // slow per-artifact verdict path.
80
+ function pipResolveTimeoutMs(env) {
81
+ const raw = Number(env.DG_PIP_RESOLVE_TIMEOUT_MS);
82
+ return Number.isFinite(raw) && raw > 0 ? raw : 120_000;
83
+ }
73
84
  function resolvePipInstallSet(binary, args, env) {
74
85
  if (!binary)
75
86
  return Promise.resolve(UNRESOLVED);
@@ -87,9 +98,11 @@ function resolvePipInstallSet(binary, args, env) {
87
98
  };
88
99
  let child;
89
100
  try {
90
- child = spawn(binary, [...args, "--dry-run", "--report", "-", "--quiet"], {
101
+ const invocation = resolveSpawnInvocation(binary, [...args, "--dry-run", "--report", "-", "--quiet"]);
102
+ child = spawn(invocation.command, [...invocation.args], {
91
103
  env,
92
- stdio: ["ignore", "pipe", "ignore"]
104
+ stdio: ["ignore", "pipe", "ignore"],
105
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments
93
106
  });
94
107
  }
95
108
  catch {
@@ -102,7 +115,7 @@ function resolvePipInstallSet(binary, args, env) {
102
115
  }
103
116
  catch { /* already exited */ }
104
117
  finish(UNRESOLVED);
105
- }, 30_000);
118
+ }, pipResolveTimeoutMs(env));
106
119
  child.stdout?.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
107
120
  child.on("error", () => finish(UNRESOLVED));
108
121
  child.on("close", (code) => finish(code === 0
@@ -154,7 +167,7 @@ export function renderProvenanceDowngrades(packages, out) {
154
167
  out.write(` ⚠ ${pkg.name}@${pkg.version}: ${line} (display only, verdict unchanged)\n`);
155
168
  }
156
169
  }
157
- export async function runInstallPreflight(manager, binary, childArgs, env, cwd = process.cwd()) {
170
+ export async function runInstallPreflight(manager, binary, childArgs, env, cwd = process.cwd(), beforeInteraction) {
158
171
  if (manager !== "pip" || !binary) {
159
172
  return PROCEED;
160
173
  }
@@ -165,18 +178,42 @@ export async function runInstallPreflight(manager, binary, childArgs, env, cwd =
165
178
  pipResolutions.set(key, resolution);
166
179
  }
167
180
  const set = resolution.set;
168
- if (!set || set.length === 0) {
181
+ if (!set) {
182
+ process.stderr.write(" dg: could not resolve the install set ahead of time — verifying during install instead\n");
183
+ return PROCEED;
184
+ }
185
+ if (set.length === 0) {
169
186
  return PROCEED;
170
187
  }
171
- const cooldownContext = resolvePreflightCooldown(env);
188
+ const cooldownContext = resolvePreflightCooldown(env, "pypi");
172
189
  let verdicts;
173
190
  try {
174
191
  verdicts = await analyzePackages(set.map((pkg) => ({ name: pkg.name, version: pkg.version })), { ecosystem: "pypi", env, ...(cooldownContext ? { cooldown: cooldownContext.param } : {}) });
175
192
  }
176
193
  catch {
194
+ process.stderr.write(" dg: pre-install check unavailable — verifying during install instead\n");
177
195
  return PROCEED;
178
196
  }
179
- return decideFromVerdicts(verdicts.packages, defaultPromptIo(), cooldownContext, resolvePreflightDecisions("pypi", cwd, env));
197
+ beforeInteraction?.();
198
+ const decision = await decideFromVerdicts(verdicts.packages, defaultPromptIo(), cooldownContext, resolvePreflightDecisions("pypi", cwd, env));
199
+ if (!decision.proceed || decision.forceOverride) {
200
+ return decision;
201
+ }
202
+ const preverified = preverifiedEntries(verdicts.packages, cooldownContext !== undefined);
203
+ return preverified.length > 0 ? { ...decision, preverified } : decision;
204
+ }
205
+ export function preverifiedEntries(packages, cooldownRequested) {
206
+ return packages
207
+ .filter((pkg) => pkg.action === "pass" || pkg.action === "warn")
208
+ .map((pkg) => ({
209
+ ecosystem: "pypi",
210
+ name: pkg.name,
211
+ version: pkg.version,
212
+ action: pkg.action,
213
+ ...(pkg.reasons[0] ? { reason: pkg.reasons[0] } : {}),
214
+ ...(typeof pkg.artifactSha256 === "string" && pkg.artifactSha256.length > 0 ? { scannedSha256: pkg.artifactSha256 } : {}),
215
+ cooldownEvaluated: cooldownRequested && pkg.cooldown !== undefined
216
+ }));
180
217
  }
181
218
  export async function decideFromVerdicts(packages, io, cooldownContext, decisions = null) {
182
219
  if (io.isTTY) {
@@ -185,7 +222,7 @@ export async function decideFromVerdicts(packages, io, cooldownContext, decision
185
222
  const covered = [];
186
223
  const entries = packages
187
224
  .map((pkg) => {
188
- const viaCooldown = isQuarantined(pkg, cooldownContext) && pkg.action !== "block";
225
+ const viaCooldown = isQuarantined(pkg, cooldownContext, decisions?.file.cooldownExemptions ?? []) && pkg.action !== "block";
189
226
  const action = viaCooldown ? "block" : pkg.action ?? "pass";
190
227
  return { pkg, action, viaCooldown };
191
228
  })
@@ -1,7 +1,8 @@
1
- import { createLaunchPlan, runWithProductionProxyLive, EXIT_INSTALL_BLOCKED } from "./run.js";
1
+ import { createLaunchPlan, prepareProxyWorker, runWithProductionProxyLive, EXIT_INSTALL_BLOCKED } from "./run.js";
2
2
  import { runInstallPreflight } from "./install-preflight.js";
3
3
  import { isSupportedPackageManager } from "./classify.js";
4
4
  import { isCiEnv, resolvePresentation } from "../presentation/mode.js";
5
+ import { startPrepSpinner } from "../install-ui/prep-spinner.js";
5
6
  const FALL_THROUGH = { handled: false };
6
7
  export async function maybeRunLiveInstall(args, options = {}) {
7
8
  const env = options.env ?? process.env;
@@ -17,24 +18,43 @@ export async function maybeRunLiveInstall(args, options = {}) {
17
18
  if (plan.classification.kind !== "protected" || !plan.realBinary.path) {
18
19
  return FALL_THROUGH;
19
20
  }
21
+ const appLoading = import("../install-ui/live-install-app.js");
22
+ appLoading.catch(() => undefined);
23
+ let prepared = forceOverride ? undefined : prepareProxyWorker(plan.classification, env);
20
24
  let effectiveOverride = forceOverride;
25
+ let preverified;
21
26
  if (!forceOverride) {
22
- const preflight = await runInstallPreflight(manager, plan.realBinary.path, childArgs, env);
27
+ const spinner = startPrepSpinner("DG preparing…");
28
+ let preflight;
29
+ try {
30
+ preflight = await runInstallPreflight(manager, plan.realBinary.path, childArgs, env, process.cwd(), spinner.stop);
31
+ }
32
+ finally {
33
+ spinner.stop();
34
+ }
23
35
  if (!preflight.proceed) {
36
+ prepared?.discard();
24
37
  return { handled: true, result: { exitCode: EXIT_INSTALL_BLOCKED, stdout: "", stderr: " Install cancelled.\n" } };
25
38
  }
26
39
  effectiveOverride = preflight.forceOverride;
40
+ preverified = preflight.preverified;
41
+ if (effectiveOverride) {
42
+ prepared?.discard();
43
+ prepared = undefined;
44
+ }
27
45
  }
28
46
  const runOptions = {
29
47
  env,
30
- ...(effectiveOverride ? { forceOverride: effectiveOverride } : {})
48
+ ...(effectiveOverride ? { forceOverride: effectiveOverride } : {}),
49
+ ...(preverified && preverified.length > 0 && !effectiveOverride ? { preverified } : {})
31
50
  };
32
- const { renderLiveInstall } = await import("../install-ui/live-install-app.js");
33
51
  try {
34
- const result = await renderLiveInstall((onView) => runWithProductionProxyLive(plan, childArgs, runOptions, onView));
52
+ const { renderLiveInstall } = await appLoading;
53
+ const result = await renderLiveInstall((onView) => runWithProductionProxyLive(plan, childArgs, runOptions, onView, prepared));
35
54
  return { handled: true, result };
36
55
  }
37
56
  catch (error) {
57
+ prepared?.discard();
38
58
  return {
39
59
  handled: true,
40
60
  result: {
@@ -1,23 +1,26 @@
1
1
  const credentialUrlPattern = /\b([a-z][a-z0-9+.-]{0,31}:\/\/)([^/\s:@]+):([^/\s@]+)@/gi;
2
2
  const authHeaderPattern = /\b(proxy-authorization|authorization):\s*[^\r\n]+/gi;
3
- const tokenAssignmentPattern = /(?<![A-Za-z0-9])([A-Za-z0-9_-]*(?:secret[_-]key|access[_-]key|api[_-]key|token|password|secret))=([^\s]+)/gi;
3
+ // Matches both KEY=value and colon-form (KEY: value, YAML) secret assignments;
4
+ // the separator and any opening quote are preserved in the replacement.
5
+ const tokenAssignmentPattern = /(?<![A-Za-z0-9])([A-Za-z0-9_-]*(?:secret[_-]key|access[_-]key|api[_-]key|token|password|secret))(\s*[:=]\s*)("?)([^\s"]+)/gi;
4
6
  const npmrcAuthPattern = /(_authToken|_auth|_password)\s*=\s*("[^"]*"|[^\s;,]+)/gi;
5
7
  const jsonSecretPattern = /("[A-Za-z0-9_.$-]*(?:secret[_-]key|access[_-]key|api[_-]key|token|password|secret)"\s*:\s*)"(?:[^"\\]|\\.)*"/gi;
6
8
  const bearerPattern = /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/g;
7
- const knownTokenShapePattern = /\b(npm_[A-Za-z0-9]{36}|gh[pousr]_[A-Za-z0-9]{36,255}|pypi-[A-Za-z0-9_-]{20,}|glpat-[A-Za-z0-9_-]{20,}|xox[bp]-[A-Za-z0-9-]{10,})\b/g;
9
+ // Bare tokens (not in URL userinfo or KEY=value form), by their published shape.
10
+ const knownTokenShapePattern = /\b(npm_[A-Za-z0-9]{36}|gh[pousr]_[A-Za-z0-9]{36,255}|github_pat_[A-Za-z0-9_]{22,255}|pypi-[A-Za-z0-9_-]{20,}|glpat-[A-Za-z0-9_-]{20,}|hf_[A-Za-z0-9]{30,}|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|sk_(?:live|test)_[A-Za-z0-9]{20,}|rk_live_[A-Za-z0-9]{20,}|sk-(?:proj-)?[A-Za-z0-9_-]{20,}|xox[abdeprs]-[A-Za-z0-9-]{10,})\b/g;
8
11
  export function redactSecrets(text) {
9
12
  return text
10
13
  .replace(credentialUrlPattern, "$1<redacted>@")
11
14
  .replace(authHeaderPattern, (_match, header) => `${header}: <redacted>`)
12
15
  .replace(npmrcAuthPattern, "$1=<redacted>")
13
16
  .replace(jsonSecretPattern, '$1"<redacted>"')
14
- .replace(tokenAssignmentPattern, "$1=<redacted>")
17
+ .replace(tokenAssignmentPattern, "$1$2$3<redacted>")
15
18
  .replace(bearerPattern, "Bearer <redacted>")
16
19
  .replace(knownTokenShapePattern, "<redacted>");
17
20
  }
18
21
  const STREAM_FLUSH_QUIET_MS = 80;
19
22
  const SECRET_TAIL_SCAN_CHARS = 80;
20
- const secretTailPattern = /(Bearer\s+[\w.~+/=-]*|npm_[A-Za-z0-9]*|gh[pousr]_[A-Za-z0-9]*|pypi-[\w-]*|glpat-[\w-]*|xox[bp]-[\w-]*|[\w-]*(?:_authToken|_auth|_password|secret[_-]key|access[_-]key|api[_-]key|token|password|secret)=\S*|"[\w.$-]*(?:secret[_-]key|access[_-]key|api[_-]key|token|password|secret)"\s*:\s*"?[^"\n]*)$/i;
23
+ const secretTailPattern = /(Bearer\s+[\w.~+/=-]*|npm_[A-Za-z0-9]*|gh[pousr]_[A-Za-z0-9]*|github_pat_[A-Za-z0-9_]*|pypi-[\w-]*|glpat-[\w-]*|hf_[A-Za-z0-9]*|AKIA[0-9A-Z]*|ASIA[0-9A-Z]*|AIza[\w-]*|sk_(?:live|test)_[A-Za-z0-9]*|rk_live_[A-Za-z0-9]*|sk-(?:proj-)?[\w-]*|xox[abdeprs]-[\w-]*|[\w-]*(?:_authToken|_auth|_password|secret[_-]key|access[_-]key|api[_-]key|token|password|secret)\s*[:=]\s*"?\S*|"[\w.$-]*(?:secret[_-]key|access[_-]key|api[_-]key|token|password|secret)"\s*:\s*"?[^"\n]*)$/i;
21
24
  export function createStreamRedactor(emit) {
22
25
  let pending = "";
23
26
  let timer;
@@ -4,11 +4,13 @@ import { matchDecision } from "../decisions/apply.js";
4
4
  import { offerRememberOnIo } from "../decisions/remember-prompt.js";
5
5
  import { renderInstallDecision } from "../install-ui/block-render.js";
6
6
  import { defaultPromptIo } from "../install-ui/prompt.js";
7
+ import { normalizePypiName } from "../policy/pypi-name.js";
7
8
  import { resolveAcceptedBy } from "../project/dgfile.js";
8
9
  import { enforceProtectedInstall } from "../proxy/enforcement.js";
9
10
  import { classifyPackageManagerInvocation, isSupportedPackageManager } from "./classify.js";
10
11
  import { actionRank, promptPreflightYesNo, recordPreflightApprovals, renderCoveredWarns, renderProvenanceDowngrades, resolvePreflightDecisions } from "./install-preflight.js";
11
12
  import { redactSecrets } from "./output-redaction.js";
13
+ import { startPrepSpinner } from "../install-ui/prep-spinner.js";
12
14
  const ECOSYSTEM_BY_MANAGER = {
13
15
  npm: "npm",
14
16
  pnpm: "npm",
@@ -44,11 +46,13 @@ export async function maybePreflightInstallPrompt(args, options = {}) {
44
46
  const decisions = options.decisionsCwd ? resolvePreflightDecisions(ecosystem, options.decisionsCwd, env) : null;
45
47
  const flagged = [];
46
48
  const covered = [];
49
+ const spinner = startPrepSpinner("DG preparing…");
47
50
  try {
48
51
  const response = await (options.analyze ?? analyzePackages)(specs.map((spec) => ({ name: spec.name, version: spec.version })), { ecosystem, env });
52
+ spinner.stop();
49
53
  renderProvenanceDowngrades(response.packages, io.output);
50
54
  for (const spec of specs) {
51
- const pkg = response.packages.find((entry) => entry.name === spec.name && entry.version === spec.version);
55
+ const pkg = response.packages.find((entry) => sameSpecName(entry.name, spec.name, ecosystem) && entry.version === spec.version);
52
56
  const action = pkg?.action ?? "pass";
53
57
  if (action === "pass") {
54
58
  continue;
@@ -68,6 +72,9 @@ export async function maybePreflightInstallPrompt(args, options = {}) {
68
72
  catch {
69
73
  return FALL_THROUGH;
70
74
  }
75
+ finally {
76
+ spinner.stop();
77
+ }
71
78
  if (covered.length > 0) {
72
79
  renderCoveredWarns(covered, decisions, io.output);
73
80
  recordPreflightApprovals(covered.map((pkg) => ({ name: pkg.name, version: pkg.version, action: "warn" })));
@@ -131,6 +138,12 @@ export async function maybePreflightInstallPrompt(args, options = {}) {
131
138
  result: { exitCode: 1, stdout: "", stderr: "Declined. Nothing was installed.\n" }
132
139
  };
133
140
  }
141
+ function sameSpecName(entryName, specName, ecosystem) {
142
+ if (ecosystem === "pypi") {
143
+ return normalizePypiName(entryName) === normalizePypiName(specName);
144
+ }
145
+ return entryName === specName;
146
+ }
134
147
  function asFlaggedPackage(entry) {
135
148
  return {
136
149
  name: entry.spec.name,