magic 0.0.6
magic: ^0.0.6 copied to clipboard
A Laravel-inspired Flutter framework with Eloquent ORM, routing, and MVC architecture.
Changelog #
All notable changes to this project will be documented in this file.
[Unreleased] #
0.0.6 - 2026-07-29 #
Contributing checklist (before merging into [Unreleased]) #
- ❌ CHANGELOG entry added under the appropriate bucket (BREAKING / Added / Changed / Removed / Fixed / Improvements)
- ❌
doc/updated when the change touches public-facing behavior - ❌
README.mdupdated when the change touches the overview or quick-start - ❌
skills/magic-framework/updated when the change touches APIs the skill documents - ❌
example/updated when the change touches the canonical consumer scaffold - ❌
flutter testgreen;dart analyzeclean;dart formatno diff;dart pub publish --dry-runno blocking errors
0.0.5 - 2026-07-26 #
Added #
-
Model.save()now exposes the backend's per-field 422 errors instead of discarding them.save()returned only abool, so a form that wrote through the ORM could tell that a remote save failed but not why, and every 422 collapsed into a generic "something went wrong" toast. A failed remote save now captures the Laravel validation shape ({"message": ..., "errors": {"field": ["message"]}}) into two new members onInteractsWithPersistence:validationErrors(Map<String, List<String>>) andvalidationError(field)(the first message for one field). The map is cleared at the start of every remote save, so it stays empty after a save that succeeded or carried no field errors, and it also stays empty when the remote leg throws (a transport failure), which is how a caller distinguishes a field-validation failure from a network failure: an empty map plus afalsereturn means "render a generic error". It is deeply unmodifiable (both the map and each message list), and it tracks the REMOTE leg rather thansave()'s return value, so a hybrid model (useRemoteanduseLocal) whose remote save 422s while its local write succeeds returnstruewith the errors filled. Theboolreturn contract is unchanged, so this is purely additive for existing callers. Toucheslib/src/database/eloquent/concerns/interacts_with_persistence.dart; covered by theInteractsWithPersistence validation errorsgroup intest/database/eloquent/model_test.dart; documented indoc/eloquent/getting-started.md(Inserting & Updating -> Validation Errors) andskills/magic-framework/references/forms-validation.md(Server Error Mapping). -
MagicRouternow re-runs its redirect chain when auth state changes, not only on navigation. The router evaluated its guards (the'auth'/'guest'redirects) only while resolving a route, so a login or logout that happened while the user was already sitting on a page (a token expiry, a background sign-out, a successful login on the auth screen) did not move them off a now-forbidden route until the next manual navigation. The router now listens to the auth guard's state notifier and refreshesrouterConfigon a change, so an auth transition re-evaluates redirects immediately (an expired session bounces to login; a login leaves the guest-only auth screen). Consumers with no boundauthguard are unaffected (the notifier is absent and the listener is a no-op). Toucheslib/src/routing/magic_router.dart; covered bytest/routing/router_auth_refresh_test.dart.
Fixed #
-
LocalizationServiceProvidernow bootsDateManager, solocalization.timezoneandauto_detect_timezonefinally do something on their own. Nothing in the framework ever calledDateManager.instance.boot(), which meant the IANA database was never initialized, both timezone config keys were inert, and theX-Timezoneheader thatLocalizationInterceptorsends on every request reported the unbooted default rather than the device's zone. Every consumer had to boot it by hand beforerunApp, and a consumer that did not know to do so shipped a wrong header silently. The provider now boots it as the first thing inboot(), symmetrically with how it already handlesauto_detect_locale. Booting is idempotent and cannot fail startup (an unresolvable zone degrades to UTC). Toucheslib/src/localization/localization_service_provider.dart; covered bytest/localization/localization_service_provider_boot_test.dart. -
DateManagerno longer crashes application startup when it falls back to UTC._setTimezoneInternalresolved every zone throughtz.getLocation, including its own UTC fallback, but the database loaded fromtimezone/data/latest.darthas NO entry namedUTC(it shipsEtc/UTC). SogetLocation('UTC')threw, and because that call sat inside thecatchblock that was supposed to handle an unresolvable zone, the exception escapedboot()and tookMagic.init()down with it. The same gap made_isValidTimezone('UTC')return false, so the documented default oflocalization.timezonewas reported as invalid. Both paths now resolve through the package's consttz.UTClocation, whose name is the canonical'UTC', so an app that cannot detect a zone (or that simply keeps the default) boots and reportsUTCinstead of throwing. This was latent until detection stopped guessing: whiledetectTimezone()always returned a plausible city, the fallback was unreachable. Toucheslib/src/support/date_manager.dart; covered by theDateManager UTC resolutiongroup intest/support/date_manager_timezone_test.dart. -
MagicApplicationnow followsLang.currentfor runtime locale changes, eliminating the need for consumer workarounds. WhenLang.setLocalewas called at runtime to change the app's language, the locale reverted on the next widget rebuild becauseMaterialApp.localewas wired to the static config value, not the liveLang.currentstate. Consumers had to hand-write aListenableBuilderthat listened toTranslator.instanceand passedlocale: Lang.currentdown the tree to make language switching work.MagicApplicationnow bindslocaletoLang.currentdirectly, so runtime language switching works transparently, and consumers with the hand-written workaround can delete it. Explicitlocale:arguments passed toMagicApplicationstill take precedence, and config stays authoritative while no runtime locale has been loaded (Lang.isLoadedfalse), so an app whose translator is bound but not yet booted keeps its configured locale instead of snapping to the translator'sendefault. The subscription is aListenableBuilderonTranslator.instancescoped insideMagicAppWidget, belowWindTheme, which is the same scopeMagic.reload()refreshes: rebuilding fromMagicApplicationitself would handWindThemea freshWindThemeDataon every locale change and put the live brightness toggle at stake. Toucheslib/src/foundation/magic_app_widget.dart; covered bytest/foundation/magic_app_widget_locale_test.dart; documented indoc/digging-deeper/localization.md. -
DateManager.detectTimezone()now reads the real IANA timezone identifier instead of guessing from UTC offset. The method first triedDateTime.now().timeZoneName, which returns an abbreviation like+03orEETon most platforms, then fell back to finding the first timezone-database location whose current UTC offset matched the device. An offset does not uniquely identify a zone; Istanbul and Kyiv share the same winter offset but differ in DST rules, so a device's timezone could be misidentified. Detection now uses theflutter_timezonepackage to read the real IANA identifier from the platform, and when no valid zone resolves, returnsnulland leaves the configured default in place instead of guessing. Consumers who worked around the issue by addingflutter_timezoneas a dependency and manually calling a detection service beforerunAppcan now remove that code. Detection stays opt-in throughlocalization.auto_detect_timezone(defaultfalse), and the private offset-scanning helper_findTimezoneByOffsetis deleted rather than left unreachable. Toucheslib/src/support/date_manager.dartandpubspec.yaml(addsflutter_timezone); covered bytest/support/date_manager_timezone_test.dart; documented indoc/digging-deeper/localization.md(Timezone Detection) anddoc/digging-deeper/carbon.md(Timezone Support). -
MagicFeedbacktoasts now show in Scaffold-less (Wind-only) views instead of throwing or silently doing nothing.Magic.error/Magic.success/MagicFeedback.inforouted throughScaffoldMessenger.of(context).showSnackBar, which asserts_scaffolds.isNotEmptywhen no MaterialScaffoldhosts the view. In a Wind-built screen (noScaffold) that assertion escaped the caller's owntry/catchand stalled the flow. Toast delivery now goes through the Navigator overlay, read fromnavigatorKey.currentState.overlay(NOTOverlay.maybeOf, which sits above that overlay), as a single non-interactive auto-dismissing bottom entry that replaces the previous one and degrades to a logged warning when no overlay is available (never throwing). TheMagic.error/success/toastAPI surface is unchanged; only the delivery path is. Toucheslib/src/ui/magic_feedback.dart; covered bytest/ui/magic_feedback_test.dart. -
MagicFeedbackoverlay toasts render clean text and degrade without throwing. The overlay toast content was not wrapped in aMaterial, so its text inherited the root fallbackDefaultTextStyle(the yellow debug double-underline); it now sits under a transparentMaterial, matching the dialog / loading builders. The unusedbackgroundColor/colorparameters onshowSnackbar(the overlay path never applied them) are removed, and the degrade-path warnings now log throughLogonly when thelogservice is bound (falling back todebugPrint), so feedback triggered beforeMagic.initbinds logging degrades instead of throwingService [log] is not registered. Toucheslib/src/ui/magic_feedback.dart; covered bytest/ui/magic_feedback_test.dart. -
TitleManagertreats a blank title suffix as absent. Anullsuffix was already skipped, but an empty or whitespace suffix (e.g. an unsetAPP_NAMEresolving to"") still produced"Route | "with a trailing separator and an empty tail. A blank suffix is now treated as absent (via_withSuffix), so the browser tab shows just the route title. Toucheslib/src/routing/title_manager.dart; covered bytest/routing/title_manager_test.dart.
0.0.4 - 2026-07-08 #
Added #
design:sync+design:lintcommands make DESIGN.md the single source of truth for the app theme. Two new commands joinMagicArtisanProvider.design:syncparses aDESIGN.md(YAML front matter: color roles with a single-filedark:overlay, typography,rounded,spacing, andcomponentscarrying{colors.x}/{rounded.x}/{spacing.x}references; the markdown body is ignored), resolves the references against a dotted-path symbol table with a cycle guard, and emits a wind theme source file (--output, defaultlib/config/wind_theme.g.dart). The generated file exposesMap<String, String> designAliasescarrying the 17 property-prefixed semantic keys (bg-surface,text-fg,border-color-border, ...) with arbitrary-hex light +dark:pairs ('bg-surface': 'bg-[#f9f9ff] dark:bg-[#0f1419]'), drop-in forWindThemeData(aliases: ...)and matching theMagicStarterTokens.defaultAliasescontract, plus a brandprimaryMaterialColorwith a generated 50-900 ramp (seeded from the DESIGN.mdprimarylight hex) forWindThemeData.toThemeData()Material interop. It writes atomically via.tmp+ rename and is idempotent (byte-identical output on re-run for an unchanged DESIGN.md).design:lintvalidates a DESIGN.md against six rules ported from the open design.md reference linter and adapted to the wind-flavored superset: broken-ref (error), missing-primary (warning), unknown-key (warning; thedark:overlay lives insidecolorsand is structurally never a top-level key, so it is never flagged), section-order (warning), missing-sections (info), orphaned-tokens (warning, with Material Design 3 baseline families exempt), and contrast-ratio (warning; a greenfield WCAG relative-luminance helper does sRGB channel linearization + the 4.5:1 ratio check on each componentbackgroundColor/textColorpair). The Tailwind/DTCG export-conformance and rem-based spacing/rounded rules from the reference linter are intentionally dropped (wind uses 4px logical spacing and arbitrary-hex aliases). The command exits nonzero only on an error-severity finding. Toucheslib/src/cli/commands/design_sync_command.dart,lib/src/cli/commands/design_lint_command.dart,lib/src/cli/helpers/design_md_parser.dart,lib/src/cli/magic_artisan_provider.dart; addstest/cli/commands/{design_sync,design_lint}_command_test.dart+test/cli/helpers/design_md_parser_test.dart; documented indoc/packages/magic-cli.md(including the DESIGN.md format page).previews:refresh+make:componentcodegen commands for the design-first preview catalog. Two new commands joinMagicArtisanProvider.previews:refreshscans a configurable target directory (--path, defaultlib) for*.preview.dartfiles, extracts the single public*Previewclass from each via regex (the private_*Statecompanion of a stateful preview is ignored), validates the class name is a clean PascalCase identifier before interpolation, fails fast on a slug collision, sorts deterministically, and renders<scan-dir>/_previews.g.dartthrough an atomic.tmp+ rename. The generated file returns a freshly-builtList<PreviewEntry>from thepreviewEntries()FUNCTION (never a top-level const list) so the dev-only catalog tree-shakes from release builds (dart-lang/sdk#33920); it importsPreviewEntryfrompackage:magic_devtools/preview.dartand each preview widget by its path relative to the generated file (preview files are not exported from any barrel). Re-running the command produces a byte-identical file.make:component <Name> [--variants=intent,size] [--slots]extendsArtisanGeneratorCommandand scaffolds the canonical 4-file atomic component folder underlib/ui/components/<name>/(<name>.dart,<name>.recipe.dart,<name>.preview.dart,index.dart): the class is unprefixed PascalCase, the recipe is seeded with the requested variant axes (or aWindSlotRecipeshape under--slots), the index re-exports the component + recipe but not the preview, then the command chainspreviews:refreshso the new preview lands in_previews.g.dart. Toucheslib/src/cli/commands/previews_refresh_command.dart,lib/src/cli/commands/make_component_command.dart,lib/src/cli/helpers/previews_index_writer.dart,lib/src/cli/helpers/magic_stub_loader.dart(addsloadFrom),lib/src/cli/magic_artisan_provider.dart, and six stubs underassets/stubs/; addstest/cli/commands/{previews_refresh,make_component}_command_test.dart.magic:install --with-devtoolswires the debug trio in one step. Installing the optional debug tooling (magic_devtools+fluttersdk_dusk+fluttersdk_telescope) previously meant a manual multi-step bootstrap: add three deps, runplugin:installtwice, thendusk:install+telescope:install. The new--with-devtoolsflag does all of it after the core install: it adds the three packages todependencies(regular, notdev_dependencies, becauselib/main.dartimports them and thekDebugModegate tree-shakes the subsystem from release builds, sodev_dependencieswould tripdepend_on_referenced_packages) and wireslib/main.dartunderkDebugModeexactly asdusk:install/telescope:installdo:DuskPlugin.install()andTelescopePlugin.install()(plusExceptionWatcher+DumpWatcher) beforeMagic.init(), thenMagicDuskIntegration.install()andMagicTelescopeIntegration.install()after it. The wiring is a pure-functional, idempotent transform (buildDevtoolsWiring) over the generated main.dart, and the dep-add rides the sameinstaller.addDependencymechanism the install already uses, so re-runningmagic:install --with-devtoolsnever duplicates a wiring block or a dependency entry. The injected package imports are placed within the existing package-import group (before the relativeconfig/...imports, withpackage:flutter/foundation.dartordered beforepackage:flutter/material.dart), so the generated main.dart staysdirectives_ordering-clean and a freshly installed app emits no analyzer warnings. Absent the flag, nothing changes for the existing install path. Toucheslib/src/cli/commands/magic_install_command.dart; adds theMagicInstallCommand.buildDevtoolsWiringtest group plus a real-FS full-install group totest/cli/commands/magic_install_command_test.dart.MagicMiddleware.redirectTarget(String location)for pre-build redirect guards. Redirect-style guards (auth / guest) can now return a redirect target synchronously, evaluated inside the router'sredirectcallback BEFORE any page builds. Previously the only way to redirect was an imperativeMagicRoute.to()insidehandle(), which runs post-mount and remounts the destination view, recreating its form state on every mount (the login-double-mount bug)._handleRedirectnow evaluates every matched route's global + route middlewareredirectTargetand returns the first non-null target. The default returnsnull, andhandle()now defaults tonext(), so a redirect-only guard overrides justredirectTarget. Fully backward compatible: existinghandle()-based guards keep working. Toucheslib/src/http/middleware/magic_middleware.dart,lib/src/routing/magic_router.dart; addstest/routing/redirect_guard_mount_test.dart(asserts the destination mounts exactly once, including through a layout ShellRoute).
Fixed #
Pick.saveFileis source-compatible with file_picker 12. file_picker 12 madeFilePicker.saveFile'sfileNameandbytesparameters required and non-null, which broke the analyzer build (argument_type_not_assignable) under a freshflutter pub getthat resolved the newer file_picker.Pick.saveFilekeeps its nullable facade surface but now guards both arguments before forwarding, so the call type-checks against file_picker 11 and 12 and a null argument fails with a clearArgumentErrorinstead of an unhelpful type error. Toucheslib/src/facades/pick.dart.file_pickerconstraint tightened to exclude the 12.0.0 prerelease line. The constraint is now>=11.0.2 <12.0.0-0to lock the 11.x stable releases and exclude every12.0.0-*prerelease. A<12.0.0bound would NOT have been enough: pub_semver orders prereleases below the stable release (12.0.0-beta < 12.0.0), so12.0.0-betastill satisfied it; the-0suffix is the lowest possible prerelease and excludes the entire12.0.0line. This pairs with thePick.saveFilesource-compatibility guard above as defense in depth. Touchespubspec.yaml.Cryptnow accepts thebase64:app key thatkey:generateproduces.key:generatewritesAPP_KEY=base64:<base64 of 32 random bytes>, butEncryptionServiceProviderrequiredapp.keyto be a raw 32-character string and threwApp Key must be 32 characters for AES-256on the generated key, soCrypt.encrypt/decryptwere unusable out of the box. AddedMagicEncrypter.fromAppKey(appKey)which base64-decodes abase64:-prefixed key to its 32 bytes (and still accepts a raw 32-character key);EncryptionServiceProvidernow binds through it. Toucheslib/src/encryption/magic_encrypter.dart,lib/src/encryption/encryption_service_provider.dart; adds threefromAppKeycases totest/encryption/magic_encrypter_test.dart.MagicStatefulViewnow calls the controller'sonInit()lifecycle hook.MagicStatefulViewState.initStatelistened to the controller and called the VIEW's ownonInit()hook, but never invoked the CONTROLLER'sonInit(), despite the documented contract. A controller that bootstraps inonInit(initial data load, table creation, subscriptions) silently never ran it when backed by aMagicStatefulView, so the screen rendered against uninitialized state (e.g. a query against a table the controller'sonInitwas supposed to create). It now calls_controller.onInit()guarded byMagicController.initialized, so aSimpleMagicControllerthat already initialized in its constructor is not double-initialized and a singleton controller reused across re-mounts initializes exactly once per lifetime. Toucheslib/src/ui/magic_view.dart; addstest/ui/magic_view_controller_oninit_test.dart.- Auth no longer warns on every boot of a fresh app.
AuthServiceProvider.boot()logged auserFactory not registeredwarning (blaming provider order) whenever no userFactory was set, even for apps with no stored session to restore. It now only warns when a stored session actually exists (Auth.hasToken()) but cannot be rebuilt; a fresh app or a logged-out user stays quiet (debug-level). The stored-session check is guarded so a misconfigured Auth (for example, no Vault registered) cannot crash boot from this warning-verbosity path. Toucheslib/src/auth/auth_service_provider.dart; adds three cases totest/auth/auth_test.dart.
Changed #
fluttersdk_windconstraint bumped to^1.2.0. Requires wind 1.2.0'sWindRecipe/WindSlotRecipe(thetv()-equivalent recipe API, NEW in 1.2.0 and re-exported bypackage:magic/magic.dart— the design-first component layer andmake:componentscaffolds depend on it), plus the intrinsic-safe flex, the seededprimarytoken, the min-width-stretch scroll, and theh-full-inside-vertical-scroll dev assert. Also picks up wind 1.1.0's Material-freeWInput/WTextrewrite, 1.1.1's two fixes that magic's W-widget UI depends on (WInputnative text selection restored (mouse drag-select, double-tap word, long-press), andWTextnow inherits an ancestorDefaultTextStylecolor (the CSS text-color cascade) before falling back to the OS-brightness baseline; the latter fixes invisible labels on magic's W-rendered surfaces (Magic*View,MagicFeedback, dialog buttons whose color lives on the container) when the app theme disagrees with the OS theme), and 1.1.2'sWPopoverfix: a popover with an interactive trigger (aWButton/WAnchorwith its ownonTap) now opens reliably and no longer dismisses itself on the opening gesture, with the trigger kept accessible via aSemanticstap action. This is the primitive behind magic_starter's team selector and user/notification dropdowns. Touchespubspec.yaml.- Debug-tooling install guidance corrected to regular
dependencies. Themagic:installpost-install message recommended addingmagic_devtools/fluttersdk_dusk/fluttersdk_telescopetodev_dependencies, but the install commands wire them intolib/main.dart(underkDebugMode), which trips thedepend_on_referenced_packageslint. They are now documented as regulardependencies(tree-shaken from release viakDebugMode), matching dusk/telescope's own install docs. Also bumps the message's stalefluttersdk_dusk ^0.0.7to^0.0.8. Touchesinstall.yaml.
0.0.3 - 2026-06-17 #
Stabilization (magic-stabilize-dusk-telescope plan) #
-
BREAKING: the Dusk + Telescope Magic adapters moved out of magic core into the new sibling
magic_devtoolspackage.MagicDuskIntegration(14 enrichers),MagicTelescopeIntegration(5 watchers +MagicHttpFacadeAdapter) and their tests now live inmagic_devtools; magic core no longer depends onfluttersdk_duskorfluttersdk_telescopeat all. The class and function names are unchanged; only the import path moves and ownership shifts to a dedicated dev-tooling package. Consumer migration (pre-1.0 clean break, no shim):// before (interim sub-barrel, never released): import 'package:magic/dusk_integration.dart'; import 'package:magic/telescope_integration.dart'; // after — add magic_devtools as a dev_dependency, then: import 'package:magic_devtools/dusk.dart'; import 'package:magic_devtools/telescope.dart'; // MagicDuskIntegration.install(); / MagicTelescopeIntegration.install();Deletes
lib/src/cli/{dusk,telescope}_integration.dart, thelib/{dusk,telescope}_integration.dartsub-barrels, andtest/cli/{dusk,telescope}_integration_test.dartfrom magic; drops the twofluttersdk_dusk/fluttersdk_telescopedependency lines frompubspec.yaml. -
Granular scaffold + documentation is the default (M1). The E2E-drivability defaults (
processingListenable+MagicBuilder, stableValueKey,semanticLabelon ambiguous interactive widgets) are documented in.claude/rules/testability.mdand reflected in generated view stubs. Opt-in, no runtime behavior break for existing consumers. -
Testability rules formalized (M2).
.claude/rules/testability.mddefines view drivability as the third gate of "done" alongside passing tests and correct appearance, with the three widget-identity rules dusk depends on. -
fluttersdk_artisanconstraint bumped^0.0.7->^0.0.8. Drop-in: magic uses no artisan symbol changed between the two versions.
Fixed (consumer-blocking bugs surfaced by /tmp fresh-app E2E test plan) #
make:*commands now work on consumers that pull magic from pub.dev / path: dependency.MakeControllerCommand,MakeModelCommand, and the other 12make:*commands used to callStubLoader.load('controller')directly, which searches$ARTISAN_STUBS_DIR→$MAGIC_CLI_STUBS_DIR→fluttersdk_artisan-<version>/assets/stubs/. Magic's own stubs live at<magic>/assets/stubs/; neither env var was set in typical environments, and the fluttersdk_artisan pub-cache fallback contained only artisan substrate stubs. The 14 generators now load raw stub content via the newMagicStubLoaderhelper (which resolves<magic>/assets/stubs/<name>.stubfrom the consumer's.dart_tool/package_config.jsonmagic entry) and pass the content throughgetStub()forArtisanGeneratorCommand.buildClassto consume as a literal template. Addslib/src/cli/helpers/magic_stub_loader.dart; toucheslib/src/cli/commands/make_*.dart× 14.magic:installis now self-registering — adds magic to.artisan/plugins.jsonbeforeplugins:refreshruns, soMagicArtisanProviderappears inlib/app/_plugins.g.dartautomatically. Consumers no longer need a separatedart run magic:artisan plugin:install magicstep before invokingmake:controlleretc. Toucheslib/src/cli/commands/magic_install_command.dart(adds_selfRegisterPlugin).plugin:install magicre-invocations no longer corruptlib/config/app.dart. The staticinstall/app_configpublish entry rendered the raw{{ allImports }}/{{ allProviders }}placeholders when invoked outsideMagicInstallCommand.handle(where the fluent override would overwrite with the dynamic providers list). Removedinstall/app_config: lib/config/app.dartfrominstall.yamlpublish:; the fluent override is now the sole writer. Touchesinstall.yaml.assets/lang/en.jsonis now scaffolded on install. Addsinstall/lang_en: assets/lang/en.jsontoinstall.yamlpublish:with a minimal stub coveringcommon.welcome,common.loading, …, and avalidation.*block matching the built-in rule names. Consumers usingLang.trans('common.welcome')now resolve out of the box; previously the lang dir was empty until the operator ranmake:lang. Touchesinstall.yaml, addsassets/stubs/install/lang_en.stub.
Fixed (PR #87 code review) #
- Cache hit/miss detection no longer misclassifies.
CacheManager.get()decided hit-vs-miss withvalue == defaultValue, which dispatched aCacheMisswhen the stored value happened to equal the caller'sdefaultValue, or when a storednullwas read with anulldefault. It now usesdriver().has(key)for presence. Toucheslib/src/cache/cache_manager.dart; adds two regression cases totest/cache/cache_manager_event_dispatch_test.dart. KeyGenerateCommandreuses a singleRandom.secure()instead of constructing one per byte. Toucheslib/src/cli/commands/key_generate_command.dart.- Removed the unused
yaml_editdependency frompubspec.yaml(nolib/,test/, orbin/references), trimming transitive deps and publish surface. - Example app shows a real title.
example/.envAPP_NAMEis now"Magic Example"(was"") andwelcome_view.dartfalls back to a non-emptyapp.name, so the example no longer renders a blank title. Touchesexample/.env,example/lib/resources/views/welcome_view.dart.
Improvements (UX) #
magic:installpost-install message documents the optional Dusk + Telescope setup chain. Removed the obsolete sqlite3.wasm warning (the install command auto-fetches sqlite3.wasm 3.3.1 since the artisan-install-command-magic plan). Added a setup recipe pointing operators at themagic_devtoolsdev_dependency (plusfluttersdk_dusk/fluttersdk_telescope) and thepackage:magic_devtools/{dusk,telescope}.dartadapter imports, so the debug-tooling path is discoverable without consulting the docs. Touchesinstall.yaml(post_install.message).
Changed #
- Documentation: CLAUDE.local.md updated to reflect artisan-based CLI. The stale
magic_clicompanion-project sync protocol (cross-repo stub sync, provider coupling) has been retired. Magic now owns its CLI and generators underlib/src/cli/on thefluttersdk_artisansubstrate. UpdatedCLAUDE.local.mdto document the current architecture (command locations, install manifest, stub loading) and deprecation of the legacy magic_cli sync procedure.
Deferred #
magic:install --with-debug-toolingsingle-command flag that chains the 6-step Dusk + Telescope setup recipe (currently the post_install message documents the recipe; the flag would auto-execute it). Tracking issue: TBD.MainDartSmartMergershould consolidate the 4if (kDebugMode) { ... }blocks thatdusk:install+telescope:installemit into 2 blocks (pre-Magic.init()host plugins + post-Magic.init()Magic adapters). Currently each install command writes its own block, producing four single-statement blocks. Tracking issue: TBD.
Changed (artisan-install-command-magic plan) #
magic:installnow delegates canonical Flutter scaffold to artisan'sinstallcommand in-process. AfterstagedInstaller.commit()returns Success,delegateArtisanInstallinvokesInstallCommand.scaffoldInto(from the artisan public barrel) to writebin/dispatcher.dart+ barrels + pubspec dep + bin/fsa. Gated inside the existingif (result is Success)block so dry-run / Conflict / Error results skip the delegation and atomic-commit semantics are preserved. Magic-specific extras (conditional configs, dynamiclib/config/app.dart,lib/main.dartsmart-merge, sqlite3.wasm) remain magic-side.
Removed (artisan-install-command-magic plan) #
install.yaml11th publish entry (install/consumer_artisan: bin/artisan.dart) dropped. Artisan'sinstallcommand now writes the canonical dispatcher tobin/dispatcher.dart; magic no longer ships a separate consumer wrapper. Magic-managed consumers reach the same canonical state via the delegation flow.
Added (dusk-magic-wind enrichment Wave 3 / Wave 4 wiring) #
MagicHttpFacadeAdapter.pendingCountoverride (Step 3.4 cross-package). Proxies to the file-private_TelescopeNetworkInterceptor._pending.length(null-guarded pre-install, returns 0). Reads the live in-flight FIFO soTelescopeStore.pendingHttpCountcan sum across registered adapters. Powers dusk'sext.dusk.wait_for_network_idleend-to-end.- Magic-side reader wiring for dusk's telescope-backed tools
(Steps 3.4 + 3.5).
MagicTelescopeIntegration.install()now also assigns three function-pointer readers exported frompackage:fluttersdk_dusk/dusk.dart:pendingHttpCountReader = () => TelescopeStore.pendingHttpCountrecentLogsReader = TelescopeStore.recentLogs(...) → dusk envelope(renamesloggerName→logger, ISO-formats timestamps)recentExceptionsReader = TelescopeStore.recentExceptions(...) → dusk envelope(renamesexceptionType→type, truncates stackTrace to first 3 lines asstackHead) The indirection lives on the dusk side; dusk has no hard dep on telescope. Magic is the only crossover point. Dusk hosts that do not shipfluttersdk_telescopeget the default empty-list readers (missing-telescope graceful path).
- New
test/cli/telescope_integration_test.dart(6 cases): pre-install null-guard, post-install zero, in-flight count, FIFO decrement, post-uninstall null-guard, end-to-end viaTelescopeStore.pendingHttpCount.
Changed (BREAKING for magic_cli legacy users; non-breaking via legacy fallback) #
-
magic:installrewrite to PluginInstaller DSL + install.yaml manifest. The command extendsArtisanInstallCommand(from fluttersdk_artisan ^1.0.0-alpha.1+) and delegates the install.yaml-expressible 60% toManifestInstaller. The conditional 40% (per-flag config emission, dynamiclib/main.dartconfigFactories list, dynamiclib/config/app.dartprovider list, app name extraction from pubspec.yaml) lives in a fluent override hook onManifestInstaller.prepare(). Existing--without-*flags map 1:1 to install.yamlprompts:(bool type, default false).Backward compat:
dart run :artisan magic:installcontinues to work via legacy fallback; the new canonical workflow isdart run :artisan plugin:install magic(auto-detects install.yaml, routes through ManifestInstaller in one step). -
REVERTED: First install on a fresh
flutter createapp NO LONGER requires--force.MagicInstallCommand._resolveMainDartStrategycallsMainDartScaffoldDetector.isFlutterCreateScaffoldBEFORE the ConflictDetector path; when the existinglib/main.dartmatches the flutter create scaffold heuristic,scaffoldDetected=trueflows intoPluginInstaller.commit(force: true)and bypasses the unmanaged-file check silently. Operators now rundart run magic:artisan magic:installon a freshflutter createapp without any flag; customizedlib/main.dartstill requires--forceor--preserveexplicitly. (CHANGELOG entry from an earlier alpha was stale; the scaffold detector landed before alpha-15 but the entry was not removed.) -
sqlite3.wasmauto-download wired intomagic:install. When the database feature is enabled (no--without-databaseflag) and the run is not a dry-run,MagicInstallCommandnow fetches the matchingsqlite3.wasmfromsimolus3/sqlite3.dart(pinned to 3.3.1) and writes it toweb/sqlite3.wasmafter the install commits. Closes the white-screen /WebAssembly TypeErrorfailure mode that hit fresh Flutter web targets on first run.
✨ New Features #
-
Dusk enricher expansion (7 new enrichers + 1 extension):
magicControllerFlagsEnricher- captures FutureOr status, loading/success/error flags fromMagicStateMixinmagicRouteParamsEnricher- emits route parameters (path params + query string)magicFormErrorsEnricher(extension) - now quotes per-field error messages to preserve whitespacemagicEchoConnectionEnricher- reports broadcast connection state (connecting/connected/disconnected/reconnecting)magicGateResultsAllEnricher- emits last N gate check results (ability: allowed/denied) from MRU cachemagicRecentHttpEnricher- emits last 5 HTTP requests (method, URL, status, elapsed time)magicRecentLogsEnricher- emits last 5 log entries (level, message, timestamp)magicRecentExceptionsEnricher- emits last 5 exceptions (type, message, stack trace truncated to 500 chars)
All new enrichers guard
kDebugModeand handle missing dependencies gracefully (telescope-not-installed returns null buffer). Registered byMagicDuskIntegration.install(). Combined with existing 7 enrichers (magicControllerState,magicFormErrors,magicGateResult,magicMiddleware,magicAuthUser,magicFormField,magicRoute), magic-side surface now totals 14 enrichers. Ships in coordinated bump with fluttersdk_dusk 1.0.0-alpha.3+. -
Dusk integration: 5 new snapshot enrichers (
magicControllerState,magicFormErrors,magicGateResult,magicMiddleware,magicAuthUser) registered byMagicDuskIntegration.install()for richer LLM-agent E2E context. Combined with the 2 alpha-1 enrichers (magicFormField,magicRoute) this brings the magic-side surface to 7 enrichers; with Wind's 6-fieldWindClassNameEnricherthe total enricher surface is 8. Ships in coordinated bump with fluttersdk_dusk 1.0.0-alpha.2 (seereferences/fluttersdk_dusk/CHANGELOG.mdfor the matching dusk-side contract additions: 7 new handlers, 10 new MCP descriptors, 8 new CLI commands, actionability gate,dusk_findLocator pattern, Chrome reaper,dusk:doctor). Requires fluttersdk_dusk ^1.0.0-alpha.2 — theDuskSnapshotEnrichertypedef is frozen across both repos for the alpha-2 cycle. -
Cache events:
CacheHit,CacheMiss,CachePut,CacheForget,CacheFlushevent classes added underlib/src/cache/events/cache_events.dartand exported frompackage:magic/magic.dart.CacheManager.get/put/forget/flushnow dispatch the matching event throughEventDispatcher.instanceafter the underlying store operation completes. Enablesfluttersdk_telescope'sMagicCacheWatcher(and any user-defined listener) to observe the full cache lifecycle. -
Test coverage: new
MagicInstallCommandexercised by 27 tests using InstallContext.test + InMemoryFs + FakePromptDriver + FakeStubDriver injection; one test per--without-Xflag plus first-install--force- app name extraction edge cases. Coverage: 76.5% (defensive error paths not covered; accepted per Risks Accepted in the migration plan).
🔧 Improvements #
- Routing:
MagicRouter.currentRoutepublic getter for the currently-resolved RouteDefinition. - Auth:
GateManager.lastResult(ability)accessor backed by an MRU cache (64 entries) of the most recent gate-check outcome per ability.
✨ New Features #
- Eloquent:
Model.fillnow accepts astrictflag. Whentrue, any non-fillable key throwsMassAssignmentExceptioninstead of being silently dropped. Pair with validated request payloads to catch schema drift at the boundary. (#69) - Validation:
FormRequest— Laravel-style request object that collapses authorize → prepare → validate into a single class. ThrowsAuthorizationExceptionon denied access andValidationExceptionwith a field-keyed error map on rule failure. Pairs withModel.fill(validated, strict: true). (#66) - HTTP:
MagicController.authorize(ability, [arguments]), a Laravel-style controller helper that delegates toGate.allows()and throwsAuthorizationExceptionon denial. Avoids hand-rolling gate checks in every action. (#72) - Auth:
Gate.allowsAny(abilities, [arguments])andGate.allowsAll(abilities, [arguments]), short-circuiting sugar for checking multiple abilities at once. (#72) - Routing:
MagicRoute.resource(name, controller, {only, except})auto-wires up to four canonical routes (index, create, show, edit) to a controller that mixes inResourceController. Controllers declare supported methods viaresourceMethods;only/exceptnarrow the set further. Each route gets an auto-assigned{slug}.{method}name and title. (#67) - Validation:
AsyncRulecontract plusUnique(endpoint, field: ...)rule, an async uniqueness check with per-instance debounce (coalesces rapid calls) and a pluggable.via()resolver. Network errors log and pass so they never block submission.Validator.validateAsync()runs async rules after sync rules; sync failures short-circuit per field. (#68) - Session: Add
Sessionfacade with Laravel-style flash data —Session.flash(data),Session.flashErrors(errors),Session.old(field, [fallback]),Session.error(field),Session.errors(field),Session.hasError(field),Session.hasFlash,Session.tick(). Two-bucket store promotes flashed data exactly one navigation hop so forms can repopulate after a failed submit. Top-level helpersold()anderror()mirror Laravel's Blade API - UI:
MagicFormData.validate()automatically flashes form data on validation failure — downstream views can repopulate viaold('field')without manual wiring - Validation:
In<T>rule accepts a primitive whitelist (strings, ints, etc.) andInList<T extends Enum>validates enum-backed fields, accepting either the enum instance or a wire string.InListsupportscaseInsensitive:and an optionalwire:mapper for snake_case or custom representations. Both emit the sharedvalidation.inmessage with a comma-joined:valuesparameter. (#81)
1.0.0-alpha.13 - 2026-04-16 #
✨ New Features #
- Routing: Add
currentPathgetter toMagicRouter— returns the current route path without query string, complementing the existingcurrentLocationproperty
🐛 Bug Fixes #
- Routing: Use
GoRouter.pop()instead ofNavigator.pop()inback()— syncs router state and preserves custom page transitions on reverse animation. AddStateErrorguard when router is not initialized, consistent withto()andreplace()
🔧 Improvements #
- Skill: Optimize
magic-frameworkskill for Claude Code progressive disclosure — split frontmatter, extract templates to references, compress sections (669 → 416 lines). Add version frontmatter and source-to-skill mapping in release command - Deps: Bump magic version constraint in example app
1.0.0-alpha.12 - 2026-04-09 #
✨ New Features #
- Broadcasting: Client-side activity monitor — detects silent connection loss using Pusher protocol
activity_timeoutandpusher:ping/pusher:pong. Automatically reconnects when the server stops responding - Broadcasting: Random jitter (up to 30%) on reconnection backoff delay — prevents thundering herd when many clients reconnect simultaneously after a server restart
- Broadcasting: Configurable connection establishment timeout (default 15s) — prevents indefinite hang when server doesn't complete the Pusher handshake. Automatically triggers reconnect on timeout
1.0.0-alpha.11 - 2026-04-07 #
🐛 Bug Fixes #
- Routing: Fix intermittent page title loss on web — Flutter's
Titlewidget was overwriting TitleManager's route-level title ondidChangeDependencies()rebuilds. UseonGenerateTitleto keep both in sync
⚠️ Breaking Changes #
- file_picker: Upgrade from
^10.3.10to^11.0.2— migrates to static API (FilePicker.platformremoved). Consumers usingFilePicker.platformdirectly (viamagic.dartre-export) must switch to static calls (FilePicker.pickFiles(),FilePicker.getDirectoryPath(),FilePicker.saveFile()). Includes Android path traversal security fix (CWE-22) and WASM web support
1.0.0-alpha.10 - 2026-04-07 #
✨ New Features #
- Routing: Route-level page title management with
TitleManagersingleton. Per-route titles viaRouteDefinition.title(), automatic suffix pattern viaMagicApplication(titleSuffix:), declarativeMagicTitlewidget for data-dependent titles, and imperativeMagicRoute.setTitle()/MagicRoute.currentTitleAPI. Title resolution: MagicTitle > setTitle > RouteDefinition.title > MagicApplication.title. (#49)
🔧 Improvements #
- Dependencies: Bump
magic_clito^0.0.1-alpha.6(scaffold templates now include.title()andtitleSuffix)
1.0.0-alpha.9 - 2026-04-07 #
🐛 Bug Fixes #
- Broadcasting: Auth failures in private/presence channels now surface via
Log.error()and interceptoronError()chain instead of being silently swallowed. Reconnect resubscribes all channels withawait—onReconnectstream emits only after completion. Per-channel error handling ensures one auth failure does not block other channels. (#45) - Database:
sqlite3.wasmnow loads via absolute URI (/sqlite3.wasm) instead of relative — fixes 404s on deep routes when using path URL strategy. (#46)
1.0.0-alpha.8 - 2026-04-07 #
✨ Features #
- feat: config-driven path URL strategy for Flutter web (#40)
1.0.0-alpha.7 - 2026-04-06 #
✨ Features #
- Broadcasting:
Echofacade,BroadcastManager,ReverbBroadcastDriver(Pusher-compatible WebSocket with reconnection, dedup, heartbeat),NullBroadcastDriver,BroadcastInterceptorpipeline,FakeBroadcastManager,BroadcastServiceProvider. Laravel Echo equivalent for real-time channels. (#38) - Router Observers:
MagicRouter.instance.addObserver()enables NavigatorObserver integration for analytics/monitoring (Sentry, Firebase Analytics, custom observers). Observers are passed to GoRouter automatically. (#34) - Network Driver Plugin Hook:
DioNetworkDriver.configureDriver()exposes the underlying Dio instance for SDK integrations (sentry_dio, certificate pinning, custom adapters). (#35) - Custom Log Drivers:
LogManager.extend()enables custom LoggerDriver registration (Sentry, file, Slack). Config-driven resolution with built-in override support. (#36)
1.0.0-alpha.6 - 2026-04-05 #
✨ Features #
- Http Faking:
Http.fake()enables Laravel-style HTTP faking for testing. Swap the real network driver with aFakeNetworkDriverthat records requests and returns stubbed responses. Supports URL pattern stubs, callback stubs, and assertion methods (assertSent,assertNotSent,assertNothingSent,assertSentCount). (#18) - Facade Faking:
Auth.fake(),Cache.fake(),Vault.fake(),Log.fake()— Laravel-style facade faking for testing. Swap real service implementations with in-memory fakes that record operations and expose assertion helpers. (#19) - Fetch Helpers:
fetchList()/fetchOne()onMagicStateMixin— auto state management for HTTP fetches with defensive type guards against malformed responses (#20) - MagicTest:
MagicTest.init()/MagicTest.boot()— standardized test bootstrap helper,package:magic/testing.dartbarrel export (#21)
🐛 Bug Fixes #
- Log.channel(): Now returns
LoggerDrivervia_manager.driver(name)instead ofLogManager, enablingLog.channel('slack').error(...)as documented (#27) - Http.response() null data: Sentinel pattern allows
Http.response(null, 204)for No Content stubs whileHttp.response()still returns mutable empty map (#26) - URL pattern escaping:
FakeNetworkDriverstub patterns now escape regex metacharacters (.,?,+) viaRegExp.escape()— only*is treated as wildcard (#26) - fetchList/fetchOne defensive guards: Type-check
response.dataasMapbefore indexing, filter non-Mapelements in lists viawhereType<Map>(), guardfetchOnedata cast (#28)
1.0.0-alpha.5 - 2026-03-29 #
🐛 Bug Fixes #
- Route Back Navigation:
MagicRoute.back()now works aftergo()-based navigation (cross-shell). Maintains lightweight history stack with automatic fallback. Optionalfallbackparameter for explicit control. (#11)
1.0.0-alpha.4 - 2026-03-29 #
🔧 Improvements #
- Localization Hot Restart: Translation JSON changes now reflect on hot restart during development. Uses fetch with cache-busting on web and best-effort disk reads on desktop, bypassing Flutter's asset bundle cache. Zero impact on release builds.
1.0.0-alpha.3 - 2026-03-24 #
1.0.0-alpha.2 - 2026-03-24 #
⚠️ Breaking Changes #
- Pub.dev Migration: Replaced git submodule path dependencies with pub.dev hosted packages (
fluttersdk_wind: ^1.0.0-alpha.4,magic_cli: ^0.0.1-alpha.3). Removedplugins/directory entirely. - SDK Bump: Dart
>=3.11.0 <4.0.0, Flutter>=3.41.0(previously Dart >=3.4.0, Flutter >=3.22.0)
✨ New Features #
- Launch Facade: URL, email, phone, and SMS launching via
url_launcherwithLaunch.url(),Launch.email(),Launch.phone(),Launch.sms() - Form Processing:
process(),isProcessing, andprocessingListenableonMagicFormDatafor form-scoped loading state - Reactive Auth State:
stateNotifieron Guard contract and BaseGuard for reactive auth state UI - Query Parameters:
Request.query(),Request.queryAll,MagicRouter.queryParameter()for URL query parameter access - Localization Interceptor: Automatic
Accept-LanguageandX-Timezoneheaders on HTTP requests - Theme Persistence: Auto-persist dark/light theme preference via Vault in
MagicApplication - Validation Helpers:
clearErrors()andclearFieldError()onValidatesRequestsmixin - Route Names: Route name registration on
RouteDefinition
🐛 Bug Fixes #
- Auth Config: Default config now properly wrapped under
'auth'key - Session Restore: Guards against missing
userFactory— gracefully skips instead of throwing - Barrel Export:
FileStoreexported from barrel file - Package Name: Renamed internal references from
fluttersdk_magictomagic
🔧 Improvements #
- Dependency Upgrades: go_router ^17.1.0, sqlite3 ^3.2.0, share_plus ^12.0.1, file_picker ^10.3.10, flutter_lints ^6.0.0, and more
- CLI Docs: Rewrote Magic CLI documentation with all 16 commands and
dart run magic:magicsyntax - Wind UI Docs: Moved to wind.fluttersdk.com, removed local copy
- Example App: Rebuilt with fresh
flutter createandmagic install - CI Pipeline: Upgraded GitHub Actions, added validate gate to publish workflow
- Claude Code: Added path-scoped
.claude/rules/for 8 domains, auto-format and auto-analyze hooks
1.0.0-alpha.1 - 2026-02-05 #
✨ Core Features #
- Laravel-inspired MVC architecture
- Eloquent-style ORM with relationships
- GoRouter-based routing with middleware support
- Service Provider pattern
- Facade pattern for global access
- Policy-based authorization
📦 Package Structure #
- Complete model system with HasTimestamps, InteractsWithPersistence
- HTTP client with interceptors
- Form validation system
- Event/Listener system
🔧 Developer Experience #
- Magic CLI integration
- Hot reload support
- AI agent documentation