diff --git a/NET10/BlazorInteractiveServer/Components/Demo/ComplexComponentsDemo.razor b/NET10/BlazorInteractiveServer/Components/Demo/ComplexComponentsDemo.razor
index bf45f91..b74ef99 100644
--- a/NET10/BlazorInteractiveServer/Components/Demo/ComplexComponentsDemo.razor
+++ b/NET10/BlazorInteractiveServer/Components/Demo/ComplexComponentsDemo.razor
@@ -333,7 +333,7 @@
+ CurrentPageChanged="HandlePageChange" />
diff --git a/README.md b/README.md
index 4315130..755dfbf 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ ShellUI is pre-1.0, so APIs and generated output can still change between minor
## Capabilities
-- The CLI commands are `init`, `add`, `list`, `remove`, `update` and `doctor`, plus `theme init`, `theme apply`, and `theme update`.
+- The CLI commands are `init`, `add`, `list`, `remove`, `update`, `doctor` and `migrate`, plus `theme init`, `theme apply`, and `theme update`.
- The component registry has **254 entries**: **98 direct install targets** and **156 hidden dependency entries**. `list` shows direct targets; `add` resolves hidden dependencies.
- `0.4.0-alpha.1` adds `kbd`, `aspect-ratio`, `button-group`, `toggle-group`, `input-group`, `number-input`, `stat-card`, `timeline`, `tree-view`, `qr-code`, `image-viewer`, `chat`, `chat-message`, `chat-input`, and the `auth-01`, `auth-02` and `auth-03` blocks.
- `0.3.0` added `typed-select`, `command-palette`, `data-picker`, `multi-select`, `tag-input`, `donut-chart`, `radar-chart`, and `radial-chart`.
diff --git a/ShellUI.Tests/MigrateTests.cs b/ShellUI.Tests/MigrateTests.cs
new file mode 100644
index 0000000..73fd534
--- /dev/null
+++ b/ShellUI.Tests/MigrateTests.cs
@@ -0,0 +1,126 @@
+using ShellUI.CLI.Services;
+
+namespace ShellUI.Tests;
+
+public class MigrateTests : IDisposable
+{
+ private readonly string _root = Path.Combine(Path.GetTempPath(), $"migrate-{Guid.NewGuid():N}");
+
+ public MigrateTests()
+ {
+ Directory.CreateDirectory(Path.Combine(_root, "Components", "UI"));
+ Directory.CreateDirectory(Path.Combine(_root, "Components", "Pages"));
+ Directory.CreateDirectory(Path.Combine(_root, "obj"));
+ File.WriteAllText(Path.Combine(_root, "MyApp.csproj"), "");
+ }
+
+ public void Dispose()
+ {
+ try { Directory.Delete(_root, recursive: true); } catch (IOException) { }
+ }
+
+ private string Page(string name, string content)
+ {
+ var path = Path.Combine(_root, "Components", "Pages", name);
+ File.WriteAllText(path, content);
+ return path;
+ }
+
+ private MigrationEdit SingleEdit(params string[] components) => Assert.Single(MigrateService.Plan(_root, components).Edits);
+
+ [Fact]
+ public void Input_DropsTheNestedVariantEnum()
+ {
+ Page("Login.razor", "\n@code { var v = Input.InputVariant.Default; }");
+
+ var edit = SingleEdit("input");
+
+ Assert.Equal("\n@code { var v = InputVariant.Default; }", edit.After);
+ }
+
+ [Fact]
+ public void DatePicker_RenamesSelectedDateButLeavesCalendarAlone()
+ {
+ Page("Booking.razor",
+ "\n" +
+ " b\" />\n" +
+ "");
+
+ var edit = SingleEdit("date-picker");
+
+ Assert.Equal(
+ "\n" +
+ " b\" />\n" +
+ "", edit.After);
+ }
+
+ [Fact]
+ public void TimePicker_RenamesSelectedTime()
+ {
+ Page("Alarm.razor", "");
+
+ Assert.Equal("", SingleEdit("time-picker").After);
+ }
+
+ [Fact]
+ public void Pagination_RenamesOnPageChangeUnlessCurrentPageIsBound()
+ {
+ Page("List.razor",
+ "\n" +
+ "");
+
+ var edit = SingleEdit("pagination");
+
+ Assert.Equal(
+ "\n" +
+ "", edit.After);
+ }
+
+ [Fact]
+ public void Plan_SkipsInstalledComponentsAndBuildOutput()
+ {
+ File.WriteAllText(Path.Combine(_root, "Components", "UI", "Input.razor"), "Input.InputVariant.Error");
+ File.WriteAllText(Path.Combine(_root, "obj", "Generated.cs"), "Input.InputVariant.Error");
+ var page = Page("Form.razor", "Input.InputVariant.Error");
+
+ var plan = MigrateService.Plan(_root);
+
+ Assert.Equal(page, Assert.Single(plan.Edits).Path);
+ }
+
+ [Fact]
+ public void Plan_WithNoComponentsRunsEveryMigrationAndListsWhatItApplied()
+ {
+ Page("All.razor", "\n");
+
+ var edit = SingleEdit();
+
+ Assert.Equal(2, edit.Applied.Count);
+ Assert.Equal("\n", edit.After);
+ }
+
+ [Fact]
+ public void Calendar_FlagsHandlersThatTakeADateTime()
+ {
+ var page = Page("Plan.razor",
+ "\n@code {\n void OnDay(DateTime day) { }\n void Other(DateTime d) { }\n}");
+
+ var plan = MigrateService.Plan(_root);
+
+ Assert.Empty(plan.Edits);
+ var note = Assert.Single(plan.Notes);
+ Assert.Equal(page, note.Path);
+ Assert.Equal(3, note.Line);
+ }
+
+ [Fact]
+ public void Plan_IsEmptyForAnUpToDateApp()
+ {
+ Page("Fine.razor", "\n\n@code { void OnDay(DateTime? d) { } }");
+
+ var plan = MigrateService.Plan(_root);
+
+ Assert.Empty(plan.Edits);
+ Assert.Empty(plan.Notes);
+ }
+}
diff --git a/docs/CLI_SYNTAX.md b/docs/CLI_SYNTAX.md
index 396d15f..ded3ded 100644
--- a/docs/CLI_SYNTAX.md
+++ b/docs/CLI_SYNTAX.md
@@ -22,6 +22,7 @@ shellui list [--installed|--available]
shellui remove
shellui update [name...] [--all] [--force]
shellui doctor
+shellui migrate [name...] [--dry-run] [--yes]
shellui theme init [--force] [--style