use std::path::PathBuf;
use anyhow::{bail, Result};
use clap::Args;
use offline_generator::project_wiring::{self, McpRegistration};
use offline_registry::Registry;
use serde_json::json;
use crate::config::CliConfig;
use crate::output;
#[derive(Args)]
pub struct InitArgs {
#[arg(long, default_value = ".")]
pub dir: PathBuf,
#[arg(long)]
pub app_id: Option<String>,
#[arg(long)]
pub json: bool,
}
pub fn run(args: InitArgs) -> Result<()> {
let dir = &args.dir;
let pkg_path = dir.join("package.json");
if !pkg_path.exists() {
bail!(
"no package.json in {} — `offline init` wires existing JS/TS projects; \
use `offline create` for new ones",
dir.display()
);
}
let app_id = args
.app_id
.or_else(|| CliConfig::load().ok().and_then(|c| c.effective_app_id()))
.unwrap_or_default();
let pkg: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&pkg_path)?)?;
let deps = collect_deps(&pkg);
let platform = detect_platform(&deps);
let project_name = pkg
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("offline-app")
.to_string();
let registry = Registry::load_default()?;
let installed: Vec<String> = registry
.packages()
.filter(|p| deps.iter().any(|d| d == &p.name))
.map(|p| p.name.clone())
.collect();
let mut written = Vec::new();
let config_path = dir.join("offline.config.json");
if config_path.exists() {
output::warn("offline.config.json already exists — leaving it untouched");
} else {
let config = json!({
"$schema": "https://dev.offlineprotocol.com/schemas/offline.config.json",
"projectName": project_name,
"appId": app_id,
"platform": platform,
"packages": installed,
});
std::fs::write(&config_path, serde_json::to_string_pretty(&config)?)?;
written.push("offline.config.json");
}
let mcp_path = dir.join(".mcp.json");
let registration = if mcp_path.exists() {
if project_wiring::registers_offline_mcp(&std::fs::read_to_string(&mcp_path)?) {
McpRegistration::AlreadyRegistered
} else {
output::warn(&format!(
".mcp.json already exists and does not register the Offline Protocol MCP server; \
left it unchanged. Add `{}` to your agent (see `offline mcp install`) or use the Offline Protocol plugin.",
offline_registry::mcp_serve_command_line()
));
McpRegistration::NotRegistered
}
} else {
std::fs::write(
&mcp_path,
serde_json::to_string_pretty(&project_wiring::mcp_json())? + "\n",
)?;
written.push(".mcp.json");
McpRegistration::Written
};
let agents_path = dir.join("AGENTS.md");
let skills: Vec<String> = registry
.packages()
.filter(|p| installed.contains(&p.name))
.flat_map(|p| p.skills.clone())
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
let section = project_wiring::agents_section(&installed, &skills, registration);
if agents_path.exists() {
let existing = std::fs::read_to_string(&agents_path)?;
if project_wiring::agents_has_section(&existing) {
output::warn("AGENTS.md already has an Offline Protocol section; skipping");
} else {
std::fs::write(&agents_path, format!("{existing}\n{section}"))?;
written.push("AGENTS.md (appended)");
}
} else {
std::fs::write(&agents_path, format!("# Agent guide\n{section}"))?;
written.push("AGENTS.md");
}
if args.json {
return output::json(&json!({
"dir": dir,
"platform": platform,
"detectedPackages": installed,
"filesWritten": written,
}));
}
output::success(&format!(
"Initialized Offline Protocol wiring in {} ({})",
dir.display(),
platform
));
for w in &written {
output::item(w, "");
}
if installed.is_empty() {
output::note(
"no @offline-protocol packages detected yet — add one with `offline add <package>`",
);
}
Ok(())
}
fn collect_deps(pkg: &serde_json::Value) -> Vec<String> {
let mut out = Vec::new();
for key in ["dependencies", "devDependencies", "peerDependencies"] {
if let Some(map) = pkg.get(key).and_then(|d| d.as_object()) {
out.extend(map.keys().cloned());
}
}
out
}
fn detect_platform(deps: &[String]) -> &'static str {
if deps.iter().any(|d| d == "react-native") {
"react-native"
} else if deps.iter().any(|d| d == "next") {
"nextjs"
} else if deps.iter().any(|d| d == "react") {
"web"
} else {
"unknown"
}
}