Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
6093faa
feat(init): scaffold WizardUI abstraction layer for OpenTUI migration
MathurAditya724 Apr 28, 2026
d664709
feat(init): migrate wizard call sites to WizardUI
MathurAditya724 Apr 28, 2026
c365f27
feat(init): add OpenTuiUI behind --tui flag
MathurAditya724 Apr 28, 2026
424771d
feat(init): make OpenTuiUI the default and remove ClackUI
MathurAditya724 Apr 28, 2026
44e8c34
chore: regenerate docs
github-actions[bot] Apr 28, 2026
e7f7f94
fix(init): make OpenTuiUI actually render content
MathurAditya724 Apr 28, 2026
86dc19c
feat(init): polish OpenTuiUI visual design
MathurAditya724 Apr 28, 2026
d6c540f
feat(init): rewrite OpenTuiUI in React with sidebar tips and structur…
MathurAditya724 Apr 28, 2026
6daefe0
feat(init): auto-hide tips sidebar on narrow terminals
MathurAditya724 Apr 28, 2026
248f86c
feat(init): clean up wizard chrome and post-completion report
MathurAditya724 Apr 28, 2026
51246bc
fix(ci): scope React hook lint rule and tighten URL test
MathurAditya724 Apr 28, 2026
fc381b5
feat(init): friendlier confirm, clearer multiselect, colored summary
MathurAditya724 Apr 28, 2026
b252bb1
feat(init): explicit Yes/No experimental prompt with muted hints
MathurAditya724 Apr 28, 2026
0635f52
feat(init): tree view for changed files + persistent 'Files analyzed'…
MathurAditya724 Apr 28, 2026
bba4c63
feat(init): replace 'Files analyzed' sidebar panel with inline status…
MathurAditya724 Apr 28, 2026
52b61fe
fix(init): embed opentui-app.tsx so binary build doesn't trip Bun bun…
MathurAditya724 Apr 28, 2026
e9b1bf1
fix(build): mkdir output dir before copying with-file sidecar
MathurAditya724 Apr 28, 2026
80970ce
fix(init): bypass Bun module cache collision in OpenTUI UI
MathurAditya724 Apr 28, 2026
59462ae
feat(init): files-read tree + step progress checklist in sidebar
MathurAditya724 Apr 29, 2026
40741b7
chore(init): drop dead clack-plain imports
MathurAditya724 Apr 29, 2026
d1ca5f7
feat(init): replace OpenTUI with Ink for the wizard UI
MathurAditya724 Apr 29, 2026
59900db
fix(init): make Ink select prompt actually respond to arrow keys
MathurAditya724 Apr 29, 2026
b4a591e
fix(init): make Ink useInput actually deliver keystrokes in Bun
MathurAditya724 Apr 29, 2026
4a3e835
fix(init): clear screen on dispose + tighten sidebar layout
MathurAditya724 Apr 29, 2026
ddf8cb9
chore(init): collapse FilesPanel ternary to satisfy biome formatter
MathurAditya724 Apr 29, 2026
d59356d
fix(init): cooperative Ctrl+C cancellation + sidebar visual polish
MathurAditya724 Apr 30, 2026
30186ad
docs(init): replace stale OpenTUI / OpenTuiUI references with InkUI
MathurAditya724 Apr 30, 2026
b3b6f53
test(init): add Ink App snapshot tests for visual + cancel paths
MathurAditya724 Apr 30, 2026
7a7287a
feat(init): visual scrollbar + keyboard scroll-back in FilesPanel
MathurAditya724 Apr 30, 2026
a83eb5c
feat(init): full-screen wizard UI with TitleBar, tabs, and status bar
getsentry-bot May 4, 2026
7f1b03d
fix(init): guard alternate screen buffer entry with try-catch
getsentry-bot May 4, 2026
68f7c93
refine(init): polish full-screen wizard UI visual hierarchy
getsentry-bot May 4, 2026
b2ce869
fix: defer process.exit in requestCancel so stdout flushes
getsentry-bot May 4, 2026
6044dcd
fix(init): flip layout, clean up progress panel, and surface error de…
getsentry-bot May 4, 2026
b6eb744
fix: remove escape key scrolling to bottom in FilesPanel
getsentry-bot May 4, 2026
a73bcc5
style(init): align post-dispose report colors with Sentry palette
getsentry-bot May 4, 2026
b41747f
feat(init): show banner, move files to dedicated tab, improve error r…
getsentry-bot May 4, 2026
2ff7a49
refine(init): remove TitleBar, add top padding for banner spacing
getsentry-bot May 4, 2026
81ae3f8
fix(init): remove file trees from spinner messages, fix error report …
getsentry-bot May 4, 2026
a8b8c17
fix: remove vertical separator line between split panes
getsentry-bot May 4, 2026
149cc37
feat(init): add 12 wizard UX improvements
getsentry-bot May 4, 2026
fea3352
fix(ink-ui): set activePromptCancel in confirm() for graceful Ctrl+C …
getsentry-bot May 4, 2026
d3e58de
fix(ink-ui): await user dismissal on outro screen before teardown
getsentry-bot May 4, 2026
e1164d6
fix(init): remove line-by-line animation from LearnCard
getsentry-bot May 4, 2026
9cf6915
fix(init): call ui.cancel() before throwing in checkReadiness
getsentry-bot May 4, 2026
dd47734
fix(init): fix LearnCard height and tighten content
getsentry-bot May 4, 2026
9be7c49
fix(init): remove outro screen and press-any-key dismissal
getsentry-bot May 4, 2026
bcfb65c
fix(init): remove left/right padding from main content wrapper
getsentry-bot May 4, 2026
2c0d25e
fix(init): remove 2-col width reduction on StatusScreen
getsentry-bot May 4, 2026
2070265
fix(init): replace paddingRight with gap on split pane row
getsentry-bot May 4, 2026
83f1a64
fix(init): remove centering wrapper to prevent left-side shift
getsentry-bot May 4, 2026
d497cf0
fix(test): avoid active spinner in snapshot test to prevent CI hang
getsentry-bot May 4, 2026
392a0f1
fix(init): center wizard layout with fixed margin instead of flex align
getsentry-bot May 4, 2026
fc9142b
feat(init): show sidebar (learn card + progress) on all tabs
getsentry-bot May 4, 2026
a1bb448
fix(init): resolve Seer review findings
getsentry-bot May 4, 2026
7f165a5
fix(test): add timeout race to waitUntilExit in snapshot tests
getsentry-bot May 4, 2026
95c16b9
fix(test): unref timeout timer in waitUntilExit race to prevent CI hang
getsentry-bot May 4, 2026
90c76b4
fix(init): use process.once for SIGINT handler to allow force-exit on…
getsentry-bot May 4, 2026
361da11
fix(init): use process.on for SIGINT handler so it persists across si…
getsentry-bot May 4, 2026
2c8df61
fix(test): drop waitUntilExit entirely to prevent CI hang
getsentry-bot May 4, 2026
b7d721f
fix(test): use instance API with unref timers for CI compatibility
getsentry-bot May 5, 2026
7e243bd
fix(test): reorder snapshot tests to warm up Ink before wide layout
getsentry-bot May 5, 2026
4ad9e5d
fix(test): mock checkReadiness and update file-read test assertions
getsentry-bot May 5, 2026
28e9e93
fix(init): force-exit on second Ctrl+C during stuck teardown
getsentry-bot May 5, 2026
fa2a351
merge: resolve conflicts with main branch
getsentry-bot May 5, 2026
0ab5877
fix(init): catch LoggingUIPromptError when InkUI fails to load
getsentry-bot May 5, 2026
54b64de
fix(init): guard learnTimer callback against post-teardown execution
getsentry-bot May 5, 2026
46db33c
fix(init): guard startTipRotation against post-teardown race
getsentry-bot May 5, 2026
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
20 changes: 20 additions & 0 deletions biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@
}
},
"overrides": [
{
// The React-hook lint rules infer "this is a hook" from the
// `use*` naming convention. We have a couple of test helpers
// (`useTestConfigDir`, `useEnvSandbox`) that share the prefix
// by coincidence — they register `beforeEach`/`afterEach` and
// have nothing to do with React. Without these overrides every
// call site lights up `useHookAtTopLevel` since making the
// tsconfig JSX-aware (for `OpenTuiUI`) flipped the rule on.
// The actual React tree lives in `src/lib/init/ui/opentui-app.tsx`
// and keeps the rule active.
"includes": ["test/**/*.ts", "src/**/*.ts", "!src/**/*.tsx"],
"linter": {
"rules": {
"correctness": {
"useHookAtTopLevel": "off",
"useExhaustiveDependencies": "off"
}
}
}
},
{
"includes": ["test/**/*.ts"],
"linter": {
Expand Down
90 changes: 87 additions & 3 deletions bun.lock

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"devDependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@biomejs/biome": "2.3.8",
"@clack/prompts": "^0.11.0",
"@mastra/client-js": "^1.4.0",
"@sentry/api": "^0.113.0",
"@sentry/node-core": "10.50.0",
Expand All @@ -21,6 +20,7 @@
"@types/node": "^22",
"@types/picomatch": "^4.0.3",
"@types/qrcode-terminal": "^0.12.2",
"@types/react": "^19.2.14",
"@types/semver": "^7.7.1",
"binpunch": "^1.0.0",
"chalk": "^5.6.2",
Expand All @@ -30,12 +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
1 change: 1 addition & 0 deletions plugins/sentry-cli/skills/sentry-cli/references/init.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +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 Ink-based interactive UI (default on the Bun binary). Pass --no-tui to fall back to plain log output.`

**Examples:**

Expand Down
49 changes: 46 additions & 3 deletions script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,27 @@ async function bundleJs(): Promise<boolean> {
platform: "node",
target: "esnext",
format: "esm",
external: ["bun:*"],
// 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.
//
// 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:*",
"ink",
"ink-spinner",
"react",
"react/*",
"react-reconciler",
"react-reconciler/*",
],
sourcemap: "linked",
// Minify syntax and whitespace but NOT identifiers. Bun.build
minify: true,
Expand Down Expand Up @@ -295,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 @@ -480,8 +519,12 @@ async function build(): Promise<void> {
// Step 3: Upload the composed sourcemap to Sentry (after compilation)
await uploadSourcemapToSentry();

// Clean up intermediate bundle (only the binaries are artifacts)
await $`rm -f ${BUNDLE_JS} ${SOURCEMAP_FILE}`;
// Clean up intermediate bundle (only the binaries are artifacts).
// 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/ink-app.tsx`;

// Summary
console.log(`\n${"=".repeat(40)}`);
Expand Down
40 changes: 38 additions & 2 deletions script/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,30 @@ const result = await build({
// Replace import.meta.url with the injected shim variable for CJS
"import.meta.url": "import_meta_url",
},
// Only externalize Node.js built-ins - bundle all npm packages
external: ["node:*"],
// 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:*",
"ink",
"ink-spinner",
"react",
"react/*",
"react-reconciler",
"react-reconciler/*",
"react-devtools-core",
"yoga-layout",
],
metafile: true,
plugins,
});
Expand Down Expand Up @@ -278,6 +300,20 @@ 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 `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/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/ink-app.tsx");
} catch {
// Sidecar may not exist (e.g. plugin path not exercised) — fine.
}

// Calculate bundle size (only the main bundle, not source maps)
const bundleOutput = result.metafile?.outputs["dist/index.cjs"];
const bundleSize = bundleOutput?.bytes ?? 0;
Expand Down
72 changes: 59 additions & 13 deletions script/text-import-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
/**
* esbuild plugin that polyfills Bun's `with { type: "text" }` import
* attribute (esbuild only supports `json`). Intercepts matching
* imports, reads the file, and default-exports its contents as a
* string. Runtime behavior matches Bun's native handling.
* esbuild plugin that polyfills Bun's `with { type: "text" }` and
* `with { type: "file" }` import attributes (esbuild only supports
* `json`).
*
* - `text` — intercepts the import, reads the file, and default-
* exports its contents as a string. Runtime behavior matches Bun's
* native handling.
* - `file` — copies the source file into the esbuild output
* directory, then marks the import external so the original
* `import path from "./foo" with { type: "file" }` clause
* survives in the bundled JS. Bun.compile downstream understands
* the attribute natively, embeds the file as a binary asset, and
* resolves the import to a virtual-filesystem path string at
* runtime.
*
* Used by `script/build.ts` (single-file executable) and
* `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.
* compiled builds (`text` branch). The `file` branch is kept for
* future use; today no source file goes through it.
*/

import { readFileSync } from "node:fs";
import { resolve as resolvePath } from "node:path";
import { copyFileSync, mkdirSync, readFileSync } from "node:fs";
import { basename, dirname, resolve as resolvePath } from "node:path";
import type { Plugin } from "esbuild";

const TEXT_IMPORT_NS = "text-import";
Expand All @@ -21,13 +32,48 @@ export const textImportPlugin: Plugin = {
name: "text-import",
setup(build) {
build.onResolve({ filter: ANY_FILTER }, (args) => {
if (args.with?.type !== "text") {
return null;
if (args.with?.type === "text") {
return {
path: resolvePath(args.resolveDir, args.path),
namespace: TEXT_IMPORT_NS,
};
}
return {
path: resolvePath(args.resolveDir, args.path),
namespace: TEXT_IMPORT_NS,
};
if (args.with?.type === "file") {
// Copy the source into the bundle's output directory and
// rewrite the import path so it sits next to the bundle.
// esbuild keeps the import external (preserving the
// `with { type: "file" }` clause) so Bun.compile can pick
// it up from the new location. The copy is needed because
// Bun.compile resolves imports relative to the bundle file's
// directory at compile time, not the original source.
//
// `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)
: dirname(resolvePath(build.initialOptions.outfile ?? "."));
const filename = basename(sourcePath);
const copyPath = resolvePath(outdir, filename);
try {
mkdirSync(outdir, { recursive: true });
copyFileSync(sourcePath, copyPath);
} catch (err) {
// Surface the failure so the build fails visibly rather
// than producing a binary that crashes at startup.
throw new Error(
`text-import-plugin: failed to copy ${sourcePath} → ${copyPath}: ${
err instanceof Error ? err.message : String(err)
}`
);
}
return {
path: `./${filename}`,
external: true,
};
}
return null;
});
build.onLoad({ filter: ANY_FILTER, namespace: TEXT_IMPORT_NS }, (args) => {
const content = readFileSync(args.path, "utf-8");
Expand Down
46 changes: 33 additions & 13 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ type InitFlags = {
readonly "dry-run": boolean;
readonly features?: string[];
readonly team?: string;
/**
* 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 @@ -226,6 +236,12 @@ export const initCommand = buildCommand<
brief: "Team slug to create the project under",
optional: true,
},
tui: {
kind: "boolean",
brief:
"Use the Ink-based interactive UI (default on the Bun binary). Pass --no-tui to fall back to plain log output.",
default: true,
},
},
aliases: {
...DRY_RUN_ALIASES,
Expand Down Expand Up @@ -285,25 +301,29 @@ export const initCommand = buildCommand<
team: flags.team,
org: explicitOrg,
project: explicitProject,
// `flags.tui` defaults to `true`. `--no-tui` (auto-generated
// by stricli's flag negation) flips it to `false` — that's the
// signal we forward to the factory as `forceLegacyUi`.
forceLegacyUi: flags.tui === false,
});
} 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
Loading
Loading