Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 21 additions & 22 deletions AGENTS.md

Large diffs are not rendered by default.

253 changes: 64 additions & 189 deletions bun.lock

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
"@anthropic-ai/sdk": "^0.39.0",
"@biomejs/biome": "2.3.8",
"@mastra/client-js": "^1.4.0",
"@opentui/core": "^0.2.0",
"@opentui/react": "^0.2.0",
"@sentry/api": "^0.113.0",
"@sentry/node-core": "10.50.0",
"@sentry/sqlish": "^1.0.0",
Expand All @@ -32,13 +30,16 @@
"fast-check": "^4.5.3",
"http-cache-semantics": "^4.2.0",
"ignore": "^7.0.5",
"ink": "^7.0.1",
"ink-spinner": "^5.0.0",
"marked": "^15",
"p-limit": "^7.2.0",
"peggy": "^5.1.0",
"picomatch": "^4.0.3",
"pretty-ms": "^9.3.0",
"qrcode-terminal": "^0.12.0",
"react": "^19.2.5",
"react-devtools-core": "^7.0.1",
"semver": "^7.7.3",
"string-width": "^8.2.0",
"tinyglobby": "^0.2.15",
Expand Down
2 changes: 1 addition & 1 deletion plugins/sentry-cli/skills/sentry-cli/references/init.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Initialize Sentry in your project (experimental)
- `-n, --dry-run - Show what would happen without making changes`
- `--features <value>... - Features to enable: errors,tracing,logs,replay,profiling,ai-monitoring,user-feedback`
- `-t, --team <value> - Team slug to create the project under`
- `--tui - Use the OpenTUI full-screen interface (default on the Bun binary). Pass --no-tui to disable.`
- `--tui - Use the Ink-based interactive UI (default on the Bun binary). Pass --no-tui to fall back to plain log output.`

**Examples:**

