@migrate-sdk/tui
v0.14.0
Published
Interactive terminal UI for Migrate SDK
Readme
Migrate TUI
Explore and operate migrations without memorizing CLI flags. For local use, the
TUI's Node Migrate Server loads the same migrate.config.ts as the CLI. The TUI
provides status, item counts, messages, dependency-aware execution plans, and
contextual actions for every registered migration.
The migration list opens after connecting, without waiting for durable status. Status loads once in the background with a Loading status… indicator and a session activity entry. Navigation and operations remain available while it loads. Unloaded counts are shown as Not loaded, not zero.
For expensive stores, defer the first status read until you press R (Shift+R):
pnpm exec migrate-tui --status manual
pnpm exec migrate-tui --server https://migrate.example.com/api/rpc --status manualThe default is --status background. Both modes retain the last loaded idle
status until R requests another read. When a status read finds an active run,
or you start a run from the TUI, live dashboard updates continue until no runs
remain active. Runs started elsewhere while the dashboard is idle appear after
R. Status failures leave the list usable and can be retried with R.
If the renderer restarts after an error, it reuses the last dashboard state. An idle manual session stays idle; an active run resumes live updates. Source scans that find active runs also restore live updates and run controls.
Requires Node.js 22.19.0 or newer. Install it in the migration project alongside the SDK:
pnpm add migrate-sdk effect
pnpm add --save-dev @migrate-sdk/tui
pnpm exec migrate-tuiThe published command is a small Node launcher. It runs the renderer with the
pinned Bun runtime supplied by the package, so users do not need a global Bun
installation. The renderer starts a local Node Migrate Server and communicates
with it over Effect RPC. migrate.config.ts, migrate-sdk, migration
dependencies, stores, sources, destinations, and execution adapters all remain
in Node, matching the local CLI runtime contract. See
ADR 0007.
Programmatic consumers use the same client/server boundary:
import { makeMigrationTuiRuntime } from "@migrate-sdk/tui";
const runtime = await makeMigrationTuiRuntime({ cwd: process.cwd() });
try {
const dashboard = await runtime.refresh();
console.log(dashboard.rows);
} finally {
await runtime.dispose?.();
}The package does not expose the in-process server runtime from its public entry point.
Set MIGRATE_SERVER_BUILD_ID to the immutable identifier of the application
build that contains the migration configuration. A changed build ID selects a
separate local Node server endpoint; the previous endpoint keeps running until
its active Migration Runs finish. This is deployment-skew identity, not a value
to update after editing migrate.config.ts, and it does not reload local
source. ADR 0008 defines
the separate Local Source Generation model. Without a build ID, local clients
continue to reuse the server identified by the config path and Migrate SDK
version.
Connect to a deployed Migrate Server without loading a local migration config:
pnpm exec migrate-tui --server https://migrate.example.com/api/rpcWhen MIGRATE_SERVER_TOKEN is set, its value is sent as an HTTP Bearer token.
Source, destination, Migration Store,
and Execution Adapter credentials remain in the remote environment. The TUI
requires the complete Migrate Server contract, so local and remote connections
provide the same dashboard, messages, operations, source scanning, active-run
discovery, source-identity history, and run controls. Availability that depends
on a selected migration or run is reported in its data—for example, whether
rollback or stopping is supported.
Remote clients and servers may use different Migrate SDK releases when they advertise the same Migrate Protocol version. The server's SDK version remains available in connection metadata for diagnostics; protocol compatibility is the remote connection gate. Schema administration requires Migrate Protocol v2, so update the TUI and server together.
If the configured SQL store needs an upgrade, the TUI connects and opens a
Store schema upgrade required popup before loading the dashboard. Review
the pending changes and choose Upgrade store (u). The server applies the
reviewed plan, then the dashboard opens. Cancel keeps the connection open;
r reopens the plan and q quits. Errors remain visible for retry.
Local connections reuse sqlStore in migrate.config.*. Remote hosts supply
the same SQL client layer and table prefix as sqlStore on
RegistryMigrateServer.layer or MigrateServer.layer. The database credentials
stay on the server. Non-SQL stores do not need this setup.
Transport and server hosts can integrate directly with the Effect services
exported as MigrateClient from migrate-sdk/client and MigrateServer from
migrate-sdk/server. The local TUI supplies a child-process transport and a
Node bootstrap that discovers migrate.config.*. Remote hosts instead supply
an already-imported registry and execution-adapter Layer to the registry-backed
server; no config file is required.
Remote hosts can expose the same server Layer as a Web-standard HTTP handler:
import { Effect, Layer } from "effect";
import {
HttpMiddleware,
HttpServerRequest,
HttpServerResponse,
} from "effect/unstable/http";
import {
MigrateServerHttp,
RegistryMigrateServer,
} from "migrate-sdk/server/http";
const executableLayer = /* the selected execution-adapter Layer */;
const serverLayer = RegistryMigrateServer.layer({
environment: {
id: "production",
label: "Production",
},
registry,
}).pipe(Layer.provide(executableLayer));
const httpLayer = MigrateServerHttp.layer.pipe(Layer.provide(serverLayer));
const authorize = HttpMiddleware.make((httpApp) =>
HttpServerRequest.HttpServerRequest.pipe(
Effect.flatMap((request) =>
verifyMigrateRequest(request)
? httpApp
: Effect.succeed(
HttpServerResponse.text("Unauthorized", { status: 401 }),
),
),
),
);
const remoteServer = MigrateServerHttp.toWebHandler(httpLayer, authorize);
export const POST = (request: Request) => remoteServer.handler(request);The host route owns the public URL; the Migrate Server handler is routerless and does not need to know where it is mounted.
On a serverless or otherwise short-lived HTTP host, executableLayer must use a
durable Execution Adapter. An inline Execution Adapter is supported only when
the Migrate Server process remains alive for the entire run; ending that process
ends the inline execution.
Authorization is application-owned Effect HTTP middleware so deployments can use their existing identity provider without leaving the request fiber. Deployments where authenticated infrastructure already enforces access can omit the middleware when converting the fully composed Layer. The HTTP transport uses bounded observation leases: each response returns an opaque resume token and absolute progress snapshot, and the TUI reconnects from the last token. No function invocation or HTTP response owns the lifetime of a durable Migration Run.
migrate-sdk and effect remain peer dependencies: the TUI and config use the
migration project's compatible versions. @migrate-sdk/tui and migrate-sdk
are published with matching versions.
For workspace development:
pnpm --filter @migrate-sdk/tui demoUse an explicit project config:
pnpm --filter @migrate-sdk/tui dev -- --config ./migrate.config.tsThe npm package is the supported local distribution. A compiled renderer binary remains available for packaging experiments and version smoke tests:
pnpm --filter @migrate-sdk/tui build:binary
(cd packages/tui && ./dist/binary/migrate-tui --version)The renderer binary is not currently a standalone Migrate TUI distribution: a
functional release also needs the Node Migrate Server entry and the migration
project's compatible migrate-sdk and effect packages. Cross-compilation,
the companion server layout, signing gates, and OpenCode references are documented
in docs/research/tui-binary-distribution.md.
The footer keeps primary and contextual shortcuts visible for the current
selection. Press Enter to open All actions, which includes the complete action
set such as rescan, update, and Concurrency settings. Concurrency settings provides
session-scoped concurrency overrides for the Process Pipeline, Rollback Pipeline,
and source scans; blank values preserve the configured defaults and
Process or Rollback concurrency can be set to unbounded. Press c to open
Concurrency settings and g to switch
between migration and group tabs, m for errors and messages, r to run the selected migration or group,
e to open Run selected entries, f to retry failed items, b to
rollback, s to scan sources for the current selection and its
required dependencies, l to open Session activity, R to reload status, and
q to quit. Source scan results stay visible when switching migrations or
scanning other sources; R clears them when reloading status. When applicable, t
retries skipped items, v focuses a running migration, x requests a safe stop
for a run owned by the connected Migrate Server, and u opens
the guarded break-lock confirmation. Use
Page Up and Page Down to scroll the overview while the arrow keys continue to
select migrations. Messages are loaded only when you press m or open the Messages tab. Latest
message shows m to load until then. Loaded results, including empty results,
are cached per migration or group for the session; navigation does not reload
them. Observed changes to run history or durable item counts mark affected
caches stale, including their groups. Press m again to reload a stale result
or retry a failed read. An unchanged status refresh keeps the cache. Source
scans and lock heartbeats do not invalidate it. Changes made by other clients
while this TUI is idle are discovered on R, just like status changes.
The Messages tab displays a bounded list with the current message highlighted;
use the arrow keys to move through it and Enter to open the complete message
and structured details. For non-interactive inspection or
export, use migrate messages <migration> or
migrate messages --all --json from the same project.
Run selected entries opens Next items, where you can enter an item limit for this run. Each migration gets its own limit, including any dependencies you add. Unchanged items do not count; failed and skipped attempts do. Press Enter or choose Run to start. If dependencies need attention, choose whether to include them or run without them.
For a single migration, use Next items or Source IDs to choose how to select items. From the input, press Shift+Tab to focus the selection control, use Left/Right to switch, then Tab to return to the input. You can also click the tabs. Adding an ID keeps Source IDs active, and reopening the dialog remembers your selection method. The ordinary Run action stays unlimited.
In Source IDs, the IDs set the scope. By default, unchanged migrated items stay unchanged. Enable Update: reprocess unchanged items to process the selected IDs again. From the ID input, press Tab then Space to toggle Update, or click its label; choose Update to start. This preserves other items and saved scan progress. Update resets to off when you reopen the dialog or switch selection methods. The separate Update action still updates the whole migration.
Session activity keeps the statuses, notices, warnings, and errors observed by
the current TUI session in chronological order, including active-run lifecycle
changes discovered through dashboard observation. Use the arrow keys, j/k,
Page Up and Page Down, or Home and End to navigate. Press Enter to read and
scroll the complete selected event, or e to export the retained entries as
JSON Lines without replacing an existing file. Session activity is limited to
the current TUI process; durable Migration Messages remain available through
the Messages tab and CLI after the TUI closes.
Runs start directly when their dependencies are ready. A group concurrency override controls item processing within each migration; migration definitions still execute in SDK plan order. If required dependencies lack completion, the TUI asks whether to include them or force the selected run.
Rollback offers two scope choices in one confirmation dialog. Include
dependencies (i) is selected by default and recommended; the plan rolls back
dependent migrations first. Selected only (s) leaves those migrations
outside the plan. If they still have tracked items, the dialog explains that
their records will remain and references may break; confirming this choice
authorizes forced rollback. If they have no tracked items, force is unnecessary.
Review the updated plan and choose Rollback selected (y) to execute it
once. Selected-entry rollback keeps dependency inclusion disabled and preserves
the selected identities. A failed plan or status update stays in the dialog
for retry; confirmation waits for a successfully prepared plan.
While a run is active, committed cursor-window checkpoints
carry cumulative run counts and trigger targeted durable-status refreshes for
the migration that made progress.
Inline runs and runs managed by an Execution Adapter therefore update in committed batches without
scanning their sources; after an idle interval, a slower full-plan refresh
covers adapters that cannot publish checkpoints. When an execution adapter supports native observation, the
TUI also waits through its Execution Adapter identity. If that observation
channel is unavailable, the TUI reports the fallback and continues following
durable run state. Execution Adapter failure or cancellation is reconciled against
durable terminal state before it is reported. q, Ctrl+C, SIGINT, SIGTERM, and
SIGHUP end the current observation and close the TUI without stopping the run.
The local Node Migrate Server owns each run independently and remains available
while any run is active, so a later TUI session can reconnect by Migration Run
id. Non-overlapping migrations can run concurrently; plans that include an
already-running definition are rejected by its Migration Definition Lock. The
server exits after the final client disconnects when no run remains active. A
second Ctrl+C, or a five-second graceful-shutdown timeout, always restores the
terminal and exits.
The server discovers active runs from non-terminal durable run state whose run id still owns a Migration Definition Lock. When that state includes an Execution Adapter identity, View run follows the Reconnectable Migration Run's existing checkpoints and terminal result by Migration Run id. Closing that observation leaves the Reconnectable Migration Run active. The current protocol can stop an inline run owned by the connected Migrate Server. Provider-owned runs report stopping as unsupported until their Execution Adapter implements cancellation. Break lock removes stale lock ownership but does not cancel provider work.
If React or the terminal renderer fails unexpectedly, the TUI destroys the failed renderer, reloads durable migration state, and creates one fresh UI session. An active execution remains owned by the runtime and the replacement screen reattaches to its status. A second renderer failure exits normally after restoring the terminal instead of entering a restart loop.
Terminal regression test
Pilotty is pinned as a development dependency. Exercise the real PTY interaction and responsive-layout path with:
pnpm --filter @migrate-sdk/tui test:pilottyThe harness verifies planned retry and rollback scopes, view transitions, cooperative cancellation and draining, the 72×34 compact dashboard, and the 120×36 confirmation views. It prints the directory that contains its text snapshots for further inspection.
The compiled-binary smoke check builds and relocates the host executable, loads its embedded version, and verifies it can start independently. The npm package smoke and local IPC test cover real migration execution:
pnpm --filter @migrate-sdk/tui test:binary
pnpm --filter @migrate-sdk/tui test:ipc