@westbayberry/dg 2.0.11 → 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 (143) 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 +6 -4
  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 +26 -10
  26. package/dist/commands/cooldown.js +389 -0
  27. package/dist/commands/decisions.js +169 -0
  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 +8 -4
  33. package/dist/commands/sbom.js +206 -0
  34. package/dist/commands/scan.js +3 -2
  35. package/dist/commands/service.js +32 -13
  36. package/dist/commands/setup.js +197 -27
  37. package/dist/commands/status.js +5 -2
  38. package/dist/commands/types.js +1 -0
  39. package/dist/commands/update.js +17 -8
  40. package/dist/commands/verify.js +8 -5
  41. package/dist/config/settings.js +154 -65
  42. package/dist/decisions/apply.js +128 -0
  43. package/dist/decisions/remember-prompt.js +92 -0
  44. package/dist/export-ui/ExportDialog.js +198 -0
  45. package/dist/install-ui/LiveInstall.js +2 -2
  46. package/dist/install-ui/block-render.js +21 -4
  47. package/dist/install-ui/prep-spinner.js +32 -0
  48. package/dist/install-ui/prompt.js +14 -0
  49. package/dist/launcher/agent-check.js +466 -0
  50. package/dist/launcher/agent-hook-exec.js +70 -0
  51. package/dist/launcher/agent-hook-io.js +31 -0
  52. package/dist/launcher/cargo-cache.js +40 -0
  53. package/dist/launcher/classify.js +17 -6
  54. package/dist/launcher/env.js +71 -24
  55. package/dist/launcher/install-preflight.js +167 -17
  56. package/dist/launcher/live-install.js +25 -5
  57. package/dist/launcher/output-redaction.js +7 -4
  58. package/dist/launcher/preflight-prompt.js +43 -3
  59. package/dist/launcher/run.js +100 -86
  60. package/dist/launcher/spawn-invocation.js +21 -0
  61. package/dist/policy/cooldown.js +117 -0
  62. package/dist/policy/evaluate.js +5 -21
  63. package/dist/policy/pypi-name.js +17 -0
  64. package/dist/presentation/mode.js +3 -2
  65. package/dist/presentation/package-page.js +9 -0
  66. package/dist/presentation/provenance.js +23 -0
  67. package/dist/presentation/theme.js +7 -7
  68. package/dist/project/dgfile.js +446 -0
  69. package/dist/proxy/auth.js +42 -0
  70. package/dist/proxy/ca.js +29 -9
  71. package/dist/proxy/cooldown-exemptions-file.js +33 -0
  72. package/dist/proxy/enforcement.js +57 -17
  73. package/dist/proxy/metadata-map.js +66 -4
  74. package/dist/proxy/preverified.js +55 -0
  75. package/dist/proxy/server.js +473 -45
  76. package/dist/proxy/worker.js +16 -1
  77. package/dist/publish-set/collect.js +1 -4
  78. package/dist/publish-set/npm.js +8 -5
  79. package/dist/publish-set/pack.js +9 -3
  80. package/dist/runtime/cli.js +0 -4
  81. package/dist/runtime/fatal.js +31 -0
  82. package/dist/runtime/first-run.js +12 -11
  83. package/dist/runtime/node-version.js +43 -6
  84. package/dist/runtime/nudges.js +35 -2
  85. package/dist/sbom/cyclonedx.js +211 -0
  86. package/dist/sbom-ui/SbomApp.js +158 -0
  87. package/dist/sbom-ui/components/SbomHeader.js +32 -0
  88. package/dist/sbom-ui/components/SbomList.js +52 -0
  89. package/dist/sbom-ui/inventory.js +128 -0
  90. package/dist/sbom-ui/launch.js +51 -0
  91. package/dist/sbom-ui/run.js +55 -0
  92. package/dist/sbom-ui/store.js +26 -0
  93. package/dist/scan/collect.js +10 -6
  94. package/dist/scan/command.js +51 -17
  95. package/dist/scan/discovery.js +11 -2
  96. package/dist/scan/render.js +63 -8
  97. package/dist/scan/scanner-report.js +42 -9
  98. package/dist/scan/staged.js +71 -11
  99. package/dist/scan-ui/LegacyApp.js +12 -16
  100. package/dist/scan-ui/alt-screen.js +5 -8
  101. package/dist/scan-ui/components/InteractiveResultsView.js +193 -123
  102. package/dist/scan-ui/components/ProgressBar.js +3 -14
  103. package/dist/scan-ui/components/ProjectSelector.js +1 -1
  104. package/dist/scan-ui/components/ScoreHeader.js +2 -3
  105. package/dist/scan-ui/components/SetupBanner.js +0 -6
  106. package/dist/scan-ui/format-helpers.js +61 -5
  107. package/dist/scan-ui/hooks/useResizeRepaint.js +25 -0
  108. package/dist/scan-ui/hooks/useScan.js +46 -4
  109. package/dist/scan-ui/launch.js +7 -4
  110. package/dist/scan-ui/shims.js +14 -4
  111. package/dist/scripts/detect.js +158 -0
  112. package/dist/scripts/gate.js +170 -0
  113. package/dist/service/state.js +27 -8
  114. package/dist/service/trust-refresh.js +92 -0
  115. package/dist/service/worker.js +23 -1
  116. package/dist/setup/activate-shell.js +28 -0
  117. package/dist/setup/git-hook.js +49 -4
  118. package/dist/setup/plan.js +98 -29
  119. package/dist/setup-ui/gate.js +39 -0
  120. package/dist/setup-ui/offer.js +42 -0
  121. package/dist/setup-ui/selector.js +27 -0
  122. package/dist/setup-ui/tasks.js +56 -0
  123. package/dist/setup-ui/wizard.js +225 -0
  124. package/dist/state/cooldown-held.js +66 -0
  125. package/dist/state/index.js +1 -0
  126. package/dist/state/locks.js +4 -2
  127. package/dist/state/store.js +2 -1
  128. package/dist/util/external-tool.js +25 -0
  129. package/dist/util/git.js +10 -3
  130. package/dist/util/json-file.js +24 -0
  131. package/dist/util/report-writer.js +57 -0
  132. package/dist/util/tty-prompt.js +13 -6
  133. package/dist/verify/local.js +240 -42
  134. package/dist/verify/package-check.js +86 -18
  135. package/dist/verify/preflight.js +242 -49
  136. package/dist/verify/render.js +15 -1
  137. package/npm-shrinkwrap.json +2383 -0
  138. package/package.json +14 -8
  139. package/NOTICE +0 -5
  140. package/dist/commands/completion.js +0 -116
  141. package/dist/commands/explain.js +0 -232
  142. package/dist/commands/unavailable.js +0 -11
  143. package/dist/telemetry/events.js +0 -40
