@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,8 +1,9 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { npmExecutable } from "../publish-set/npm.js";
2
+ import { toolInvocation } from "../util/external-tool.js";
3
3
  import { EXIT_USAGE } from "./types.js";
4
4
  import { dgVersion } from "./version.js";
5
5
  const PACKAGE_NAME = "@westbayberry/dg";
6
+ const VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/u;
6
7
  export const updateCommand = {
7
8
  name: "update",
8
9
  summary: "Check for dg CLI updates.",
@@ -26,7 +27,7 @@ function runUpdateCommand(args, commandName) {
26
27
  if (parsed.format === "json") {
27
28
  return {
28
29
  exitCode: report.status === "unknown" ? 1 : 0,
29
- stdout: `${JSON.stringify(report, null, 2)}\n`,
30
+ stdout: `${JSON.stringify({ schemaVersion: 1, ...report }, null, 2)}\n`,
30
31
  stderr: ""
31
32
  };
32
33
  }
@@ -62,17 +63,22 @@ function parseUpdateArgs(args) {
62
63
  };
63
64
  }
64
65
  export function readLatestVersion(timeoutMs = 5000) {
65
- const injected = process.env.DG_UPDATE_LATEST_VERSION;
66
+ const injected = process.env.NODE_ENV === "test" ? process.env.DG_UPDATE_LATEST_VERSION : undefined;
66
67
  if (injected) {
67
- return injected;
68
+ return validVersion(injected);
68
69
  }
69
- const result = spawnSync(npmExecutable(), ["view", PACKAGE_NAME, "version", "--json"], {
70
+ const invocation = toolInvocation("npm", ["view", PACKAGE_NAME, "version", "--json"]);
71
+ if (!invocation) {
72
+ return null;
73
+ }
74
+ const result = spawnSync(invocation.command, [...invocation.args], {
70
75
  encoding: "utf8",
71
76
  env: {
72
77
  ...process.env,
73
78
  npm_config_ignore_scripts: "true"
74
79
  },
75
- timeout: timeoutMs
80
+ timeout: timeoutMs,
81
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments
76
82
  });
77
83
  if (result.status !== 0) {
78
84
  return null;
@@ -83,12 +89,15 @@ export function readLatestVersion(timeoutMs = 5000) {
83
89
  }
84
90
  try {
85
91
  const parsed = JSON.parse(raw);
86
- return typeof parsed === "string" ? parsed : null;
92
+ return typeof parsed === "string" ? validVersion(parsed) : null;
87
93
  }
88
94
  catch {
89
- return raw.replace(/^"|"$/gu, "");
95
+ return validVersion(raw.replace(/^"|"$/gu, ""));
90
96
  }
91
97
  }
98
+ function validVersion(value) {
99
+ return VERSION_PATTERN.test(value) ? value : null;
100
+ }
92
101
  function buildUpdateReport(latestVersion) {
93
102
  if (!latestVersion) {
94
103
  return {
@@ -1,4 +1,4 @@
1
- import { EXIT_UNAVAILABLE, EXIT_USAGE } from "./types.js";
1
+ import { EXIT_ANALYSIS_INCOMPLETE, EXIT_UNAVAILABLE, EXIT_USAGE_VERDICT } from "./types.js";
2
2
  import { writeFileSync } from "node:fs";
3
3
  import { existsSync } from "node:fs";
4
4
  import { resolve } from "node:path";
@@ -22,14 +22,14 @@ export const verifyCommand = {
22
22
  ],
23
23
  examples: ["dg verify npm:react", "dg verify pypi:requests@2.31.0", "dg verify ./pkg.tgz --verbose", "dg verify package-lock.json --json"],
24
24
  details: [
25
- "dg verify npm:react (or pypi:requests, with an optional @version — defaults to latest) runs a real scanner check on a published package before you install it. This is a Pro/Team feature and requires dg login.",
25
+ "dg verify npm:react (or pypi:requests, with an optional @version — defaults to latest) runs a real scanner check on a published package before you install it. Signed-out runs show the verdict and top reasons; dg login unlocks full findings, license info, --json, and --output.",
26
26
  "Local paths, workspaces, tgz/zip/wheel artifacts, and lockfiles are verified offline as advisory preflight (free); proxy enforcement remains authoritative for network artifact fetches."
27
27
  ],
28
28
  handler: (context) => {
29
29
  const parsed = parseVerifyArgs(context.args);
30
30
  if ("error" in parsed) {
31
31
  return {
32
- exitCode: EXIT_USAGE,
32
+ exitCode: EXIT_USAGE_VERDICT,
33
33
  stdout: "",
34
34
  stderr: `dg verify: ${parsed.error}. Usage: dg verify <spec|path|lockfile> [--verbose] [--json|--sarif] [--output <path>]\n`
35
35
  };
@@ -52,7 +52,7 @@ export const verifyCommand = {
52
52
  }
53
53
  catch (error) {
54
54
  return {
55
- exitCode: 1,
55
+ exitCode: EXIT_ANALYSIS_INCOMPLETE,
56
56
  stdout: "",
57
57
  stderr: `dg verify could not write ${parsed.outputPath}: ${error instanceof Error ? error.message : "unknown write error"}\n`
58
58
  };
@@ -147,5 +147,8 @@ function exitCodeForReport(report) {
147
147
  if (report.status === "block") {
148
148
  return 2;
149
149
  }
150
- return report.status === "warn" || report.status === "error" ? 1 : 0;
150
+ if (report.status === "error") {
151
+ return EXIT_ANALYSIS_INCOMPLETE;
152
+ }
153
+ return report.status === "warn" ? 1 : 0;
151
154
  }
@@ -19,9 +19,7 @@ export const CONFIG_KEYS = Object.freeze([
19
19
  "cooldown.cargo.age",
20
20
  "cooldown.onUnknown",
21
21
  "cooldown.exempt",
22
- "audit.upload",
23
- "telemetry.enabled",
24
- "webhooks.enabled"
22
+ "audit.upload"
25
23
  ]);
26
24
  export const DEFAULT_CONFIG = Object.freeze({
27
25
  version: 1,
@@ -55,12 +53,6 @@ export const DEFAULT_CONFIG = Object.freeze({
55
53
  },
56
54
  audit: {
57
55
  upload: false
58
- },
59
- telemetry: {
60
- enabled: false
61
- },
62
- webhooks: {
63
- enabled: true
64
56
  }
65
57
  });
66
58
  export class ConfigError extends Error {
@@ -163,16 +155,18 @@ export function getConfigValue(config, key) {
163
155
  if (key === "cooldown.exempt") {
164
156
  return config.cooldown.exempt;
165
157
  }
166
- if (key === "audit.upload") {
167
- return String(config.audit.upload);
168
- }
169
- if (key === "telemetry.enabled") {
170
- return String(config.telemetry.enabled);
171
- }
172
- return String(config.webhooks.enabled);
158
+ return String(config.audit.upload);
173
159
  }
174
- export function listConfig(config) {
175
- return CONFIG_KEYS.map((key) => ({
160
+ export const ADVANCED_CONFIG_KEYS = new Set([
161
+ "org.id",
162
+ "policy.scriptHardening",
163
+ "scriptGate.observe",
164
+ "cooldown.npm.age",
165
+ "cooldown.pypi.age",
166
+ "cooldown.cargo.age"
167
+ ]);
168
+ export function listConfig(config, includeAdvanced = false) {
169
+ return CONFIG_KEYS.filter((key) => includeAdvanced || !ADVANCED_CONFIG_KEYS.has(key)).map((key) => ({
176
170
  key,
177
171
  value: getConfigValue(config, key)
178
172
  }));
@@ -266,26 +260,10 @@ export function setConfigValue(config, key, rawValue) {
266
260
  if (key === "cooldown.exempt") {
267
261
  return withCooldown(config, { exempt: parseCooldownExempt(rawValue) });
268
262
  }
269
- if (key === "audit.upload") {
270
- return {
271
- ...config,
272
- audit: {
273
- upload: parseBoolean(rawValue, key)
274
- }
275
- };
276
- }
277
- if (key === "telemetry.enabled") {
278
- return {
279
- ...config,
280
- telemetry: {
281
- enabled: parseBoolean(rawValue, key)
282
- }
283
- };
284
- }
285
263
  return {
286
264
  ...config,
287
- webhooks: {
288
- enabled: parseBoolean(rawValue, key)
265
+ audit: {
266
+ upload: parseBoolean(rawValue, key)
289
267
  }
290
268
  };
291
269
  }
@@ -309,8 +287,6 @@ function normalizeConfig(raw) {
309
287
  const gitHook = fieldObject(raw, "gitHook");
310
288
  const cooldown = fieldObject(raw, "cooldown");
311
289
  const audit = fieldObject(raw, "audit");
312
- const telemetry = fieldObject(raw, "telemetry");
313
- const webhooks = fieldObject(raw, "webhooks");
314
290
  const scriptHardening = fieldBoolean(policy, "policy.scriptHardening", "scriptHardening") ?? DEFAULT_CONFIG.policy.scriptHardening;
315
291
  return {
316
292
  version: 1,
@@ -344,12 +320,6 @@ function normalizeConfig(raw) {
344
320
  },
345
321
  audit: {
346
322
  upload: fieldBoolean(audit, "audit.upload", "upload") ?? DEFAULT_CONFIG.audit.upload
347
- },
348
- telemetry: {
349
- enabled: fieldBoolean(telemetry, "telemetry.enabled", "enabled") ?? DEFAULT_CONFIG.telemetry.enabled
350
- },
351
- webhooks: {
352
- enabled: fieldBoolean(webhooks, "webhooks.enabled", "enabled") ?? DEFAULT_CONFIG.webhooks.enabled
353
323
  }
354
324
  };
355
325
  }
@@ -1,7 +1,8 @@
1
- import { emitWebhookEvent, recordAuditEvent } from "../audit/events.js";
1
+ import { recordAuditEvent } from "../audit/events.js";
2
2
  import { loadUserConfig } from "../config/settings.js";
3
3
  import { promptText, promptYesNo } from "../install-ui/prompt.js";
4
- import { appendDecisions, saveDgFile } from "../project/dgfile.js";
4
+ import { dirname } from "node:path";
5
+ import { appendDecisions, mutateDgFile } from "../project/dgfile.js";
5
6
  import { promptLine as ttyPromptLine, promptYesNo as ttyPromptYesNo } from "../util/tty-prompt.js";
6
7
  import { findingFingerprint, packageKey } from "./apply.js";
7
8
  const defaultSyncPrompts = {
@@ -67,7 +68,7 @@ export function persistRemembered(file, packages, options) {
67
68
  reason: options.reason,
68
69
  acceptedBy: options.acceptedBy
69
70
  }));
70
- saveDgFile(appendDecisions(file, additions));
71
+ mutateDgFile(dirname(file.path), options.env, (current) => appendDecisions(current, additions));
71
72
  recordDecisionEvents("decision.accepted", packages.map((pkg) => `${pkg.ecosystem}:${packageKey(pkg.name, pkg.version)}`), options.reason, options.env);
72
73
  }
73
74
  export function recordDecisionEvents(type, packageNames, reason, env) {
@@ -86,12 +87,6 @@ export function recordDecisionEvents(type, packageNames, reason, env) {
86
87
  policyMode,
87
88
  createdAt: new Date().toISOString()
88
89
  };
89
- try {
90
- recordAuditEvent(event, env);
91
- emitWebhookEvent(event, env);
92
- }
93
- catch {
94
- // audit trail is best-effort; the decision write already succeeded
95
- }
90
+ recordAuditEvent(event, env);
96
91
  }
97
92
  }
@@ -0,0 +1,198 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import chalk from "chalk";
5
+ import { existsSync, readdirSync } from "node:fs";
6
+ import { basename, join, resolve, sep } from "node:path";
7
+ import { exportDestinations, resolveExportPath, userHomeDir, writeReportAtomic } from "../util/report-writer.js";
8
+ export function loginRequiredToast() {
9
+ return `${chalk.yellow("Sign in to export:")} ${chalk.cyan.bold("dg login")} ${chalk.dim("(free account)")}`;
10
+ }
11
+ const PATH_DISPLAY_MAX = 48;
12
+ function cursorBeforeExtension(name) {
13
+ const slash = name.lastIndexOf("/");
14
+ const dot = name.indexOf(".", slash + 2);
15
+ return dot > slash + 1 ? dot : name.length;
16
+ }
17
+ function fieldFor(name) {
18
+ return { value: name, cursor: cursorBeforeExtension(name) };
19
+ }
20
+ function abbreviateHome(dir, home) {
21
+ if (dir === home)
22
+ return "~";
23
+ if (dir.startsWith(home + sep))
24
+ return `~${dir.slice(home.length)}`;
25
+ return dir;
26
+ }
27
+ function truncatePath(text) {
28
+ if (text.length <= PATH_DISPLAY_MAX)
29
+ return text;
30
+ return `…${text.slice(text.length - PATH_DISPLAY_MAX + 1)}`;
31
+ }
32
+ function prefillFor(dir, cwd, home, filename) {
33
+ if (resolve(dir) === resolve(cwd))
34
+ return filename;
35
+ if (dir === home)
36
+ return `~/${filename}`;
37
+ if (dir.startsWith(home + sep))
38
+ return `~${dir.slice(home.length)}/${filename}`;
39
+ return join(dir, filename);
40
+ }
41
+ function completePath(value, cwd) {
42
+ const slash = value.lastIndexOf("/");
43
+ const dir = slash < 0 ? "" : value.slice(0, slash + 1);
44
+ const fragment = slash < 0 ? value : value.slice(slash + 1);
45
+ let entries;
46
+ try {
47
+ entries = readdirSync(resolveExportPath(dir === "" ? "." : dir, cwd), { withFileTypes: true })
48
+ .map((entry) => ({ name: entry.name, isDir: entry.isDirectory() }));
49
+ }
50
+ catch {
51
+ return value;
52
+ }
53
+ const match = entries
54
+ .filter((entry) => entry.name.toLowerCase().startsWith(fragment.toLowerCase()))
55
+ .sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1))[0];
56
+ if (!match)
57
+ return value;
58
+ return `${dir}${match.name}${match.isDir ? "/" : ""}`;
59
+ }
60
+ export const ExportDialog = ({ options, theme, cwd, onDone, env = process.env }) => {
61
+ const multi = options.length > 1;
62
+ const [stage, setStage] = useState(multi ? "format" : "dest");
63
+ const [selected, setSelected] = useState(0);
64
+ const [destIndex, setDestIndex] = useState(0);
65
+ const [field, setField] = useState({ value: "", cursor: 0 });
66
+ const home = useMemo(() => userHomeDir(env), [env]);
67
+ const destinations = useMemo(() => exportDestinations(cwd, env), [cwd, env]);
68
+ const option = options[selected];
69
+ const filename = basename(option?.defaultName ?? "");
70
+ const rowCount = destinations.length + 1;
71
+ const writeTo = (target) => {
72
+ if (!option)
73
+ return;
74
+ try {
75
+ writeReportAtomic(target, option.render());
76
+ onDone({ path: target });
77
+ }
78
+ catch (error) {
79
+ onDone({ error: error instanceof Error ? error.message : "write failed" });
80
+ }
81
+ };
82
+ const openEditor = (dest) => {
83
+ const prefill = dest ? prefillFor(dest.dir, cwd, home, filename) : filename;
84
+ setField(fieldFor(prefill));
85
+ setStage("path");
86
+ };
87
+ const insertAtCursor = (text) => {
88
+ setField((f) => ({ value: f.value.slice(0, f.cursor) + text + f.value.slice(f.cursor), cursor: f.cursor + text.length }));
89
+ };
90
+ const moveCursor = (to) => {
91
+ setField((f) => ({ ...f, cursor: Math.max(0, Math.min(f.value.length, to(f))) }));
92
+ };
93
+ useInput((input, key) => {
94
+ if (stage === "format") {
95
+ if (key.escape || input === "q") {
96
+ onDone(null);
97
+ return;
98
+ }
99
+ if (key.return) {
100
+ setStage("dest");
101
+ return;
102
+ }
103
+ if (key.upArrow || input === "k") {
104
+ setSelected((s) => Math.max(0, s - 1));
105
+ return;
106
+ }
107
+ if (key.downArrow || input === "j") {
108
+ setSelected((s) => Math.min(options.length - 1, s + 1));
109
+ return;
110
+ }
111
+ return;
112
+ }
113
+ if (stage === "dest") {
114
+ if (key.escape) {
115
+ if (multi)
116
+ setStage("format");
117
+ else
118
+ onDone(null);
119
+ return;
120
+ }
121
+ if (input === "t") {
122
+ openEditor(destinations[destIndex]);
123
+ return;
124
+ }
125
+ if (key.upArrow || input === "k") {
126
+ setDestIndex((d) => Math.max(0, d - 1));
127
+ return;
128
+ }
129
+ if (key.downArrow || input === "j") {
130
+ setDestIndex((d) => Math.min(rowCount - 1, d + 1));
131
+ return;
132
+ }
133
+ if (key.return) {
134
+ const dest = destinations[destIndex];
135
+ if (dest)
136
+ writeTo(join(dest.dir, filename));
137
+ else
138
+ openEditor(destinations[destIndex - 1] ?? destinations[0]);
139
+ }
140
+ return;
141
+ }
142
+ if (key.escape) {
143
+ setStage("dest");
144
+ return;
145
+ }
146
+ if (key.return) {
147
+ const trimmed = field.value.trim();
148
+ if (trimmed.length > 0)
149
+ writeTo(resolveExportPath(trimmed, cwd));
150
+ return;
151
+ }
152
+ if (key.tab) {
153
+ setField((f) => fieldFor(completePath(f.value, cwd)));
154
+ return;
155
+ }
156
+ if (key.leftArrow) {
157
+ moveCursor((f) => f.cursor - 1);
158
+ return;
159
+ }
160
+ if (key.rightArrow) {
161
+ moveCursor((f) => f.cursor + 1);
162
+ return;
163
+ }
164
+ if (key.ctrl && input === "a") {
165
+ moveCursor(() => 0);
166
+ return;
167
+ }
168
+ if (key.ctrl && input === "e") {
169
+ moveCursor((f) => f.value.length);
170
+ return;
171
+ }
172
+ if (key.delete || key.backspace) {
173
+ setField((f) => (f.cursor === 0 ? f : { value: f.value.slice(0, f.cursor - 1) + f.value.slice(f.cursor), cursor: f.cursor - 1 }));
174
+ return;
175
+ }
176
+ if (input && !key.ctrl && !key.meta && /^[\x20-\x7e]+$/.test(input)) {
177
+ insertAtCursor(input);
178
+ }
179
+ });
180
+ const target = field.value.trim().length > 0 ? resolveExportPath(field.value, cwd) : null;
181
+ const targetExists = target !== null && existsSync(target);
182
+ const beforeCursor = field.value.slice(0, field.cursor);
183
+ const atCursor = field.value.charAt(field.cursor);
184
+ const afterCursor = field.value.slice(field.cursor + 1);
185
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingLeft: 2, paddingRight: 2, children: [_jsx(Text, { bold: true, children: "Export" }), _jsx(Text, { children: "" }), stage === "format" && options.map((opt, idx) => {
186
+ const isSelected = idx === selected;
187
+ return (_jsxs(Box, { children: [_jsx(Box, { width: 5, flexShrink: 0, children: _jsxs(Text, { children: [" ", isSelected ? chalk.cyan("●") : chalk.dim("○")] }) }), _jsx(Box, { flexShrink: 1, children: _jsx(Text, { wrap: "truncate-end", children: isSelected ? chalk.bold(opt.label) : opt.label }) })] }, opt.label));
188
+ }), stage === "dest" && (_jsxs(_Fragment, { children: [destinations.map((dest, idx) => {
189
+ const isSelected = idx === destIndex;
190
+ const replaces = existsSync(join(dest.dir, filename));
191
+ const shown = truncatePath(`${abbreviateHome(dest.dir, home)}/${filename}`.replace(/\/+/g, "/"));
192
+ return (_jsxs(Box, { children: [_jsx(Box, { width: 3, flexShrink: 0, children: _jsxs(Text, { children: [" ", isSelected ? chalk.cyan("▸") : " "] }) }), _jsx(Box, { width: 14, flexShrink: 0, children: _jsx(Text, { children: isSelected ? chalk.bold(dest.label) : dest.label }) }), _jsx(Box, { flexShrink: 1, children: _jsxs(Text, { wrap: "truncate-start", children: [theme.paint("muted", shown), replaces ? theme.paint("warn", " (replaces)") : ""] }) })] }, dest.label));
193
+ }), _jsxs(Box, { children: [_jsx(Box, { width: 3, flexShrink: 0, children: _jsxs(Text, { children: [" ", destIndex === destinations.length ? chalk.cyan("▸") : " "] }) }), _jsx(Box, { flexShrink: 1, children: _jsx(Text, { children: destIndex === destinations.length ? chalk.bold("Type a path…") : theme.paint("muted", "Type a path…") }) })] })] })), stage === "path" && (_jsxs(_Fragment, { children: [option && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: option.label }), _jsxs(Text, { wrap: "truncate-start", children: [chalk.cyan("▌ "), chalk.bold("Save as"), " ", beforeCursor, atCursor === "" ? chalk.cyan("█") : chalk.inverse(atCursor) + afterCursor] }), _jsx(Text, { wrap: "truncate-start", children: target === null
194
+ ? theme.paint("muted", "enter a file name")
195
+ : targetExists
196
+ ? theme.paint("warn", `${target} exists — Enter overwrites`)
197
+ : theme.paint("muted", `→ ${target}`) })] }))] }), _jsxs(Text, { children: [" ", stage === "format" ? (_jsxs(_Fragment, { children: [chalk.bold.cyan("↑↓"), " ", chalk.dim("choose"), " ", chalk.bold.cyan("⏎"), " ", chalk.dim("next"), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("cancel")] })) : stage === "dest" ? (_jsxs(_Fragment, { children: [chalk.bold.cyan("↑↓"), " ", chalk.dim("choose"), " ", chalk.bold.cyan("⏎"), " ", chalk.dim(`save ${filename}`), " ", chalk.bold.cyan("t"), " ", chalk.dim("type path"), " ", chalk.bold.cyan("Esc"), " ", chalk.dim(multi ? "back" : "cancel")] })) : (_jsxs(_Fragment, { children: [chalk.bold.cyan("⏎"), " ", chalk.dim("save"), " ", chalk.bold.cyan("Tab"), " ", chalk.dim("complete"), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("back")] }))] })] }));
198
+ };
@@ -13,8 +13,8 @@ export const LiveInstall = ({ view }) => {
13
13
  if (view.phase === "scanning") {
14
14
  return (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: "cyan", children: _jsx(InkSpinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", view.total === 0
15
15
  ? "DG starting protection…"
16
- : view.resolvedTotal !== undefined
17
- ? `DG verifying ${view.total}/${Math.max(view.total, view.resolvedTotal)}…`
16
+ : view.resolvedTotal !== undefined && view.total <= view.resolvedTotal
17
+ ? `DG verifying ${view.total}/${view.resolvedTotal}…`
18
18
  : `DG verifying ${packageCount(view.total)}…`] }), view.current ? _jsxs(Text, { dimColor: true, children: [" ", view.current] }) : null] }));
19
19
  }
20
20
  if (view.total === 0 && !view.blocked) {
@@ -32,7 +32,7 @@ const NEXT_STEP = {
32
32
  "private-upload-disabled": "Enable private artifact scanning to verify this package.",
33
33
  "needs-login": "Run 'dg login' (free) to check packages from the registry before they install.",
34
34
  "quota-exceeded": "Upgrade your plan or wait for your monthly limit to reset. See westbayberry.com/pricing.",
35
- cooldown: "Wait for the cooldown, pin an older version, or exempt it: dg config set cooldown.exempt <name>"
35
+ cooldown: "Wait it out (see holds: dg cooldown), pin an older version, or exempt it: dg cooldown exempt <name>"
36
36
  };
37
37
  function cooldownDetailLine(cooldown) {
38
38
  const window = formatCooldownDuration(cooldown.requiredDays);
@@ -0,0 +1,32 @@
1
+ import { resolvePresentation } from "../presentation/mode.js";
2
+ import { createTheme } from "../presentation/theme.js";
3
+ const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
4
+ const FRAME_INTERVAL_MS = 80;
5
+ const CLEAR_LINE = "\r\u001b[2K";
6
+ const INACTIVE = { stop: () => undefined };
7
+ export function startPrepSpinner(label, stream = process.stderr, env = process.env) {
8
+ const presentation = resolvePresentation({ stream, env });
9
+ if (presentation.mode !== "rich") {
10
+ return INACTIVE;
11
+ }
12
+ const theme = createTheme(presentation.color);
13
+ let frame = 0;
14
+ const draw = () => {
15
+ stream.write(`\r ${theme.paint("unknown", FRAMES[frame % FRAMES.length] ?? "")} ${label}`);
16
+ frame += 1;
17
+ };
18
+ draw();
19
+ const timer = setInterval(draw, FRAME_INTERVAL_MS);
20
+ timer.unref();
21
+ let stopped = false;
22
+ return {
23
+ stop: () => {
24
+ if (stopped) {
25
+ return;
26
+ }
27
+ stopped = true;
28
+ clearInterval(timer);
29
+ stream.write(CLEAR_LINE);
30
+ }
31
+ };
32
+ }