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
//! Offline Protocol developer CLI.
mod api;
mod auth;
mod commands;
mod config;
mod output;
mod setup;
use std::future::Future;
use clap::{CommandFactory, Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "offline",
version,
about = "Build offline-first apps on the Offline Protocol ecosystem",
long_about = "The Offline Protocol developer CLI: authenticate with the developer \
portal, scaffold production-ready apps from ecosystem knowledge, and serve \
that knowledge to AI agents over MCP.\n\nAgent-friendly: every informational \
command supports --json, and `offline mcp serve` exposes the full registry \
as MCP tools/resources."
)]
struct Cli {
/// Organization to use for this command (id or name), overriding the default from `offline org use`
#[arg(long, global = true, value_name = "ORG")]
org: Option<String>,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Authenticate with the Offline Protocol developer portal
Login(commands::login::LoginArgs),
/// Revoke the stored API key and clear local credentials
Logout(commands::login::LogoutArgs),
/// Show the currently authenticated developer
Whoami(commands::login::WhoamiArgs),
/// Provision org, application, and default API key (needs a developer session)
///
/// Needs the developer session `offline login` stores. An app key from
/// `offline login --api-key` (CI) can't run it. Reuses the API key this
/// machine already created for the application.
Setup(commands::setup::SetupArgs),
/// Manage organizations: list, create, and `use` one as the default (needs a developer session)
///
/// Needs the developer session `offline login` stores. An app key from
/// `offline login --api-key` (CI) can't run it.
#[command(subcommand, alias = "org")]
Orgs(commands::orgs::OrgsCommand),
/// Manage applications: native apps, origins, features, status (needs a developer session)
///
/// Needs the developer session `offline login` stores (or
/// OFFLINE_ACCESS_TOKEN). An app key from `offline login --api-key` (CI)
/// can't run it.
#[command(subcommand)]
Apps(commands::apps::AppsCommand),
/// Converge the application to the "app" block of offline.config.json (native apps, origins, features)
///
/// Additive and idempotent: registers what is missing and never removes a
/// registration. With --detect it also registers the native apps the
/// project ships (app.json, Xcode, Gradle, assetlinks.json).
Apply(commands::apply::ApplyArgs),
/// Manage application API keys (needs a developer session)
///
/// Needs the developer session `offline login` stores. An app key from
/// `offline login --api-key` (CI) can't run it.
#[command(subcommand)]
Keys(commands::keys::KeysCommand),
/// Create a new project from a workflow, template, or natural-language prompt
Create(commands::create::CreateArgs),
/// Wire Offline Protocol config + agent guidance into an existing project
Init(commands::init::InitArgs),
/// Add an Offline Protocol package to an existing project
Add(commands::add::AddArgs),
/// Plan an app from a prompt: workflow, packages, architecture documents
Plan(commands::plan::PlanArgs),
/// Check your environment for everything Offline Protocol development needs
Doctor(commands::doctor::DoctorArgs),
/// Check for a newer release of this CLI
Update(commands::update::UpdateArgs),
/// Query the ecosystem registry (packages, workflows, skills, templates)
#[command(subcommand)]
Registry(commands::registry::RegistryCommand),
/// Read integration skills (deep guides with accurate SDK usage)
#[command(subcommand)]
Skills(commands::skills::SkillsCommand),
/// Model Context Protocol server for AI agents
#[command(subcommand)]
Mcp(commands::mcp::McpCommand),
/// Generate shell completions
Completions {
/// Shell to generate completions for
#[arg(value_enum)]
shell: clap_complete::Shell,
},
}
/// Restore SIG_DFL for SIGPIPE so piping to `head` does not panic.
#[cfg(unix)]
fn reset_sigpipe() {
// SAFETY: setting a signal disposition to SIG_DFL is async-signal-safe
// and has no preconditions.
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
}
#[cfg(not(unix))]
fn reset_sigpipe() {}
fn run_async<F>(future: F) -> anyhow::Result<()>
where
F: Future<Output = anyhow::Result<()>>,
{
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("tokio runtime")
.block_on(future)
}
fn main() {
reset_sigpipe();
let cli = Cli::parse();
// `--org` overrides the stored default for this command, like OFFLINE_ORG_ID.
if let Some(org) = cli.org.as_deref().filter(|o| !o.trim().is_empty()) {
std::env::set_var(config::storage::ENV_ORG_ID, org.trim());
}
let result = match cli.command {
Command::Login(args) => run_async(commands::login::login(args)),
Command::Logout(args) => run_async(commands::login::logout(args)),
Command::Whoami(args) => run_async(commands::login::whoami(args)),
Command::Setup(args) => run_async(commands::setup::run(args)),
Command::Orgs(cmd) => run_async(commands::orgs::run(cmd)),
Command::Apps(cmd) => run_async(commands::apps::run(cmd)),
Command::Apply(args) => run_async(commands::apply::run(args)),
Command::Keys(cmd) => run_async(commands::keys::run(cmd)),
Command::Create(args) => run_async(commands::create::run(args)),
Command::Init(args) => commands::init::run(args),
Command::Add(args) => commands::add::run(args),
Command::Plan(args) => commands::plan::run(args),
Command::Doctor(args) => run_async(commands::doctor::run(args)),
Command::Update(args) => commands::update::run(args),
Command::Registry(cmd) => commands::registry::run(cmd),
Command::Skills(cmd) => commands::skills::run(cmd),
Command::Mcp(cmd) => commands::mcp::run(cmd),
Command::Completions { shell } => {
let mut cmd = Cli::command();
clap_complete::generate(shell, &mut cmd, "offline", &mut std::io::stdout());
Ok(())
}
};
if let Err(err) = result {
eprintln!("{} {err:#}", console::style("error:").red().bold());
std::process::exit(1);
}
}