Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 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
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 @@ -32,6 +32,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
217 changes: 213 additions & 4 deletions bun.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
"devDependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@biomejs/biome": "2.3.8",
"@clack/prompts": "^0.11.0",
"@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 @@ -21,6 +22,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 @@ -36,6 +38,7 @@
"picomatch": "^4.0.3",
"pretty-ms": "^9.3.0",
"qrcode-terminal": "^0.12.0",
"react": "^19.2.5",
"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 OpenTUI full-screen interface (default on the Bun binary). Pass --no-tui to disable.`

**Examples:**

Expand Down
37 changes: 34 additions & 3 deletions script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,34 @@ async function bundleJs(): Promise<boolean> {
platform: "node",
target: "esnext",
format: "esm",
external: ["bun:*"],
// Externalize the OpenTUI + React stack from the esbuild
// bundling step. Two reasons:
//
// 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.
external: [
"bun:*",
"@opentui/core",
"@opentui/core/*",
"@opentui/react",
"@opentui/react/*",
"react",
"react/*",
],
sourcemap: "linked",
// Minify syntax and whitespace but NOT identifiers. Bun.build
minify: true,
Expand Down Expand Up @@ -480,8 +507,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 `opentui-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`;

// Summary
console.log(`\n${"=".repeat(40)}`);
Expand Down
32 changes: 30 additions & 2 deletions script/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,23 @@ 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 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.
external: [
"node:*",
"@opentui/core",
"@opentui/core/*",
"@opentui/react",
"@opentui/react/*",
"react",
"react/*",
],
metafile: true,
plugins,
});
Expand Down Expand Up @@ -278,6 +293,19 @@ 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
// 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.
try {
await unlink("./dist/opentui-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
88 changes: 73 additions & 15 deletions script/text-import-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
/**
* 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.
* `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.
*/

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 +39,53 @@ 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.
//
// 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.
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
18 changes: 18 additions & 0 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ type InitFlags = {
readonly "dry-run": boolean;
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.
*/
readonly tui: boolean;
};

/**
Expand Down Expand Up @@ -226,6 +234,12 @@ export const initCommand = buildCommand<
brief: "Team slug to create the project under",
optional: true,
},
tui: {
kind: "boolean",
brief:
"Use the OpenTUI full-screen interface (default on the Bun binary). Pass --no-tui to disable.",
default: true,
},
},
aliases: {
...DRY_RUN_ALIASES,
Expand Down Expand Up @@ -285,6 +299,10 @@ 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.
Expand Down
Loading
Loading