akyos/ux-native-cli
Bundle Symfony exposant des commandes console pour générer des coques Hotwire Native minimales (Android et iOS) depuis des modèles, puis lancer Gradle et xcodebuild depuis la racine d’un projet PHP. Configuration via native.yaml (URL, application_id, bundle_id, chemins).
Install the CLI and the Hotwire Native runtime:
composer require akyos/ux-native-cli symfony/ux-native
The CLI ships as a Symfony bundle (Akyos\UxNativeCliBundle\AkyosUxNativeCliBundle), so it runs through a bin/console binary rather than php artisan. In a Laravel-only repo, keep a thin tooling console (or a small sibling Symfony app) that points at your Laravel server URL — the app it wraps is still your Laravel app.
Create the config file (native.yaml) in the directory the console runs from:
native:
app_name: "MyApp"
url: "http://192.168.1.20:8000" # LAN IP, not 127.0.0.1, if you test on a device
application_id: "com.example.myapp"
bundle_id: "com.example.myapp"
ios_development_team: "ABCDE12345" # required for real iOS devices
icon: "resources/images/app-icon.png" # new in 0.5.0 — generates iOS + Android icons
As of 0.5.0 the CLI also drives the top bar (via top_bar in path-configuration rules) and the app icon from this file.
Generate the native apps:
php bin/console native:init
This scaffolds Hotwire Native iOS (native/ios) and Android (native/android).
Add bridges you didn't enable at init (new in 0.5.0 — no need to re-init):
php bin/console native:update:config --notification --barcode-scanner --admob
native:update:config reinjects native.yaml values (URL, name, icon, top bar) between markers and adds any missing bridge wiring, leaving the rest of the generated code untouched.
First build and install:
php bin/console native:build --platform=android --install
php bin/console native:build --platform=ios --install
native:build now runs the config update first, so native.yaml edits are picked up automatically on every build.
Where to look first: native.yaml (single source of truth), then native/ios / native/android for the generated shells, then your Laravel routes — the shell renders whatever your app serves at native.url.
Point the shell at php artisan serve (port 8000 is the default native.yaml expects) and confirm your existing Blade/Turbo pages render inside the native shell. Drop a 1024×1024 PNG at the icon: path, rebuild, and confirm the launcher icon changed — that's the fastest sanity check that the 0.5.0 config injection is actually running.
php artisan serve --host=0.0.0.0 (bind to all interfaces so a physical device can reach it).native.yaml (URL, name, icon, top bar), then native:build. Use native:update:config alone when you only want to sync the generated projects without compiling.php bin/console native:qr-install --platform=android --build
native.yaml in Sync (0.5.0)native:update:config writes between markers inside the generated native projects. Treat everything between those markers as generated code.# pulled a teammate's URL/top-bar change — apply without rebuilding
php bin/console native:update:config
# add a bridge to an existing app after the fact
php bin/console native:update:config --barcode-scanner
# normal build already does the update step
php bin/console native:build --platform=android --install
native.yaml files and let native:build re-inject the URL — a staging build can't accidentally ship production URLs if the file is right.native:init --notification, or native:update:config --notification on an existing project. This wires Firebase (Android) and APNs (iOS) and installs the notification-token JS bridge.document.addEventListener('notification-token', (e) =>
fetch('/api/device-tokens', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf },
body: JSON.stringify({ token: e.detail.token, platform: e.detail.platform ?? 'unknown' })
})
);
Store per user + device in a device_tokens table (unique on token), with platform, last_seen_at.NotificationTrait: 0.5.0 ships a Symfony UX Live Component trait. In Laravel, port the same behaviour into a Livewire trait or a queued notification channel — don't try to use the Symfony trait directly.composer require akyos/native-push
Use its server-side API inside a queued job / notification channel so a slow FCM/APNs call never blocks a request.native.url route set — no auth wall a cold start can't clear.native:update:config --barcode-scanner; it ships a Stimulus controller plus a ScanQRTrait for Symfony Live Components.native:update:config --admob adds the ads bridge to the generated projects. Ad unit IDs are per-app/per-platform; keep them out of native.yaml if you don't want them committed, and inject per environment.Map URL patterns to native presentation instead of building separate screens:
{
"settings": { "screenshots_enabled": true },
"rules": [
{ "patterns": ["/home$"], "properties": { "context": "default", "uri": "hotwire://fragment/web/home" } },
{ "patterns": ["/new$", "/edit$"], "properties": { "context": "modal", "top_bar": false } },
{ "patterns": ["/account$"], "properties": { "context": "default", "pull_to_refresh_enabled": false } }
]
}
top_bar is new in 0.5.0 and applies on both iOS and Android. It is evaluated per rule, so a global "hide the bar" means adding it to every rule that matches, not one top-level setting./native/path-configuration.json) and point the shell at it.ios_development_team in native.yaml; builds pass -allowProvisioningUpdates so Xcode can create/refresh profiles without opening the GUI.php bin/console native:build --platform=android --no-install
Outputs: native/android/app/build/outputs/apk/debug/app-debug.apk and native/ios/build/DerivedDataCli/Build/Products/Debug-iphoneos/MyApp.app.--ios-destination="simulator:UDID".native:
android_gradle_task: "assembleRelease"
native.icon and will fail or produce stale assets if the path is missing on the runner.config/bundles.php and won't auto-register the bundle. Run the commands from a dedicated tooling console, or vendor the command classes into app/Console/Commands and adapt them — the artifacts it produces (native/) are framework-agnostic.@akyos/ux-native-cli instead of being copied into your project. Laravel uses Vite, not importmap. You must either add the vendor path to Vite's resolve/alias config or copy the controllers into resources/js/controllers and register them yourself. Symptom of missing this: notification-token / scanner / ads bridges never fire.NotificationTrait / ScanQRTrait are Symfony UX Live Component traits. There is no Laravel equivalent — reimplement them as Livewire traits or plain services. Composer will install them happily; they just won't do anything in a Livewire component.native/. native:update:config — and therefore native:build, which calls it — cannot find its anchors without them. Hand-edits inside the marked regions are overwritten on every build; put customisations outside.native:init --force still wipes hand-edits. Use native:update:config for config/bridge changes; reserve --force for genuine re-scaffolds.127.0.0.1 is not reachable from a device. Always use the LAN IP or a tunnel.usesCleartextTraffic, iOS needs an ATS exception for http:// dev URLs. Prefer HTTPS or local tunnels before editing manifests.GoogleService-Info.plist matching bundle_id.POST_NOTIFICATIONS must be requested; a token can be issued while notifications are silently dropped.ios_development_team, or a bundle ID that doesn't exist in the Apple portal.iOS ... is not installed: install the runtime via Xcode → Settings → Components.No devices/simulators found:
xcrun simctl boot "iPhone 15"
9876; open it in your firewall. Android will still ask for "Unknown Sources".php bin/console native:build -vv.php bin/console native:init --app-name="Test" --dry-run.adb logcat | grep -i hotwire for Android; Xcode console / xcrun simctl spawn booted log stream for iOS.target=_blank).cd native/android && ./gradlew assembleDebug
xcodebuild -project NativeApp.xcodeproj -scheme NativeApp \
-destination 'platform=iOS Simulator,name=iPhone 15'
xcrun simctl list devices booted -j.vendor/akyos/ux-native-cli/Resources/templates into config/native/templates and edit there — keeps upgrades clean.native:build and then adds signing/upload steps:
$this->getApplication()->find('native:build')->run($input, $output);
// then: sign, upload to S3, notify Slack
.env.local / CI secrets): ANDROID_HOME, JAVA_HOME.resources/js, alias the vendor path in vite.config.js so @akyos/ux-native-cli resolves from vendor/ — you get upgrades for free and stay aligned with the Asset Mapper layout.{project}/native/android and {project}/native/ios; override with android_path / ios_path.JAVA_HOME — set java_home in native.yaml or export it; AGP versions in generated projects pin to specific JDK majors (17 is the safe default).android_home: set it explicitly if adb: command not found:
native:
android_home: "/Users/you/Library/Android/sdk"
native.yaml is the single source of truth, and since 0.5.0 native:build re-injects it automatically — config drift between environments is a file-diff problem, not a code problem. Keep it version-controlled and override secrets via env.top_bar lives in path-configuration rules, not native.yaml. If the bar won't hide, confirm the URL actually matches the rule you edited.Assets.xcassets / mipmap files by hand will be overwritten.bundle_id and application_id are baked into the generated projects; changing them later requires re-running native:init --force or editing the native projects by hand.How can I help you explore Laravel packages today?