From ba956d06fa1669b3807acb62d5b98a58e872b7f1 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 16 Sep 2025 02:00:47 -0700 Subject: [PATCH 1/6] `bun feedback` --- src/cli.zig | 1 + src/cli/feedback.ts | 631 ++++++++++++++++++++++++++++++++++++++++ src/cli/run_command.zig | 12 + 3 files changed, 644 insertions(+) create mode 100644 src/cli/feedback.ts diff --git a/src/cli.zig b/src/cli.zig index 7b5918b93de70b..6176f627c9655c 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -187,6 +187,7 @@ pub const HelpCommand = struct { \\ init Start an empty Bun project from a built-in template \\ create {s:<16} Create a new project from a template (bun c) \\ upgrade Upgrade to latest version of Bun. + \\ feedback ./file1 ./file2 Provide feedback to the Bun team. \\ \ --help Print help text for command. \\ ; diff --git a/src/cli/feedback.ts b/src/cli/feedback.ts new file mode 100644 index 00000000000000..b2e7ad27a50044 --- /dev/null +++ b/src/cli/feedback.ts @@ -0,0 +1,631 @@ +import { spawnSync } from "node:child_process"; +import { promises as fsp, openSync, closeSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import readline from "node:readline"; +import { parseArgs as nodeParseArgs } from "node:util"; +import tty from "node:tty"; +import { File } from "node:buffer"; + +const supportsAnsi = Boolean(process.stdout.isTTY && !("NO_COLOR" in process.env)); +const colors = { + reset: supportsAnsi ? "\x1b[0m" : "", + bold: supportsAnsi ? "\x1b[1m" : "", + dim: supportsAnsi ? "\x1b[2m" : "", + red: supportsAnsi ? "\x1b[31m" : "", + green: supportsAnsi ? "\x1b[32m" : "", + yellow: supportsAnsi ? "\x1b[33m" : "", + cyan: supportsAnsi ? "\x1b[36m" : "", + gray: supportsAnsi ? "\x1b[90m" : "", +}; +const symbols = { + question: `${colors.cyan}?${colors.reset}` || "?", + check: `${colors.green}✔${colors.reset}` || "✔", + cross: `${colors.red}✖${colors.reset}` || "✖", +}; +const inputPrefix = `${colors.gray}> ${colors.reset}`; +const thankYouBanner = ` +${supportsAnsi ? colors.green : ""}T H A N K Y O U ! ${colors.reset}`; + +type TerminalIO = { + input: tty.ReadStream; + output: tty.WriteStream; + cleanup: () => void; +}; + +function openTerminal(): TerminalIO | null { + if (process.stdin.isTTY && process.stdout.isTTY) { + return { + input: process.stdin as unknown as tty.ReadStream, + output: process.stdout as unknown as tty.WriteStream, + cleanup: () => {}, + }; + } + + const candidates = process.platform === "win32" ? ["CON"] : ["/dev/tty"]; + + for (const candidate of candidates) { + try { + const fd = openSync(candidate, "r+"); + const input = new tty.ReadStream(fd); + const output = new tty.WriteStream(fd); + input.setEncoding("utf8"); + return { + input, + output, + cleanup: () => { + input.destroy(); + output.destroy(); + try { + closeSync(fd); + } catch {} + }, + }; + } catch {} + } + + return null; +} +const logError = (message: string) => { + process.stderr.write(`${symbols.cross} ${message}\n`); +}; + +const isValidEmail = (value: string | undefined): value is string => { + if (!value) return false; + const trimmed = value.trim(); + if (!trimmed.includes("@")) return false; + if (!trimmed.includes(".")) return false; + return true; +}; + +type ParsedArgs = { + email?: string; + help: boolean; + positionals: string[]; +}; + +function parseCliArgs(argv: string[]): ParsedArgs { + try { + const { values, positionals } = nodeParseArgs({ + args: argv, + allowPositionals: true, + strict: false, + options: { + email: { + type: "string", + short: "e", + }, + help: { + type: "boolean", + short: "h", + }, + }, + }); + + return { + email: values.email, + help: Boolean(values.help), + positionals, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logError(message); + process.exit(1); + return { email: undefined, help: false, positionals: [] }; + } +} + +function printHelp() { + const heading = `${colors.bold}${colors.cyan}bun feedback${colors.reset}`; + const usage = `${colors.bold}Usage${colors.reset} + bun feedback [options] [feedback text ... | files ...]`; + const options = `${colors.bold}Options${colors.reset} + ${colors.cyan}-e${colors.reset}, ${colors.cyan}--email${colors.reset} Set the email address used for this submission + ${colors.cyan}-h${colors.reset}, ${colors.cyan}--help${colors.reset} Show this help message and exit`; + const examples = `${colors.bold}Examples${colors.reset} + bun feedback "Love the new release!" + bun feedback report.txt details.log + echo "please document X" | bun feedback --email you@example.com`; + + console.log([heading, "", usage, "", options, "", examples].join("\n")); +} + +async function readEmailFromBunInstall(): Promise { + const installRoot = process.env.BUN_INSTALL ?? path.join(os.homedir(), ".bun"); + const emailFile = path.join(installRoot, "feedback"); + try { + const data = await fsp.readFile(emailFile, "utf8"); + const trimmed = data.trim(); + return trimmed.length > 0 ? trimmed : undefined; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + console.warn(`Unable to read ${emailFile}:`, (error as Error).message); + } + return undefined; + } +} + +async function persistEmailToBunInstall(email: string): Promise { + const installRoot = process.env.BUN_INSTALL; + if (!installRoot) return; + + const emailFile = path.join(installRoot, "feedback"); + try { + await fsp.mkdir(path.dirname(emailFile), { recursive: true }); + await fsp.writeFile(emailFile, `${email.trim()}\n`, "utf8"); + } catch (error) { + console.warn(`Unable to persist email to ${emailFile}:`, (error as Error).message); + } +} + +function readEmailFromGitConfig(): string | undefined { + const result = spawnSync("git", ["config", "user.email"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + if (result.status !== 0) { + return undefined; + } + const output = result.stdout.trim(); + return output.length > 0 ? output : undefined; +} + +async function promptForEmail(terminal: TerminalIO | null, defaultEmail?: string): Promise { + if (!terminal) { + return defaultEmail && isValidEmail(defaultEmail) ? defaultEmail : undefined; + } + + let currentDefault = defaultEmail; + + for (;;) { + const answer = await promptForEmailInteractive(terminal, currentDefault); + if (typeof answer === "string" && isValidEmail(answer)) { + return answer.trim(); + } + + terminal.output.write(`${symbols.cross} Please provide a valid email address containing "@" and ".".\n`); + currentDefault = undefined; + } +} + +async function promptForEmailInteractive(terminal: TerminalIO, defaultEmail?: string): Promise { + const input = terminal.input; + const output = terminal.output; + + readline.emitKeypressEvents(input); + const hadRawMode = typeof input.isRaw === "boolean" ? input.isRaw : undefined; + if (typeof input.setRawMode === "function") { + input.setRawMode(true); + } + if (typeof input.resume === "function") { + input.resume(); + } + + const placeholder = defaultEmail ?? ""; + let placeholderActive = placeholder.length > 0; + let value = ""; + let resolved = false; + + const render = () => { + output.write(`\r\x1b[2K${symbols.question} ${colors.bold}Email${colors.reset}: `); + if (placeholderActive && placeholder.length > 0) { + output.write(`${colors.dim}<${placeholder}>${colors.reset}`); + output.write(`\x1b[${placeholder.length + 2}D`); + } else { + output.write(value); + } + }; + + render(); + + return await new Promise(resolve => { + const cleanup = (result?: string) => { + if (resolved) return; + resolved = true; + input.removeListener("keypress", onKeypress); + if (typeof input.setRawMode === "function") { + if (typeof hadRawMode === "boolean") { + input.setRawMode(hadRawMode); + } else { + input.setRawMode(false); + } + } + if (typeof input.pause === "function") { + input.pause(); + } + output.write("\n"); + resolve(result); + }; + + const onKeypress = (str: string, key: readline.Key) => { + if (!key && str) { + if (placeholderActive) { + placeholderActive = false; + value = ""; + render(); + } + value += str; + output.write(str); + return; + } + + if (key && (key.sequence === "\u0003" || (key.ctrl && key.name === "c"))) { + cleanup(); + process.exit(130); + return; + } + + if (key?.name === "return") { + if (placeholderActive && placeholder.length > 0) { + cleanup(placeholder); + return; + } + const trimmed = value.trim(); + cleanup(trimmed.length > 0 ? trimmed : undefined); + return; + } + + if (key?.name === "backspace") { + if (placeholderActive) { + return; + } + if (value.length > 0) { + value = value.slice(0, -1); + render(); + } + return; + } + + if (!str) { + return; + } + + if (key && key.name && key.name.length > 1 && key.name !== "space") { + return; + } + + if (placeholderActive) { + placeholderActive = false; + value = ""; + render(); + } + + value += str; + output.write(str); + }; + + input.on("keypress", onKeypress); + }); +} + +async function promptForBody( + terminal: TerminalIO | null, + attachments: PositionalContent["files"], +): Promise { + if (!terminal) { + return undefined; + } + + const input = terminal.input; + const output = terminal.output; + + readline.emitKeypressEvents(input); + const hadRawMode = typeof input.isRaw === "boolean" ? input.isRaw : undefined; + if (typeof input.setRawMode === "function") { + input.setRawMode(true); + } + if (typeof input.resume === "function") { + input.resume(); + } + + const header = `${symbols.question} ${colors.bold}Share your feedback${colors.reset} ${colors.dim}(Enter to send, Shift+Enter for a newline)${colors.reset}`; + output.write(`${header}\n`); + if (attachments.length > 0) { + output.write(`${colors.dim}files: ${attachments.map(file => file.filename).join(", ")}${colors.reset}\n`); + } + output.write(`${inputPrefix}`); + + const lines: string[] = [""]; + let currentLine = 0; + let resolved = false; + + return await new Promise(resolve => { + const cleanup = (value?: string) => { + if (resolved) return; + resolved = true; + input.removeListener("keypress", onKeypress); + if (typeof input.setRawMode === "function") { + if (typeof hadRawMode === "boolean") { + input.setRawMode(hadRawMode); + } else { + input.setRawMode(false); + } + } + if (typeof input.pause === "function") { + input.pause(); + } + output.write("\n"); + resolve(value); + }; + + const onKeypress = (str: string, key: readline.Key) => { + if (!key) { + if (str) { + lines[currentLine] += str; + output.write(str); + } + return; + } + + if (key.sequence === "\u0003" || (key.ctrl && key.name === "c")) { + cleanup(); + process.exit(130); + return; + } + + if (key.name === "return") { + if (key.shift) { + lines.push(""); + currentLine += 1; + output.write(`\n${inputPrefix}`); + return; + } + const message = lines.join("\n"); + cleanup(message); + return; + } + + if (key.name === "backspace") { + const current = lines[currentLine]; + if (current.length > 0) { + lines[currentLine] = current.slice(0, -1); + output.write("\b \b"); + } else if (currentLine > 0) { + lines.pop(); + currentLine -= 1; + output.write("\r\x1b[2K"); + output.write("\x1b[F"); + output.write("\r\x1b[2K"); + output.write(`${inputPrefix}${lines[currentLine]}`); + } + return; + } + + if (key.name && key.name.length > 1 && key.name !== "space") { + return; + } + + if (str) { + lines[currentLine] += str; + output.write(str); + } + }; + + input.on("keypress", onKeypress); + }); +} + +async function readFromStdin(): Promise { + const stdin = process.stdin; + if (!stdin || stdin.isTTY) return undefined; + + if (typeof stdin.setEncoding === "function") { + stdin.setEncoding("utf8"); + } + + if (typeof stdin.resume === "function") { + stdin.resume(); + } + + const chunks: string[] = []; + for await (const chunk of stdin as AsyncIterable) { + chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf8")); + } + + const content = chunks.join(""); + return content.length > 0 ? content : undefined; +} + +type PositionalContent = { + messageParts: string[]; + files: { filename: string; content: string }[]; +}; + +async function resolveFileCandidate(token: string): Promise { + const candidates = new Set(); + candidates.add(token); + + if (token.startsWith("~/")) { + candidates.add(path.join(os.homedir(), token.slice(2))); + } + + const resolved = path.resolve(process.cwd(), token); + candidates.add(resolved); + + for (const candidate of candidates) { + try { + const stat = await fsp.stat(candidate); + if (stat.isFile()) { + return candidate; + } + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code && (code === "ENOENT" || code === "ENOTDIR")) { + continue; + } + console.warn(`Unable to inspect ${candidate}:`, (error as Error).message); + } + } + + return undefined; +} + +async function readFromPositionals(positionals: string[]): Promise { + const messageParts: string[] = []; + const files: PositionalContent["files"] = []; + let literalTokens: string[] = []; + + const flushTokens = () => { + if (literalTokens.length > 0) { + messageParts.push(literalTokens.join(" ")); + literalTokens = []; + } + }; + + for (const token of positionals) { + const filePath = await resolveFileCandidate(token); + if (filePath) { + try { + flushTokens(); + const fileContents = await fsp.readFile(filePath, "utf8"); + files.push({ filename: path.basename(filePath), content: fileContents }); + continue; + } catch { + // Ignore read errors; treat token as part of the message instead. + } + } + + literalTokens.push(token); + } + + flushTokens(); + return { messageParts, files }; +} + +function getOldestGitSha(): string | undefined { + const result = spawnSync("git", ["rev-list", "--max-parents=0", "HEAD"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + + if (result.status !== 0) { + return undefined; + } + + const firstLine = result.stdout.split(/\r?\n/).find(line => line.trim().length > 0); + return firstLine?.trim(); +} + +async function main() { + const rawArgv = [...Bun.argv.slice(2)]; + if (rawArgv.length > 0 && /feedback(\.ts)?$/.test(rawArgv[0])) { + rawArgv.shift(); + } + + let terminal: TerminalIO | null = null; + try { + const { email: emailFlag, help, positionals } = parseCliArgs(rawArgv); + if (help) { + printHelp(); + return; + } + + terminal = openTerminal(); + + const exit = (code: number): never => { + terminal?.cleanup(); + process.exit(code); + }; + + if (emailFlag && !isValidEmail(emailFlag)) { + logError("The provided email must include both '@' and '.'."); + exit(1); + } + + const storedEmailRaw = await readEmailFromBunInstall(); + const storedEmail = isValidEmail(storedEmailRaw) ? storedEmailRaw.trim() : undefined; + + const gitEmailRaw = readEmailFromGitConfig(); + const gitEmail = isValidEmail(gitEmailRaw) ? gitEmailRaw.trim() : undefined; + + const canPrompt = terminal !== null; + + let email = emailFlag?.trim() ?? storedEmail ?? gitEmail; + + if (canPrompt && !emailFlag && !storedEmail) { + email = await promptForEmail(terminal, email ?? gitEmail ?? undefined); + } + + if (!isValidEmail(email)) { + if (!canPrompt) { + logError("Unable to determine email automatically. Pass --email
."); + } else { + logError("An email address is required. Pass --email or configure git user.email."); + } + exit(1); + return; + } + + const normalizedEmail = email.trim(); + + if (process.env.BUN_INSTALL && !storedEmail) { + await persistEmailToBunInstall(normalizedEmail); + } + + const stdinContent = await readFromStdin(); + const positionalContent = await readFromPositionals(positionals); + const positionalParts = positionalContent.messageParts; + const pieces: string[] = []; + if (stdinContent) pieces.push(stdinContent); + pieces.push(...positionalParts); + + let message = pieces.join(pieces.length > 1 ? "\n\n" : ""); + + if (message.trim().length === 0 && terminal) { + const interactiveBody = await promptForBody(terminal, positionalContent.files); + if (interactiveBody && interactiveBody.trim().length > 0) { + message = interactiveBody; + } + } + + const normalizedMessage = message.trim(); + if (normalizedMessage.length === 0) { + logError("No feedback provided. Supply text, file paths, or pipe input."); + exit(1); + return; + } + + const messageBody = normalizedMessage; + + const projectId = getOldestGitSha(); + const endpoint = process.env.BUN_FEEDBACK_URL || "https://bun.com/api/v1/feedback"; + + const form = new FormData(); + form.append("email", normalizedEmail); + form.append("message", messageBody); + for (const file of positionalContent.files) { + form.append("files[]", new File([file.content], file.filename)); + } + form.append("bun_version", Bun.version_with_sha); + if (projectId) { + form.append("project_id", projectId); + } + + const response = await fetch(endpoint, { + method: "POST", + body: form, + headers: { + "User-Agent": `bun-feedback/${Bun.version_with_sha}`, + }, + }); + + if (!response.ok) { + const bodyText = await response.text().catch(() => ""); + logError(`Failed to send feedback (${response.status} ${response.statusText}).`); + if (bodyText) { + process.stderr.write(`${bodyText}\n`); + } + exit(1); + } + + process.stdout.write(`${symbols.check} Feedback sent.\n${thankYouBanner}\n`); + } finally { + terminal?.cleanup(); + } +} + +await main().catch(error => { + const detail = error instanceof Error ? error.message : String(error); + logError(`Unexpected error while sending feedback: ${detail}`); + process.exit(1); +}); diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index 23fb3f5ff779c5..dee17eddb1e46b 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -1603,6 +1603,18 @@ pub const RunCommand = struct { return true; } + if (ctx.filters.len == 0 and !ctx.workspaces and CLI.Cli.cmd != null and CLI.Cli.cmd.? == .AutoCommand) { + if (bun.strings.eqlComptime(target_name, "feedback")) { + const trigger = bun.pathLiteral("/[eval]"); + var entry_point_buf: [bun.MAX_PATH_BYTES + trigger.len]u8 = undefined; + const cwd = try std.posix.getcwd(&entry_point_buf); + @memcpy(entry_point_buf[cwd.len..][0..trigger.len], trigger); + ctx.runtime_options.eval.script = @embedFile("./feedback.ts"); + try Run.boot(ctx, entry_point_buf[0 .. cwd.len + trigger.len], null); + Global.exit(0); + } + } + if (log_errors) { const ext = std.fs.path.extension(target_name); const default_loader = options.defaultLoaders.get(ext); From 25fc9f68f848ad9dbbaed8916863790901abf14e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 09:04:40 +0000 Subject: [PATCH 2/6] [autofix.ci] apply automated fixes --- src/cli/feedback.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli/feedback.ts b/src/cli/feedback.ts index b2e7ad27a50044..d9c97ffe294273 100644 --- a/src/cli/feedback.ts +++ b/src/cli/feedback.ts @@ -1,11 +1,11 @@ +import { File } from "node:buffer"; import { spawnSync } from "node:child_process"; -import { promises as fsp, openSync, closeSync } from "node:fs"; +import { closeSync, promises as fsp, openSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import readline from "node:readline"; -import { parseArgs as nodeParseArgs } from "node:util"; import tty from "node:tty"; -import { File } from "node:buffer"; +import { parseArgs as nodeParseArgs } from "node:util"; const supportsAnsi = Boolean(process.stdout.isTTY && !("NO_COLOR" in process.env)); const colors = { From 63f6c7b7a66382bebfd770f9d17a99095bfafb56 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 16 Sep 2025 02:54:19 -0700 Subject: [PATCH 3/6] Update feedback.ts --- src/cli/feedback.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/cli/feedback.ts b/src/cli/feedback.ts index d9c97ffe294273..72c0d04280d821 100644 --- a/src/cli/feedback.ts +++ b/src/cli/feedback.ts @@ -69,6 +69,9 @@ function openTerminal(): TerminalIO | null { const logError = (message: string) => { process.stderr.write(`${symbols.cross} ${message}\n`); }; +const logInfo = (message: string) => { + process.stdout.write(`${colors.bold}${message}${colors.reset}\n`); +}; const isValidEmail = (value: string | undefined): value is string => { if (!value) return false; @@ -476,8 +479,8 @@ async function readFromPositionals(positionals: string[]): Promise 0) pieces.push(stdinContent); + for (const part of positionalParts) { + if (part.trim().length > 0) { + pieces.push(part); + } + } - let message = pieces.join(pieces.length > 1 ? "\n\n" : ""); + let message = pieces.length > 0 ? pieces.join(pieces.length > 1 ? "\n\n" : "") : ""; if (message.trim().length === 0 && terminal) { const interactiveBody = await promptForBody(terminal, positionalContent.files); @@ -592,7 +599,14 @@ async function main() { const form = new FormData(); form.append("email", normalizedEmail); - form.append("message", messageBody); + const fileList = positionalContent.files.map(file => file.filename); + if (fileList.length > 0) { + const filenames = fileList.join(", "); + form.append("message", `${messageBody}\n\n+ files: ${filenames}`); + logInfo(`+ ${filenames}`); + } else { + form.append("message", messageBody); + } for (const file of positionalContent.files) { form.append("files[]", new File([file.content], file.filename)); } From 22b52b253e7e7852cc76740d22d16b4472d0e917 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 16 Sep 2025 14:05:46 -0700 Subject: [PATCH 4/6] Update feedback.ts --- src/cli/feedback.ts | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/cli/feedback.ts b/src/cli/feedback.ts index 72c0d04280d821..b1b1b5463b1404 100644 --- a/src/cli/feedback.ts +++ b/src/cli/feedback.ts @@ -1,4 +1,3 @@ -import { File } from "node:buffer"; import { spawnSync } from "node:child_process"; import { closeSync, promises as fsp, openSync } from "node:fs"; import os from "node:os"; @@ -25,7 +24,7 @@ const symbols = { }; const inputPrefix = `${colors.gray}> ${colors.reset}`; const thankYouBanner = ` -${supportsAnsi ? colors.green : ""}T H A N K Y O U ! ${colors.reset}`; +${supportsAnsi ? colors.bold : ""}THANK YOU! ${colors.reset}`; type TerminalIO = { input: tty.ReadStream; @@ -324,7 +323,7 @@ async function promptForBody( const header = `${symbols.question} ${colors.bold}Share your feedback${colors.reset} ${colors.dim}(Enter to send, Shift+Enter for a newline)${colors.reset}`; output.write(`${header}\n`); if (attachments.length > 0) { - output.write(`${colors.dim}files: ${attachments.map(file => file.filename).join(", ")}${colors.reset}\n`); + output.write(`${colors.dim}+ ${attachments.map(file => file.filename).join(", ")}${colors.reset}\n`); } output.write(`${inputPrefix}`); @@ -510,7 +509,7 @@ function getOldestGitSha(): string | undefined { } async function main() { - const rawArgv = [...Bun.argv.slice(2)]; + const rawArgv = [...process.argv.slice(2)]; if (rawArgv.length > 0 && /feedback(\.ts)?$/.test(rawArgv[0])) { rawArgv.shift(); } @@ -600,30 +599,27 @@ async function main() { const form = new FormData(); form.append("email", normalizedEmail); const fileList = positionalContent.files.map(file => file.filename); - if (fileList.length > 0) { - const filenames = fileList.join(", "); - form.append("message", `${messageBody}\n\n+ files: ${filenames}`); - logInfo(`+ ${filenames}`); - } else { - form.append("message", messageBody); - } + form.append("message", messageBody); for (const file of positionalContent.files) { - form.append("files[]", new File([file.content], file.filename)); + form.append("files[]", new Blob([file.content]), file.filename); } - form.append("bun_version", Bun.version_with_sha); + + form.append("platform", process.platform); + form.append("arch", process.arch); + form.append("bunRevision", Bun.revision); + form.append("hardwareConcurrency", String(navigator.hardwareConcurrency)); + form.append("bunVersion", Bun.version); + form.append("bunBuild", path.basename(process.release.sourceUrl!, path.extname(process.release.sourceUrl!))); if (projectId) { - form.append("project_id", projectId); + form.append("projectId", projectId); } const response = await fetch(endpoint, { method: "POST", body: form, - headers: { - "User-Agent": `bun-feedback/${Bun.version_with_sha}`, - }, }); - if (!response.ok) { + if (!response.ok || response.status !== 200) { const bodyText = await response.text().catch(() => ""); logError(`Failed to send feedback (${response.status} ${response.statusText}).`); if (bodyText) { From ceafe0f722e4c50b75f3716e22cd82493e6ef7ec Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 16 Sep 2025 20:16:42 -0700 Subject: [PATCH 5/6] mv --- src/{cli => js/eval}/feedback.ts | 177 ++++++++++++++++++++++++++----- 1 file changed, 148 insertions(+), 29 deletions(-) rename src/{cli => js/eval}/feedback.ts (76%) diff --git a/src/cli/feedback.ts b/src/js/eval/feedback.ts similarity index 76% rename from src/cli/feedback.ts rename to src/js/eval/feedback.ts index b1b1b5463b1404..e9ce4141dab23d 100644 --- a/src/cli/feedback.ts +++ b/src/js/eval/feedback.ts @@ -7,24 +7,27 @@ import tty from "node:tty"; import { parseArgs as nodeParseArgs } from "node:util"; const supportsAnsi = Boolean(process.stdout.isTTY && !("NO_COLOR" in process.env)); -const colors = { - reset: supportsAnsi ? "\x1b[0m" : "", - bold: supportsAnsi ? "\x1b[1m" : "", - dim: supportsAnsi ? "\x1b[2m" : "", - red: supportsAnsi ? "\x1b[31m" : "", - green: supportsAnsi ? "\x1b[32m" : "", - yellow: supportsAnsi ? "\x1b[33m" : "", - cyan: supportsAnsi ? "\x1b[36m" : "", - gray: supportsAnsi ? "\x1b[90m" : "", -}; +const reset = supportsAnsi ? "\x1b[0m" : ""; +const bold = supportsAnsi ? "\x1b[1m" : ""; +const dim = supportsAnsi ? "\x1b[2m" : ""; +const red = supportsAnsi ? "\x1b[31m" : ""; +const green = supportsAnsi ? "\x1b[32m" : ""; +const cyan = supportsAnsi ? "\x1b[36m" : ""; +const gray = supportsAnsi ? "\x1b[90m" : ""; const symbols = { - question: `${colors.cyan}?${colors.reset}` || "?", - check: `${colors.green}✔${colors.reset}` || "✔", - cross: `${colors.red}✖${colors.reset}` || "✖", + question: `${cyan}?${reset}` || "?", + check: `${green}✔${reset}` || "✔", + cross: `${red}✖${reset}` || "✖", }; -const inputPrefix = `${colors.gray}> ${colors.reset}`; +const inputPrefix = `${gray}> ${reset}`; const thankYouBanner = ` -${supportsAnsi ? colors.bold : ""}THANK YOU! ${colors.reset}`; +${supportsAnsi ? bold : ""}THANK YOU! ${reset}`; +const enum IPSupport { + ipv4 = "ipv4", + ipv6 = "ipv6", + ipv4_and_ipv6 = "ipv4_and_ipv6", + none = "none", +} type TerminalIO = { input: tty.ReadStream; @@ -69,7 +72,7 @@ const logError = (message: string) => { process.stderr.write(`${symbols.cross} ${message}\n`); }; const logInfo = (message: string) => { - process.stdout.write(`${colors.bold}${message}${colors.reset}\n`); + process.stdout.write(`${bold}${message}${reset}\n`); }; const isValidEmail = (value: string | undefined): value is string => { @@ -118,13 +121,13 @@ function parseCliArgs(argv: string[]): ParsedArgs { } function printHelp() { - const heading = `${colors.bold}${colors.cyan}bun feedback${colors.reset}`; - const usage = `${colors.bold}Usage${colors.reset} + const heading = `${bold}${cyan}bun feedback${reset}`; + const usage = `${bold}Usage${reset} bun feedback [options] [feedback text ... | files ...]`; - const options = `${colors.bold}Options${colors.reset} - ${colors.cyan}-e${colors.reset}, ${colors.cyan}--email${colors.reset} Set the email address used for this submission - ${colors.cyan}-h${colors.reset}, ${colors.cyan}--help${colors.reset} Show this help message and exit`; - const examples = `${colors.bold}Examples${colors.reset} + const options = `${bold}Options${reset} + ${cyan}-e${reset}, ${cyan}--email${reset} Set the email address used for this submission + ${cyan}-h${reset}, ${cyan}--help${reset} Show this help message and exit`; + const examples = `${bold}Examples${reset} bun feedback "Love the new release!" bun feedback report.txt details.log echo "please document X" | bun feedback --email you@example.com`; @@ -209,9 +212,9 @@ async function promptForEmailInteractive(terminal: TerminalIO, defaultEmail?: st let resolved = false; const render = () => { - output.write(`\r\x1b[2K${symbols.question} ${colors.bold}Email${colors.reset}: `); + output.write(`\r\x1b[2K${symbols.question} ${bold}Email${reset}: `); if (placeholderActive && placeholder.length > 0) { - output.write(`${colors.dim}<${placeholder}>${colors.reset}`); + output.write(`${dim}<${placeholder}>${reset}`); output.write(`\x1b[${placeholder.length + 2}D`); } else { output.write(value); @@ -320,10 +323,10 @@ async function promptForBody( input.resume(); } - const header = `${symbols.question} ${colors.bold}Share your feedback${colors.reset} ${colors.dim}(Enter to send, Shift+Enter for a newline)${colors.reset}`; + const header = `${symbols.question} ${bold}Share your feedback${reset} ${dim}(Enter to send, Shift+Enter for a newline)${reset}`; output.write(`${header}\n`); if (attachments.length > 0) { - output.write(`${colors.dim}+ ${attachments.map(file => file.filename).join(", ")}${colors.reset}\n`); + output.write(`${dim}+ ${attachments.map(file => file.filename).join(", ")}${reset}\n`); } output.write(`${inputPrefix}`); @@ -430,7 +433,7 @@ async function readFromStdin(): Promise { type PositionalContent = { messageParts: string[]; - files: { filename: string; content: string }[]; + files: { filename: string; content: Uint8Array }[]; }; async function resolveFileCandidate(token: string): Promise { @@ -478,7 +481,12 @@ async function readFromPositionals(positionals: string[]): Promise 1024 * 1024 * 10) { + fileContents = fileContents.slice(0, 1024 * 1024 * 10); + } + flushTokens(); files.push({ filename: path.basename(filePath), content: fileContents }); continue; @@ -494,6 +502,33 @@ async function readFromPositionals(positionals: string[]): Promise Date: Tue, 16 Sep 2025 20:16:58 -0700 Subject: [PATCH 6/6] Make it part of the build system --- build.zig | 1 + src/cli/run_command.zig | 21 ++++++++++++++------- src/codegen/bundle-modules.ts | 21 +++++++++++++++++++++ src/js/eval/README.md | 1 + 4 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 src/js/eval/README.md diff --git a/build.zig b/build.zig index 4bc45fcdead980..608406a46233ab 100644 --- a/build.zig +++ b/build.zig @@ -739,6 +739,7 @@ fn addInternalImports(b: *Build, mod: *Module, opts: *BunBuildOptions) void { .{ .file = "node-fallbacks/url.js", .enable = opts.shouldEmbedCode() }, .{ .file = "node-fallbacks/util.js", .enable = opts.shouldEmbedCode() }, .{ .file = "node-fallbacks/zlib.js", .enable = opts.shouldEmbedCode() }, + .{ .file = "eval/feedback.ts", .enable = opts.shouldEmbedCode() }, }) |entry| { if (!@hasField(@TypeOf(entry), "enable") or entry.enable) { const path = b.pathJoin(&.{ opts.codegen_path, entry.file }); diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index dee17eddb1e46b..c72159c96c86b9 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -1605,13 +1605,7 @@ pub const RunCommand = struct { if (ctx.filters.len == 0 and !ctx.workspaces and CLI.Cli.cmd != null and CLI.Cli.cmd.? == .AutoCommand) { if (bun.strings.eqlComptime(target_name, "feedback")) { - const trigger = bun.pathLiteral("/[eval]"); - var entry_point_buf: [bun.MAX_PATH_BYTES + trigger.len]u8 = undefined; - const cwd = try std.posix.getcwd(&entry_point_buf); - @memcpy(entry_point_buf[cwd.len..][0..trigger.len], trigger); - ctx.runtime_options.eval.script = @embedFile("./feedback.ts"); - try Run.boot(ctx, entry_point_buf[0 .. cwd.len + trigger.len], null); - Global.exit(0); + try @"bun feedback"(ctx); } } @@ -1677,6 +1671,19 @@ pub const RunCommand = struct { Global.exit(1); }; } + + fn @"bun feedback"(ctx: Command.Context) !noreturn { + const trigger = bun.pathLiteral("/[eval]"); + var entry_point_buf: [bun.MAX_PATH_BYTES + trigger.len]u8 = undefined; + const cwd = try std.posix.getcwd(&entry_point_buf); + @memcpy(entry_point_buf[cwd.len..][0..trigger.len], trigger); + ctx.runtime_options.eval.script = if (bun.Environment.codegen_embed) + @embedFile("eval/feedback.ts") + else + bun.runtimeEmbedFile(.codegen, "eval/feedback.ts"); + try Run.boot(ctx, entry_point_buf[0 .. cwd.len + trigger.len], null); + Global.exit(0); + } }; pub const BunXFastPath = struct { diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index 46a99d44ac45e8..8ebfdfa3736ab6 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -542,6 +542,27 @@ declare module "module" { mark("Generate Code"); +const evalFiles = new Bun.Glob(path.join(BASE, "eval", "*.ts")).scanSync(); +for (const file of evalFiles) { + const { + outputs: [output], + } = await Bun.build({ + entrypoints: [file], + + // Shrink it. + minify: !debug, + + target: "bun", + format: "esm", + env: "disable", + define: { + "process.platform": JSON.stringify(process.platform), + "process.arch": JSON.stringify(process.arch), + }, + }); + writeIfNotChanged(path.join(CODEGEN_DIR, "eval", path.basename(file)), await output.text()); +} + if (!silent) { console.log(""); console.timeEnd(timeString); diff --git a/src/js/eval/README.md b/src/js/eval/README.md new file mode 100644 index 00000000000000..b92df5e8f63c4b --- /dev/null +++ b/src/js/eval/README.md @@ -0,0 +1 @@ +These are not bundled as builtin modules and instead are minified.