@@ -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) {
@@ -1,3 +1,4 @@
1
+ import { formatCooldownDuration, formatPackageAge } from "../policy/cooldown.js";
1
2
  const VERIFIED_BAD = new Set([
2
3
  "malware",
3
4
  "policy",
@@ -19,6 +20,7 @@ const HEADLINES = {
19
20
  "api-timeout": "scanner timed out",
20
21
  "registry-timeout": "registry timed out",
21
22
  "analysis-incomplete": "analysis incomplete",
23
+ cooldown: "release too new (cooldown)",
22
24
  "unsupported-manager": "unsupported package manager",
23
25
  "proxy-setup-failure": "protection unavailable"
24
26
  };
@@ -29,14 +31,24 @@ const NEXT_STEP = {
29
31
  "hash-mismatch": "Clear your package cache and retry. If it persists, do not install.",
30
32
  "private-upload-disabled": "Enable private artifact scanning to verify this package.",
31
33
  "needs-login": "Run 'dg login' (free) to check packages from the registry before they install.",
32
- "quota-exceeded": "Upgrade your plan or wait for your monthly limit to reset. See westbayberry.com/pricing."
34
+ "quota-exceeded": "Upgrade your plan or wait for your monthly limit to reset. See westbayberry.com/pricing.",
35
+ cooldown: "Wait it out (see holds: dg cooldown), pin an older version, or exempt it: dg cooldown exempt <name>"
33
36
  };
37
+ function cooldownDetailLine(cooldown) {
38
+ const window = formatCooldownDuration(cooldown.requiredDays);
39
+ const eligible = formatResetDate(cooldown.eligibleAt);
40
+ const suffix = eligible ? ` (eligible ${eligible})` : "";
41
+ if (cooldown.ageDays === undefined) {
42
+ return `publish time unknown; your cooldown is ${window}${suffix}`;
43
+ }
44
+ return `published ${formatPackageAge(cooldown.ageDays)}; your cooldown is ${window}${suffix}`;
45
+ }
34
46
  export function describeBlockedInstall(decision) {
35
47
  const verifiedBad = VERIFIED_BAD.has(decision.cause);
36
48
  const override = decision.forceOverride && !decision.forceOverride.allowed
37
49
  ? "not allowed by your policy"
38
50
  : "re-run with --dg-force-install";
39
- const nextStep = verifiedBad || decision.cause === "needs-login"
51
+ const nextStep = verifiedBad || decision.cause === "needs-login" || decision.cause === "cooldown"
40
52
  ? NEXT_STEP[decision.cause]
41
53
  : "Re-check later with 'dg verify', or override if you accept the risk.";
42
54
  return {
@@ -85,9 +97,14 @@ export function renderInstallDecision(decision) {
85
97
  const lines = [
86
98
  verifiedBad
87
99
  ? `✘ DG blocked install — ${headline}`
88
- : `? DG could not verify ${decision.packageName} ${headline}`,
100
+ : decision.cause === "cooldown"
101
+ ? `? DG quarantined ${decision.packageName} — ${headline}`
102
+ : `? DG could not verify ${decision.packageName} — ${headline}`,
89
103
  ` ${decision.packageName} ${decision.reason}`
90
104
  ];
105
+ if (decision.cause === "cooldown" && decision.cooldown) {
106
+ lines.push(` ${cooldownDetailLine(decision.cooldown)}`);
107
+ }
91
108
  if (decision.dashboardUrl) {
92
109
  lines.push(` Evidence: ${decision.dashboardUrl}`);
93
110
  }
@@ -97,7 +114,7 @@ export function renderInstallDecision(decision) {
97
114
  lines.push(decision.forceOverride && !decision.forceOverride.allowed
98
115
  ? " Override: not allowed by your policy"
99
116
  : " Override: re-run with --dg-force-install");
100
- const next = verifiedBad || decision.cause === "needs-login"
117
+ const next = verifiedBad || decision.cause === "needs-login" || decision.cause === "cooldown"
101
118
  ? NEXT_STEP[decision.cause]
102
119
  : "Re-check later with 'dg verify', or override if you accept the risk.";
103
120
  if (next) {
@@ -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
+ }
@@ -6,6 +6,20 @@ export function defaultPromptIo() {
6
6
  isTTY: Boolean(process.stdin.isTTY && process.stderr.isTTY)
7
7
  };
8
8
  }
9
+ export async function promptText(question, io) {
10
+ if (!io.isTTY) {
11
+ return "";
12
+ }
13
+ const rl = createInterface({ input: io.input, output: io.output });
14
+ try {
15
+ return await new Promise((resolve) => {
16
+ rl.question(question, resolve);
17
+ });
18
+ }
19
+ finally {
20
+ rl.close();
21
+ }
22
+ }
9
23
  export async function promptYesNo(question, io, defaultYes = false) {
10
24
  if (!io.isTTY) {
11
25
  return false;