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
169
170
171
172
173
174
175
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.
/// Without `--apply` nothing the build reads changes: merges and new files are offered as `<file>.mobiler-new`
/// (Android resources under `.mobiler/new/`) and the dep bump is reported. `--apply` writes them in place (saving
/// the old file under `.mobiler/backup/`) and bumps the dep. Every run lists what still waits for review
/// (`.mobiler/pending/`). Never touches your Rust app code; never applies a conflict.
Upgrade {
/// Overwrite changed shell files in place instead of writing `.mobiler-new` (the
/// old version of each is saved under `.mobiler/backup/` first).
#[arg(long)]
apply: bool,
/// Mark a file waiting for review as dealt with: a conflict you resolved, or an offered change to a
/// file you have that you decided not to take (your version stays). Its review stops and the next upgrade merges only
/// newer changes. Repeatable; checked before any is applied.
#[arg(long, value_name = "FILE")]
resolved: Vec<String>,
},
}
#[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, resolved } => match upgrade::run(apply, &resolved) {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e:#}");
std::process::ExitCode::FAILURE
}
},
}
}