Expand Down
60 changes: 36 additions & 24 deletions script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,33 +124,26 @@ async function bundleJs(): Promise<boolean> {
platform: "node",
target: "esnext",
format: "esm",
// Externalize the OpenTUI + React stack from the esbuild
// bundling step. Two reasons:
// Externalize the Ink + React stack from the esbuild bundling
// step. `react`'s CJS jsx-runtime, when pulled into esbuild's
// `__commonJS` wrappers and re-bundled by Bun.compile, produces
// malformed output containing a TDZ `init_react` symbol
// embedded in the wrong scope. Keeping React (and its
// consumers) external lets Bun's runtime resolve them fresh at
// first invocation, outside the buggy bundler path.
//
// 1. `@opentui/core` ships Bun-specific
// `import "..." with { type: "file" }` syntax for
// tree-sitter assets (`*.scm`, `*.wasm`) that esbuild
// doesn't understand. Bun.compile downstream resolves
// them natively and embeds the assets into the binary.
//
// 2. `react`'s CJS jsx-runtime, when pulled into esbuild's
// `__commonJS` wrappers and re-bundled by Bun.compile,
// produces malformed output containing a TDZ
// `init_react` symbol embedded in the wrong scope. We
// sidestep this by keeping React out of esbuild AND
// reaching it only through the embedded `opentui-app.tsx`
// asset (see `src/lib/init/ui/opentui-ui.ts`'s
// `with { type: "file" }` import) — Bun's runtime
// resolves React fresh at first invocation, outside the
// buggy bundler path.
// The npm bundle (`script/bundle.ts`) externalizes the same
// packages for the same reason — bundling Ink's React tree
// through esbuild produces a CJS wrapper that hits a TDZ at
// runtime when React is first touched.
external: [
"bun:*",
"@opentui/core",
"@opentui/core/*",
"@opentui/react",
"@opentui/react/*",
"ink",
"ink-spinner",
"react",
"react/*",
"react-reconciler",
"react-reconciler/*",
],
sourcemap: "linked",
// Minify syntax and whitespace but NOT identifiers. Bun.build
Expand Down Expand Up @@ -322,6 +315,25 @@ async function compileTarget(target: BuildTarget): Promise<boolean> {
try {
const result = await Bun.build({
entrypoints: [BUNDLE_JS],
// Force React to load its production builds. React's CJS
// entry switches at runtime via
// `if (process.env.NODE_ENV === "production")`
// — leaving NODE_ENV unset would drag in the development
// builds, whose CJS wrappers Bun.compile can't bundle cleanly
// (it injects `__promiseAll` runtime helpers in positions the
// dev-build's IIFE doesn't tolerate, causing a SyntaxError at
// startup). Production builds parse fine.
//
// `react-devtools-core` is gated behind `process.env.DEV ===
// "true"` inside Ink's reconciler — never reached in our
// production binary. We still install it as a devDep so
// Bun.compile can resolve the static `import devtools from
// "react-devtools-core"` reference; without it the build
// fails with "Could not resolve". The inlined module gets
// dead-code-eliminated by the DEV gate at runtime.
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
},
compile: {
target: getBunTarget(target) as
| "bun-darwin-arm64"
Expand Down Expand Up @@ -508,11 +520,11 @@ async function build(): Promise<void> {
await uploadSourcemapToSentry();

// Clean up intermediate bundle (only the binaries are artifacts).
// The `opentui-app.tsx` copy comes from the text-import-plugin's
// The `ink-app.tsx` copy comes from the text-import-plugin's
// `with { type: "file" }` handling — it gets embedded into the
// compiled binary, so the sidecar copy is no longer needed once
// every target has compiled.
await $`rm -f ${BUNDLE_JS} ${SOURCEMAP_FILE} dist-bin/opentui-app.tsx`;
await $`rm -f ${BUNDLE_JS} ${SOURCEMAP_FILE} dist-bin/ink-app.tsx`;

// Summary
console.log(`\n${"=".repeat(40)}`);
Expand Down
46 changes: 27 additions & 19 deletions script/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,22 +215,29 @@ const result = await build({
// Replace import.meta.url with the injected shim variable for CJS
"import.meta.url": "import_meta_url",
},
// Externalize Node.js built-ins, plus the OpenTUI + React stack.
// OpenTUI ships native Zig bindings that only load under the Bun
// runtime, so the npm/Node distribution must NOT bundle them. The
// factory in `src/lib/init/ui/factory.ts` lazy-imports the OpenTUI
// path and falls back to LoggingUI on import failure, so marking
// these external means a Node user simply gets the non-TUI flow
// without a crash. The Bun compile (`script/build.ts`) bundles
// them into the native binary, where the loader is available.
// Externalize Node.js built-ins, plus Ink + React + companions.
// Ink uses top-level await (in `node_modules/ink/build/reconciler.js`
// and `yoga-layout/dist/src/index.js`) which esbuild can't emit in
// a CJS bundle, so the packages must stay external for the
// npm/Node distribution. The factory in `factory.ts` lazy-imports
// the Ink path via `with { type: "file" }` and falls back to
// `LoggingUI` on import failure, so a Node user without Ink
// installed simply gets the non-TUI flow without a crash.
//
// The Bun compile (`script/build.ts`) embeds `ink-app.tsx` as a
// file resource — at runtime Bun's loader resolves Ink + React
// fresh, sidestepping the same CJS-wrapping bug that'd hit if
// these were bundled into the binary's pre-compiled JS.
external: [
"node:*",
"@opentui/core",
"@opentui/core/*",
"@opentui/react",
"@opentui/react/*",
"ink",
"ink-spinner",
"react",
"react/*",
"react-reconciler",
"react-reconciler/*",
"react-devtools-core",
"yoga-layout",
],
metafile: true,
plugins,
Expand Down Expand Up @@ -293,15 +300,16 @@ await Bun.write("./dist/index.d.cts", TYPE_DECLARATIONS);
console.log(" -> dist/bin.cjs (CLI wrapper)");
console.log(" -> dist/index.d.cts (type declarations)");

// Clean up the `opentui-app.tsx` sidecar that the text-import-plugin
// Clean up the `ink-app.tsx` sidecar that the text-import-plugin
// drops into `dist/` when it sees the `with { type: "file" }` import
// in `src/lib/init/ui/opentui-ui.ts`. The npm distribution doesn't
// run the OpenTuiUI factory at all (it's gated to the Bun binary),
// so the sidecar is unused — and it's not in `package.json#files`
// either, so it wouldn't ship even without this cleanup. Removing
// it just keeps the local `dist/` directory tidy.
// in `src/lib/init/ui/ink-ui.ts`. The npm distribution doesn't run
// the InkUI factory at all (it's gated to the Bun binary because
// Ink uses top-level await that we can't bundle into CJS), so the
// sidecar is unused — and it's not in `package.json#files` either,
// so it wouldn't ship even without this cleanup. Removing it just
// keeps the local `dist/` directory tidy.
try {
await unlink("./dist/opentui-app.tsx");
await unlink("./dist/ink-app.tsx");
} catch {
// Sidecar may not exist (e.g. plugin path not exercised) — fine.
}
Expand Down
26 changes: 7 additions & 19 deletions script/text-import-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,10 @@
* runtime.
*
* Used by `script/build.ts` (single-file executable) and
* `script/bundle.ts` (CJS library bundle) so:
*
* 1. The grep-worker source in `src/lib/scan/worker-pool.ts` loads
* correctly in both dev and compiled builds (`text` branch).
* 2. `src/lib/init/ui/opentui-app.tsx` ships embedded into the
* Bun binary as a file resource (`file` branch). `OpenTuiUI`
* then `await import(path)`s it at runtime, sidestepping a Bun
* bundler bug that mangles React's CJS jsx-runtime wrapping
* when reached through static imports inside `__commonJS`
* scope. Embedding the .tsx as raw bytes pushes resolution to
* Bun's runtime (not bundler), which doesn't have the bug.
* `script/bundle.ts` (CJS library bundle) so the grep-worker source
* in `src/lib/scan/worker-pool.ts` loads correctly in both dev and
* compiled builds (`text` branch). The `file` branch is kept for
* future use; today no source file goes through it.
*/

import { copyFileSync, mkdirSync, readFileSync } from "node:fs";
Expand Down Expand Up @@ -54,14 +47,9 @@ export const textImportPlugin: Plugin = {
// Bun.compile resolves imports relative to the bundle file's
// directory at compile time, not the original source.
//
// The npm bundle path (`script/bundle.ts`) also reaches this
// branch — `opentui-ui.ts` has the import at module top —
// but `@opentui/*` and `react` are externalized there, so
// the OpenTuiUI factory never runs and the embedded copy is
// unused at runtime. We still produce it because esbuild
// resolves all reachable imports regardless of whether they
// execute. The `mkdirSync` below guards against the
// bundle's `outdir` not yet existing when the plugin fires.
// `mkdirSync` guards against the bundle's `outdir` not yet
// existing when the plugin fires — esbuild creates the
// outdir lazily on first write.
const sourcePath = resolvePath(args.resolveDir, args.path);
const outdir = build.initialOptions.outdir
? resolvePath(build.initialOptions.outdir)
Expand Down
40 changes: 21 additions & 19 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@ type InitFlags = {
readonly features?: string[];
readonly team?: string;
/**
* Default `true` (OpenTUI is the default UI). Stricli auto-generates
* a negated `--no-tui` flag that flips this to `false` — that's the
* escape hatch users invoke when the OpenTUI path misbehaves. The
* positive `--tui` flag is also accepted for symmetry but is a no-op
* versus the default.
* Default `true` (Ink is the default UI on the Bun binary). Stricli
* auto-generates a negated `--no-tui` flag that flips this to
* `false` — that's the escape hatch users invoke when the Ink path
* misbehaves (e.g. on unusual terminal emulators). The positive
* `--tui` flag is also accepted for symmetry but is a no-op versus
* the default. On the npm/Node distribution this flag has no
* effect; the factory always picks `LoggingUI` there.
*/
readonly tui: boolean;
};
Expand Down Expand Up @@ -237,7 +239,7 @@ export const initCommand = buildCommand<
tui: {
kind: "boolean",
brief:
"Use the OpenTUI full-screen interface (default on the Bun binary). Pass --no-tui to disable.",
"Use the Ink-based interactive UI (default on the Bun binary). Pass --no-tui to fall back to plain log output.",
default: true,
},
},
Expand Down Expand Up @@ -307,21 +309,21 @@ export const initCommand = buildCommand<
} finally {
// 7. macOS-only force-exit safety net.
//
// On Darwin, `runWizard` installs the `/dev/tty` forwarding
// workaround from stdin-reopen.ts to get keystrokes through to
// clack. That workaround opens a second `tty.ReadStream` which
// leaks a libuv handle on Bun 1.3.11 — no userland cleanup
// releases it (upstream oven-sh/bun#29126). After `runWizard`
// returns (or throws), the event loop stays ref'd and the process
// hangs until the user presses a key.
// On Darwin, `InkUI` opens a fresh `/dev/tty` `tty.ReadStream`
// (so Ink's `useInput` actually receives keystrokes — Bun's
// `process.stdin` doesn't deliver `readable` events properly,
// see oven-sh/bun#6862 / vadimdemedes/ink#636). The fresh
// stream is destroyed in the InkUI dispose path, but Bun's
// libuv handle for it can linger past `destroy()` on Darwin
// (oven-sh/bun#29126), keeping the event loop ref'd so the
// process hangs until the user presses a key.
//
// The .unref() timer doesn't hold the loop itself, so it's a no-op
// in the happy path (Linux: no workaround installed, loop drains
// naturally; `--yes` on Darwin: no prompts, no keystroke issue,
// may still drain naturally). On the Darwin hang path, it
// force-exits after a 100ms grace window — imperceptible to the
// user and enough for Sentry telemetry + stdio flushes to
// complete first.
// in the happy path (Linux: handle drains naturally; `--yes`
// on Darwin: LoggingUI doesn't open /dev/tty, may still drain
// naturally). On the Darwin hang path, it force-exits after a
// 100ms grace window — imperceptible to the user and enough
// for Sentry telemetry + stdio flushes to complete first.
//
// Skipped under `bun test` (which sets NODE_ENV=test automatically)
// because the test runner calls `initCommand.func` directly; an
Expand Down
4 changes: 2 additions & 2 deletions src/lib/init/clack-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export const STEP_LABELS: Record<string, string> = {
/**
* Canonical execution order of the wizard's workflow steps.
*
* Used by the OpenTUI sidebar's progress checklist as the static
* Used by the Ink sidebar's progress checklist as the static
* pre-rendered list. The wizard advertises step transitions via
* `WizardUI.setStep(...)`; the store back-fills any earlier
* `pending` rows as `skipped` when a later step starts (the workflow
Expand All @@ -154,7 +154,7 @@ export const CANONICAL_STEP_ORDER: readonly string[] = [

/**
* Subset of {@link CANONICAL_STEP_ORDER} surfaced in the progress
* checklist. The OpenTUI sidebar is 36 cols wide and shares vertical
* checklist. The Ink sidebar is 36 cols wide and shares vertical
* space with the tip card and the files-read panel, so showing all
* 12 step rows would push the files panel off-screen on shorter
* terminals.
Expand Down
8 changes: 4 additions & 4 deletions src/lib/init/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
* the UI implementations render. The previous version assembled
* terminal-flavored markdown (color tags, an aligned key/value table,
* a tree of changed files) and pushed it through `ui.log.message`.
* That worked for `LoggingUI` (which calls `renderMarkdown`) but
* showed literal markup like `<yellow>~</yellow>` and pipe-cells in
* `OpenTuiUI` because TextRenderable can't parse markdown — only
* strip ANSI.
* That worked for `LoggingUI` (which calls `renderMarkdown`) but the
* earlier TUI showed literal markup like `<yellow>~</yellow>` and
* pipe-cells because the underlying text primitive couldn't parse
* markdown — only strip ANSI.
*
* Now `formatResult` calls `ui.summary(structuredData)` and lets each
* implementation decide how to lay it out. `formatError` still uses
Expand Down
4 changes: 2 additions & 2 deletions src/lib/init/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
* Low-level git primitives live in `src/lib/git.ts`. This module
* re-exports them for backward compatibility and adds the interactive
* `checkGitStatus` orchestrator. All UI I/O is routed through the
* injected `WizardUI` so the same code drives clack, OpenTUI, and the
* non-interactive `LoggingUI` paths.
* injected `WizardUI` so the same code drives `InkUI` (interactive)
* and `LoggingUI` (CI / npm) paths.
*/

import {
Expand Down
4 changes: 2 additions & 2 deletions src/lib/init/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* Respects --yes flag for non-interactive mode.
*
* All UI I/O goes through the injected `WizardUI` so the dispatcher
* works identically against `ClackUI` (interactive), `LoggingUI` (CI),
* and the upcoming OpenTUI implementation.
* works identically against `InkUI` (interactive Bun binary) and
* `LoggingUI` (CI / npm fallback).
*/

import chalk from "chalk";
Expand Down
4 changes: 2 additions & 2 deletions src/lib/init/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export type WizardOptions = {
org?: string;
project?: string;
/**
* Force the non-OpenTUI fallback (`LoggingUI`). Mapped from
* `--no-tui`. Acts as an escape hatch when the OpenTUI path
* Force the non-Ink fallback (`LoggingUI`). Mapped from
* `--no-tui`. Acts as an escape hatch when the Ink TUI
* misbehaves; in an interactive run this effectively disables
* prompts (any prompt path will throw a `LoggingUIPromptError`),
* so users hitting this flag should also pass `--yes` or set
Expand Down
Loading
Loading