Magic Deeplink CLI
On this page
Introduction#
Magic Deeplink ships a CLI that scaffolds deep link configuration into your project and generates the server-side verification files required by iOS Universal Links and Android App Links.
All commands are run via Dart's run mechanism:
dart run <app>:artisan deeplink:<command> [options]
Commands#
install#
Scaffolds the deep link configuration file into the host project and wires it into the Magic app bootstrap automatically.
dart run <app>:artisan deeplink:install
dart run <app>:artisan deeplink:install --force
What it does
- Validates that Magic Framework is installed by checking for
lib/config/app.dart. If the file is absent it exits with an error:Magic Framework not detected. Run 'magic install' first. - Writes
lib/config/deeplink.dartfrom the built-in stub. Skips the write when the file already exists unless--forceis passed. - Injects into
lib/config/app.dart:- Import:
import 'package:magic_deeplink/magic_deeplink.dart'; - Provider registration:
(app) => DeeplinkServiceProvider(app),
- Import:
- Injects into
lib/main.dart(when present):- Import:
import 'config/deeplink.dart'; - Config factory:
() => deeplinkConfig,
- Import:
All injections are idempotent — running the command twice does not duplicate entries.
Options
| Flag | Abbr | Type | Default | Description |
|---|---|---|---|---|
--force |
-f |
bool | false |
Overwrite lib/config/deeplink.dart even if it already exists. |
Output files
| File | Action |
|---|---|
lib/config/deeplink.dart |
Created (or overwritten with --force) |
lib/config/app.dart |
Import + DeeplinkServiceProvider injected |
lib/main.dart |
Import + deeplinkConfig factory injected |
generate#
Generates the server-side deep link verification files: apple-app-site-association for iOS Universal Links and assetlinks.json for Android App Links.
dart run <app>:artisan deeplink:generate \
--team-id ABCDE12345 \
--bundle-id com.example.app \
--package-name com.example.app \
--sha256-fingerprints "AA:BB:CC:DD:..." \
--output public
Both files can be generated in one invocation, or individually by omitting the flags for the platform you don't need.
Options
| Option | Abbr | Type | Default | Description |
|---|---|---|---|---|
--output |
-o |
string | public |
Output directory for generated files, relative to the project root. |
--root |
— | string | . |
Override the project root directory. Defaults to the current working directory. |
--team-id |
— | string | — | Apple Developer Team ID. Required to generate apple-app-site-association. |
--bundle-id |
— | string | — | iOS app bundle identifier. Required to generate apple-app-site-association. |
--package-name |
— | string | — | Android package name. Required to generate assetlinks.json. |
--sha256-fingerprints |
— | list | — | One or more SHA-256 certificate fingerprints. Pass the flag multiple times or comma-separate values. Required to generate assetlinks.json. |
--paths |
— | list | ['/*'] |
Universal Link and App Link paths to register. Defaults to all paths (/*). |
Output files
| File | Platform | Required flags |
|---|---|---|
<output>/apple-app-site-association |
iOS Universal Links | --team-id, --bundle-id |
<output>/assetlinks.json |
Android App Links | --package-name, --sha256-fingerprints |
If the required flags for a platform are absent the command skips that file with a warning rather than failing, allowing you to generate only one platform's file at a time.
Generated file structure
apple-app-site-association:
{
"applinks": {
"details": [
{
"appIDs": ["<team-id>.<bundle-id>"],
"components": [
{ "/": "/*", "comment": "Matches any URL whose path matches /*" }
]
}
]
}
}
assetlinks.json (one entry per fingerprint):
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": ["AA:BB:CC:DD:..."]
}
}
]
doctor#
Checks whether a deep link install actually works, before a device is ever involved.
Manifest-driven install can publish the config file and inject a provider, but it cannot place an <intent-filter> inside a specific <activity> or tell you a <meta-data> landed on the wrong element. Every way of getting that hand-written half of the setup wrong is silent: a deep link simply opens the browser, with no exception and no log line anywhere.
dart run <app>:artisan deeplink:doctor
dart run <app>:artisan deeplink:doctor --verbose
dart run <app>:artisan deeplink:doctor --remote
What it checks
Local checks, always run:
lib/config/deeplink.dartexists and parses (reusing the same parsergenerateuses), and none ofdomain,team_id,bundle_id,package_name,sha256_fingerprintsare still a scaffold placeholder (example.com,YOUR_TEAM_ID,com.example.app,YOUR_SHA256_FINGERPRINT).- iOS:
Runner.entitlementscarriescom.apple.developer.associated-domains, and itsapplinks:host equalsdeeplink.domain. - iOS:
FlutterDeepLinkingEnabledisfalseinInfo.plist. Flutter has handled deep links itself by default since 3.27, and when it does, this plugin's handler chain never sees the link. - Android: an
<intent-filter android:autoVerify="true">inside the activity carriesVIEW,DEFAULT,BROWSABLE,<data>elements for bothhttpandhttps, and a host equal todeeplink.domain. - Android:
flutter_deeplinking_enabledisfalseand sits inside<activity>, not<application>. This is parsed off the manifest's element tree rather than searched for as text — a<meta-data>in the wrong element greps identically to one in the right one and is inert. - The generated association files exist under
web/.well-known/orpublic/.well-known/, and their contents agree with the config: the AASA'sappIDsentry equals<team_id>.<bundle_id>, andassetlinks.json'spackage_nameand fingerprints match. A legacyappID+pathsAASA entry mixed with a modernappIDs+componentsone in the same file is a warning, not a failure (Apple's TN3155 says mixing formats "may result in unexpected behavior").
Remote check, only with --remote:
GET https://<domain>/.well-known/apple-app-site-associationand.../assetlinks.json— both must answer 200 with no redirect and a body that parses as JSON. A non-application/jsoncontent-type on the AASA is a warning.
What it cannot prove
No local or remote check can prove a device actually matches a link: swcutil verify needs root and Android verifies at install time. The report says so explicitly rather than letting a green run imply it. Verify the last mile on a real device.
Options
| Flag | Type | Default | Description |
|---|---|---|---|
--verbose |
bool | false |
Show per-issue detail under each section. |
--remote |
bool | false |
Also fetch both association files from the live domain. Never runs by default — a doctor that hangs on DNS is a doctor nobody runs. |
Exit codes
| Code | Meaning |
|---|---|
0 |
Every check passed (or only warnings were raised). |
1 |
At least one check failed. |
Config Merge Strategy#
The generate command reads lib/config/deeplink.dart automatically when the file exists. Values from the config file act as defaults — any flag passed on the command line takes precedence.
| Source | Priority |
|---|---|
| CLI flag | High — always wins |
lib/config/deeplink.dart |
Low — used when the corresponding CLI flag is absent or empty |
Config keys read from lib/config/deeplink.dart:
| Config key | Corresponding CLI flag |
|---|---|
team_id |
--team-id |
bundle_id |
--bundle-id |
package_name |
--package-name |
sha256_fingerprints |
--sha256-fingerprints |
paths |
--paths |
The --paths flag is special: its CLI default is ['/*']. The config file value only overrides this default when the CLI flag was not explicitly supplied and the config contains a non-empty paths list. Passing --paths on the command line always wins, even if the config file defines paths.
This means you can commit all platform identifiers in lib/config/deeplink.dart and run generate with no flags during CI, while still being able to override individual values ad-hoc.