1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
use clap::{Parser, Subcommand};
mod build;
mod dev;
mod fonts;
mod display_name;
mod doctor;
mod new;
mod plugin;
mod templating;
mod upgrade;
mod watch;
#[derive(Parser)]
#[command(name = "mobiler", version, about = "Rust + Compose mobile framework CLI")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Check that this host has everything needed to build Mobiler apps.
Doctor,
/// Scaffold a new Mobiler project.
New {
/// Project name (becomes the directory and Gradle root name).
name: String,
/// Android application/package id. Defaults to `dev.mobiler.<name>`.
#[arg(long)]
package: Option<String>,
/// Write a `CLAUDE.md` agent guide so a coding agent (e.g. Claude Code) builds
/// idiomatically. Bare `--agentic` = the generic primer; or pick a flavor:
/// `shared-ui` (same UI on mobile + web) or `api` (reusable core + JSON API).
#[arg(long, value_enum, num_args = 0..=1, default_missing_value = "generic")]
agentic: Option<new::AgenticGuide>,
/// The name users see (launcher label, system dialogs), e.g. "Appointments Admin".
/// Defaults to the PascalCase project name. Change it later with `mobiler display-name`.
#[arg(long)]
display_name: Option<String>,
},
/// Build the Mobiler project, install, and launch (default: all three).
Dev {
/// Build only — don't install on a device.
#[arg(long)]
no_install: bool,
/// Build (and install if applicable) but don't launch the activity.
#[arg(long)]
no_run: bool,
/// Which connected device to install on (adb serial). Defaults to ANDROID_SERIAL, then the
/// only connected device; with several connected, the error lists them.
#[arg(long, value_name = "SERIAL")]
device: Option<String>,
},
/// Watch shared/ and Android/ for changes; rebuild + reinstall + relaunch on each change.
Watch {
/// Build only on each change — don't install on a device.
#[arg(long)]
no_install: bool,
/// Build (and install if applicable) but don't launch the activity.
#[arg(long)]
no_run: bool,
/// Which connected device to install on (adb serial). Defaults to ANDROID_SERIAL, then the
/// only connected device; with several connected, the error lists them.
#[arg(long, value_name = "SERIAL")]
device: Option<String>,
},
/// Build the native artifact only (no install/launch) — the cloud-buildable unit.
Build {
/// Target platform.
#[arg(value_enum, default_value = "android")]
platform: build::Platform,
},
/// Custom fonts from `mobiler.toml` `[fonts]` (synced automatically by build/dev/watch).
Fonts {
#[command(subcommand)]
cmd: FontsCmd,
},
/// Manage plugins (add a capability package to this app).
Plugin {
#[command(subcommand)]
cmd: plugin::PluginCmd,
},
/// Show or set the user-visible app name (Android app_name + iOS CFBundleDisplayName).
/// Project identifiers (Gradle root, Xcode target, theme) are left unchanged.
DisplayName {
/// The new display name. Omit to print the current one.
name: Option<String>,
},
/// Update this app's generic native shells + `mobiler-core` dep to the CLI's templates.
/// Non-destructive by default (writes `<file>.mobiler-new`); `--apply` overwrites in place
/// (saving `<file>.mobiler-bak`). Never touches your Rust app code or plugin-patched files.
Upgrade {
/// Overwrite changed shell files in place instead of writing `.mobiler-new` (a
/// `.mobiler-bak` of each is saved first).
#[arg(long)]
apply: bool,
},
}
#[derive(Subcommand)]
enum FontsCmd {
/// Copy the `[fonts]` files into the Android, iOS and web shells (idempotent).
Sync,
}
fn main() -> std::process::ExitCode {
let cli = Cli::parse();
match cli.command {
Command::Doctor => doctor::run(),
Command::New { name, package, agentic, display_name } => match new::run(&name, package.as_deref(), agentic, display_name.as_deref()) {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e:#}");
std::process::ExitCode::FAILURE
}
},
Command::Dev { no_install, no_run, device } => match dev::run(no_install, no_run, device.as_deref()) {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e:#}");
std::process::ExitCode::FAILURE
}
},
Command::Watch { no_install, no_run, device } => match watch::run(no_install, no_run, device.as_deref()) {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e:#}");
std::process::ExitCode::FAILURE
}
},
Command::Build { platform } => match build::run(platform) {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e:#}");
std::process::ExitCode::FAILURE
}
},
Command::Fonts { cmd: FontsCmd::Sync } => match fonts::run_sync_cli() {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e:#}");
std::process::ExitCode::FAILURE
}
},
Command::Plugin { cmd } => match plugin::run(cmd) {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e:#}");
std::process::ExitCode::FAILURE
}
},
Command::DisplayName { name } => match display_name::run(name.as_deref()) {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e:#}");
std::process::ExitCode::FAILURE
}
},
Command::Upgrade { apply } => match upgrade::run(apply) {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e:#}");
std::process::ExitCode::FAILURE
}
},
}
}