typr tui: interactive editor for typr.yaml (jatatui) + closed-beta gate#192
Merged
Conversation
…er channel
Everything on this branch that isn't the interactive TUI / closed-beta
work. Splits cleanly from the TUI delta in the next commit.
────────────────────────────────────────────────────────────────────────
bleep M3 → M10 + built-in publishing
────────────────────────────────────────────────────────────────────────
- bleep.yaml: `\$version: 1.0.0-M3` → `1.0.0-M10`
- Migrated from custom publish scripts to bleep's built-in `publish`
subcommand:
* removed `scripts.Publish` (used CiReleasePlugin + manual
coursier Info / packageLibraries plumbing)
* removed `scripts.PublishLocal` (manual `commands.publishLocal`)
* removed `scripts.projectsToPublish` (filter list)
* removed `build.bleep::bleep-plugin-ci-release` from
typr-scripts deps (added `bleep-core` directly because
CompileBenchmark / GeneratedShowcase / GitOps were resolving
`bleep.*` and `ryddig.*` transitively through it)
* removed `my-publish-local:` + `publish:` script entries
* added `template-publishable` (groupId dev.typr,
sonatypeProfileName com.olvind, MIT, single developer) and
wired all 11 publishable projects (typr, typr-codegen,
typr-dsl, typr-dsl-{scala,kotlin,anorm,doobie,zio-jdbc},
typr-runtime-{anorm,doobie,zio-jdbc}) to extend it as a list
Verified via `bleep publish local-ivy --dry-run`: 204 files, all
POMs carry the correct groupId / description / url / license /
developer.
Distribution stays via Maven Central. `coursier-channel.json` at the
repo root is a Coursier channel descriptor (same pattern bleep uses)
pinning `dev.typr:typr_3:latest.release`, mainClass `typr.cli.Main`,
`-XX:+UseG1GC`. Users install with:
cs install --channel https://raw.githubusercontent.com/typr-dev/typr/main/coursier-channel.json typr
────────────────────────────────────────────────────────────────────────
scalafmt + `-no-indent`
────────────────────────────────────────────────────────────────────────
- `.scalafmt.conf`: added scala3 dialect overrides for the scala-3
source roots that weren't already covered — typr/src/scala,
typr-codegen/src/scala, typr-scripts/src/scala,
typr-scripts-sourcegen/src/scala. Without these, scalafmt's
default `Scala213Source3` dialect chokes on `enum` / `given` /
optional-braces syntax that the Scala 3 code uses.
- `bleep.yaml`: added `-no-indent` to `template-scala-3` scala
options so the compiler enforces brace syntax for all Scala 3
code (no significant-indentation drift between authors).
- Ran `scalac -no-indent -rewrite` once over the affected projects
to auto-convert existing indent-style → brace-style; ~115 files
changed. One manual fix afterwards: a match expression in
`typr/cli/app/screens/OutputEditor.scala` lost a comma between a
`} match` clause and the next named argument.
(The TUI source files included in the rewrite belong with the next
commit, not this one — they're added by the TUI commit on top of this
state.)
────────────────────────────────────────────────────────────────────────
Misc
────────────────────────────────────────────────────────────────────────
- `TypoLogger.Console` now prefixes lines with `typr:` instead of
the leftover `typo:`.
- Site: getting-started rewritten around closed-beta access (drops
placeholder install / quickstart, points at oyvind@typr.dev for
seats); landing-page polish (announcement bar, mobile layout).
- `typr.yaml` is heavily reordered by ConfigWriter's
sortKeys + dropNullKeys pass — functionally identical.
- `.gitignore` ignores `.claude/scheduled_tasks.lock`; that file is
removed from the tracked tree.
- Small regenerated test row tweaks under
`testers/.../product_summary` and `testers/.../inventory_check`
from a regen pass against the current schema.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CLI shell already existed (generate / watch / check, decline +
cats-effect). This commit layers two new surfaces on top.
────────────────────────────────────────────────────────────────────────
1. Interactive TUI — `typr tui` (new app/ + screens/)
────────────────────────────────────────────────────────────────────────
A jatatui-based editor for `typr.yaml` so you can create / edit /
delete sources, outputs, and types without hand-writing the YAML.
Built on **jatatui** (`com.olvind.jatatui` 0.30.0) — a Java port of
tui-rs with a React-style component layer. Six artifacts wired into
the typr project: jatatui-{core,widgets,crossterm,react,components} +
crossterm (JNI). Several higher-level components started life inside
`typr.cli.app.components` and were lifted upstream during the work;
what's still local is `CaretCell` (typr-specific clickable tree caret)
and `JatatuiInterop` (Scala 3 doesn't auto-SAM-convert bound
`() => Unit` to `Runnable`, and `Boolean => Element` doesn't fit
`j.u.f.Function[j.l.Boolean, Element]` because of primitive boxing, so
we wrap with `Run.given` Conversion + a `Link.focusable` façade).
Entry point: `typr tui` → `App.run` opens the terminal in raw mode and
runs a hand-rolled loop (resize / key / mouse → renderer.dispatch).
`Shell` mounts a Router over an `AppApi` context that screens read via
`RenderContext.useContext`.
`AppApi` (typr/cli/app/AppApi.scala):
- config: TyprConfig (parsed typr.yaml)
- configPath: Path
- updateConfig(f) — mutates + writes via ConfigWriter
- sourceCache: Map[String, LoadedSource] (one per source)
- loadStatus: Map[String, LoadStatus] (NotLoaded / Loading /
Loaded / Failed)
- quit() — flips the running flag
`LoadedSource` (typr/cli/app/LoadedSource.scala) is a sealed sum
(`Db(MetaDb)` | `Spec(ParsedSpec)` | `Avro(List[AvroSchemaFile])` |
`Proto(List[ProtoFile])`) with one `load(json, buildDir)` codec that
dispatches off the typed `ParsedSource` — no 4× isXxx() checks
re-parsing the JSON.
Shell forks one daemon thread per source on mount; caches and statuses
fill in per-source as each load completes. Screens render purely from
`sourceCache` + `loadStatus` — no per-screen re-fetch.
Screens (typr/cli/app/screens/):
Splash logo + tagline; 2s auto-dismiss (only after
acceptance); chip with closed-beta status +
expiry; `t` re-opens BetaNotice (read-only)
BetaNotice full-screen terms modal; Initial gates the rest
of the app, ReadOnly is reached via `t`
MainMenu 6-card grid: Sources / Schemas / Outputs /
Domain Types / Field Types / Generate
SourceList /
SourceEditor /
SourceForm /
SourceWizard add/edit/delete sources; live form; "test
connection" button (real JDBC probe); "used by
N outputs:" with clickable links
OutputList /
OutputEditor /
OutputWizard add/edit/delete outputs
TypeList /
TypeEditor /
TypeWizard field types or domain types depending on kind;
FieldType editor: deep editor for api/db/model
match patterns + validation rules + "preview
matches" against cached MetaDb's;
DomainType editor: primary + description + fields
rows (add/delete/rename via FieldSpecObject) +
aligned-source rows + generate options, with
a live alignment matrix below (✓ match / ⚠
missing / · extras footer)
SchemaPicker /
SchemaBrowser /
SpecBrowser /
AvroBrowser /
ProtoBrowser per-source-kind browsers; fuzzy `/` search; `e`
extracts a field as a FieldType; `c` pushes
DomainFromEntity
DomainFromEntity two-pane discovery: left = every entity from
loaded sources grouped by source; right = live
preview of the would-be DomainType + alignment
suggestions across other sources
FieldTypeForm /
DomainTypeForm create-new flows
Generate status + per-source load badges (left) +
per-output ProgressTracker rows (right) +
log panel; daemon ticker keeps duration counters
smooth
Utilities:
ConnectionTest opens a Hikari JDBC connection + reads metadata
TypePreview runs FieldType DbMatch patterns against a
cached MetaDb, returns matched columns
EntityCatalog uniform "record-shaped thing" across all four
source kinds; field-name normalisation
(snake↔camel) for cross-source alignment scoring
New subcommands wired in Main.scala:
typr tui — opens the editor
typr init — drops a starter typr.yaml + opens the TUI
typr terms — prints closed-beta terms
────────────────────────────────────────────────────────────────────────
2. Closed-beta acceptance gate + July 1, 2026 timebomb
────────────────────────────────────────────────────────────────────────
Typr launches as a partially paid product. Before any tier / pricing
details land in the binary, every first-run user sees a four-bullet
terms modal: (1) not open source; (2) commercial DBs won't stay free;
(3) a limit on how many boundaries take part in domain-type alignment
per generate run is coming; (4) this build expires 2026-07-01 — keep
yours fresh from typr.dev.
`typr.cli.beta`:
- BetaTerms — display text, one-liner, termsVersion (bumpable to
force re-prompt on material edits), expiresOn (2026-07-01),
warningWindowDays (14)
- BetaGate — XDG-aware acceptance file at
`$XDG_CONFIG_HOME/typr/beta-accepted.txt` (with
`~/.config/typr/...` / `%APPDATA%/typr/...` fallbacks); plain-text
accepted-at / binary-version / terms-version keys; queries
isCurrent / daysUntilExpiry / isExpired / isInWarningWindow plus
three pre-formatted user-facing messages
CLI gating (Main.scala):
- Every subcommand takes `--accept`; passing it writes the
acceptance file inline before running the command.
- GateLevel.CliStrict (generate / watch / check) refuses when not
accepted, with a hint pointing at `--accept` or `typr terms`.
- GateLevel.TuiOrInit (tui / init) skips the CLI refusal — the user
accepts inside the in-TUI modal instead.
- GateLevel.Informational (terms) bypasses both gates so users can
always read what they're agreeing to.
- Expiry runs first regardless: post-2026-07-01 every command prints
a stderr block pointing at https://typr.dev/get and exits 78.
Warning window: from 14 days before expiry, the CLI emits a logger.warn
countdown line (visible in CI logs). The TUI splash chip turns yellow
and counts down to the day.
────────────────────────────────────────────────────────────────────────
Known followups
────────────────────────────────────────────────────────────────────────
- ConfigWriter loses untyped fields on round-trip (e.g. saga /
state-machine YAML blocks). TUI edits that write typr.yaml will
silently drop content not modelled by the generated boundary case
classes. Either preserve unknown keys, or restrict TUI saves to
the typed surface.
- Tier-based codegen limits (per the pricing tiers) are out of
scope here — replaced with the simpler "closed beta · these
things will eventually be paid" gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's in this PR
Screen.Recording.2026-05-18.at.01.33.51.mp4
A quick tour of
typr tui— the new interactive editor fortypr.yaml, built on the jatatui-react library.typr: interactive TUI (jatatui) + closed-beta gateThe visible one.
typr tuiopens an interactive editor fortypr.yaml— sources, outputs, field types, domain types — without ever hand-writing the YAML.Built on jatatui 0.30.0 — a Java port of tui-rs with a React-style component layer, sister project to typr. Six artifacts: jatatui-{core, widgets, crossterm, react, components} + crossterm JNI. Several higher-level components started life inside
typr.cli.app.componentsduring this work and were lifted upstream; what's still local isCaretCell(typr-specific tree caret) andJatatuiInterop(a small Scala 3 SAM-conversion façade).Screens (
typr/cli/app/screens/):SplashBetaNoticetMainMenuSourceList/Editor/Form/WizardOutputList/Editor/WizardTypeList/Editor/WizardSchemaPicker+Schema/Spec/Avro/ProtoBrowser/,eextracts a field as a FieldType,cpushes the entity intoDomainFromEntityDomainFromEntityGenerateRuntime (
typr/cli/app/):AppApicarriesconfig+sourceCache+loadStatus+updateConfig(writes back via ConfigWriter) +quitLoadedSourceis a sealed sum (Db/Spec/Avro/Proto) with oneload(json, buildDir)codec dispatching off the typedParsedSource— no 4×isXxx()reparsingShellforks one daemon thread per source on mount; screens render purely fromsourceCache+loadStatus— no per-screen re-fetchApp.runopens the terminal in raw mode and runs a hand-rolledresize / key / mouse → renderer.dispatchloopClosed-beta gate (
typr/cli/beta/):BetaTerms— display text +termsVersion(bumpable to force re-prompt) +expiresOn = 2026-07-01+warningWindowDays = 14BetaGate— XDG-aware acceptance file at$XDG_CONFIG_HOME/typr/beta-accepted.txt(with~/.config/typr/.../%APPDATA%/typr/...fallbacks); queriesisCurrent/daysUntilExpiry/isExpired/isInWarningWindowRefactored
Main.scalaso there is exactly one path fromOpts.subcommandto a runnable body — the privatecmd(name, help, level)helper. Every verb picks aGateLevel:CliStrictgenerate/watch/checkTuiOrInittui/initInformationaltermsAdding a new verb means picking a level. There is no overload that skips the gate.
Side effect:
--acceptis now uniform across every subcommand, sotypr terms --acceptreads + accepts in one step — a lovely first-run flow.Also in this PR:
build: bleep M10 + built-in publishing, scalafmt + -no-indent, coursier channel— mechanical-but-load-bearing housekeeping that the TUI work sits on top of.bleep M3 → M10, custom publishing → built-in
bleep publish:Publish.scala,PublishLocal.scala,projectsToPublish.scalatemplate-publishabletemplate — groupIddev.typr, sonatypeProfileNamecom.olvind, MIT, single developerextends:lists:typr,typr-codegen,typr-dsl,typr-dsl-{scala,kotlin,anorm,doobie,zio-jdbc},typr-runtime-{anorm,doobie,zio-jdbc}bleep-plugin-ci-releaseswapped forbleep-coreintypr-scripts(CompileBenchmark / GeneratedShowcase / GitOps still needbleep.*andryddig.*)bleep publish local-ivy --dry-runproduces 204 files with the correct POMs across the lot-no-indentontemplate-scala-3:scalac -no-indent -rewritepass rewrote ~115 files automatically; one manual comma fix inOutputEditor.scalaafterwards.scalafmt.conffortypr/typr-codegen/typr-scripts/typr-scripts-sourcegencoursier-channel.jsonat the repo root mirrors bleep's install pattern:Resolves
dev.typr:typr_3:latest.releasedirectly from Maven Central — no separate release pipeline of our own.Misc:
TypoLoggerprefixtypo:→typr:typr.yamlheavily reordered by ConfigWriter'ssortKeys + dropNullKeyspass — semantically identical.claude/scheduled_tasks.lockuntracked + gitignoredtesters/.../product_summaryandtesters/.../inventory_checkBoth commits compile independently — verified at
1dcb545e8and at the tip.Test plan
bleep --no-color compileis green at the tipbleep publish local-ivy --dry-run --version 0.0.0-testproduces 11 publishable projects' POMs withdev.typrgroupId / MIT licensebleep run typr -- generate --acceptruns end-to-end against a real schematypr tuiopens, navigates, save round-trips through ConfigWritertypr termsprints the terms text + warning line if within 14 days of2026-07-01typr generatewithout acceptance refuses withnotAcceptedMessage;typr generate --acceptaccepts and runs2026-07-01(roll the system clock to test), every gated subcommand exits 78 withexpiredMessage;typr termsstill prints text (Informational level)cs install --channel https://raw.githubusercontent.com/typr-dev/typr/main/coursier-channel.json typrresolves once the channel file lands onmain🤖 Generated with Claude Code