use std::env;
use std::path::PathBuf;
use std::process::ExitCode;
use anyhow::Result;
use clap::{Args, Parser, Subcommand};
use magma::backend::Backend as _;
use magma::pangea::WorkspaceLoader as _;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InvokeMode {
TerraformCompat,
OpenTofuCompat,
MagmaNative,
}
fn detect_mode() -> InvokeMode {
let argv0 = env::args().next().unwrap_or_default();
let basename = std::path::Path::new(&argv0)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("magma")
.to_lowercase();
if let Ok(forced) = env::var("MAGMA_INVOKE_AS") {
return match forced.as_str() {
"terraform" => InvokeMode::TerraformCompat,
"tofu" => InvokeMode::OpenTofuCompat,
_ => InvokeMode::MagmaNative,
};
}
if basename.starts_with("terraform") {
InvokeMode::TerraformCompat
} else if basename.starts_with("tofu") {
InvokeMode::OpenTofuCompat
} else {
InvokeMode::MagmaNative
}
}
#[cfg(test)]
fn is_compat_mode(m: InvokeMode) -> bool {
matches!(m, InvokeMode::TerraformCompat | InvokeMode::OpenTofuCompat)
}
#[derive(Debug, Default, Clone)]
struct TfEnv {
pub vars: std::collections::HashMap<String, String>,
pub data_dir: Option<PathBuf>,
pub log_level: Option<String>,
pub log_path: Option<PathBuf>,
pub input_enabled: bool,
pub in_automation: bool,
pub workspace: Option<String>,
pub plugin_cache_dir: Option<PathBuf>,
pub registry_tokens: std::collections::HashMap<String, String>,
pub cli_args: std::collections::HashMap<String, String>,
}
fn capture_tf_env() -> TfEnv {
let mut e = TfEnv {
input_enabled: true,
..Default::default()
};
for (k, v) in env::vars() {
match k.as_str() {
"TF_DATA_DIR" => e.data_dir = Some(PathBuf::from(v)),
"TF_LOG" => e.log_level = Some(v),
"TF_LOG_PATH" => e.log_path = Some(PathBuf::from(v)),
"TF_INPUT" => {
let lower = v.to_lowercase();
e.input_enabled = !(lower == "false" || lower == "0" || lower == "no");
}
"TF_IN_AUTOMATION" => e.in_automation = !v.is_empty(),
"TF_WORKSPACE" => e.workspace = Some(v),
"TF_PLUGIN_CACHE_DIR" => e.plugin_cache_dir = Some(PathBuf::from(v)),
_ => {
if let Some(name) = k.strip_prefix("TF_VAR_") {
e.vars.insert(name.to_lowercase(), v);
} else if let Some(host) = k.strip_prefix("TF_TOKEN_") {
e.registry_tokens.insert(host.to_string(), v);
} else if let Some(sub) = k.strip_prefix("TF_CLI_ARGS_") {
e.cli_args.insert(sub.to_lowercase(), v);
}
}
}
}
e
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OutputMode {
TerraformPlain,
MagmaNative,
}
fn detect_output_mode(invoke: InvokeMode) -> OutputMode {
match env::var("MAGMA_OUTPUT").as_deref() {
Ok("native") => OutputMode::MagmaNative,
Ok("terraform" | "plain") => OutputMode::TerraformPlain,
_ => match invoke {
InvokeMode::MagmaNative => OutputMode::TerraformPlain,
_ => OutputMode::TerraformPlain,
},
}
}
#[derive(Parser, Debug)]
#[command(
name = "magma",
version,
about = "Rust-native Pangea-Ruby-first OpenTofu-compatible IaC executor",
long_about = "Pangea declares the supercontinent's shape; magma is the molten executive force that realizes it on cloud substrate. See theory/MAGMA.md for the destination spec.",
disable_help_subcommand = true
)]
struct Cli {
#[command(subcommand)]
cmd: Option<Command>,
#[arg(long, global = true)]
detailed_exitcode: bool,
#[arg(long, global = true)]
json: bool,
#[arg(long, global = true)]
magma_native: bool,
}
#[derive(Subcommand, Debug)]
enum Command {
Init(InitArgs),
Plan(PlanArgs),
Apply(ApplyArgs),
Destroy(DestroyArgs),
#[command(subcommand)]
State(StateCommand),
Import,
#[command(subcommand)]
Workspace(WorkspaceCommand),
Output,
Show,
Refresh,
Taint,
ForceUnlock,
Get,
Fmt,
Validate,
Console,
Mcp(McpArgs),
Daemon,
Watch,
#[command(subcommand)]
Attest(AttestCommand),
#[command(subcommand)]
Config(ConfigCommand),
Flow(FlowArgs),
Capabilities,
#[command(subcommand)]
Fixture(FixtureCommand),
Migrate(MigrateArgs),
Split(SplitArgs),
Merge(MergeArgs),
#[command(subcommand)]
Rubygems(RubygemsCommand),
}
#[derive(Subcommand, Debug)]
enum RubygemsCommand {
GemsetFromLock(RubygemsFileArgs),
AttestLock(RubygemsFileArgs),
ParseGemfile(RubygemsFileArgs),
ParseLock(RubygemsFileArgs),
ParseGemspec(RubygemsFileArgs),
NixHashSha256(RubygemsFileArgs),
}
#[derive(Args, Debug)]
struct RubygemsFileArgs {
#[arg(default_value = "-")]
path: String,
}
#[derive(Subcommand, Debug)]
enum FixtureCommand {
Verify(FixtureVerifyArgs),
VerifyDir(FixtureVerifyDirArgs),
LawBattery(FixtureVerifyArgs),
LawBatteryDir(FixtureVerifyDirArgs),
}
#[derive(clap::Args, Debug)]
struct FixtureVerifyArgs {
path: PathBuf,
}
#[derive(clap::Args, Debug)]
struct FixtureVerifyDirArgs {
dir: PathBuf,
}
#[derive(clap::Args, Debug)]
struct MigrateArgs {
plan: PathBuf,
#[arg(long)]
dry_run: bool,
}
#[derive(clap::Args, Debug)]
struct SplitArgs {
#[arg(long)]
from: String,
#[arg(long)]
from_state: PathBuf,
#[arg(long)]
to: String,
#[arg(long)]
to_state: PathBuf,
#[arg(long = "resource")]
resources: Vec<String>,
#[arg(long)]
dry_run: bool,
}
#[derive(clap::Args, Debug)]
struct MergeArgs {
#[arg(long)]
from: String,
#[arg(long)]
from_state: PathBuf,
#[arg(long)]
to: String,
#[arg(long)]
to_state: PathBuf,
#[arg(long)]
dry_run: bool,
}
#[derive(clap::Args, Debug, Default)]
struct InitArgs {
#[arg(default_value = ".")]
dir: PathBuf,
#[arg(long)]
no_download: bool,
}
#[derive(clap::Args, Debug, Default)]
struct PlanArgs {
#[arg(default_value = ".")]
dir: PathBuf,
#[arg(long = "var")]
vars: Vec<String>,
#[arg(long = "var-file")]
var_files: Vec<PathBuf>,
#[arg(short = 'o', long)]
out: Option<PathBuf>,
#[cfg(feature = "tlisp")]
#[arg(long)]
tlisp: Option<PathBuf>,
#[cfg(feature = "tlisp")]
#[arg(long = "tlisp-binding", value_name = "KEY=VALUE")]
tlisp_bindings: Vec<String>,
#[cfg(feature = "tlisp")]
#[arg(long)]
tlisp_gate: Option<String>,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
refresh: bool,
}
#[derive(clap::Args, Debug, Default)]
struct ApplyArgs {
#[arg(default_value = ".")]
dir: PathBuf,
#[arg(long)]
plan_id: Option<String>,
#[arg(long)]
auto_approve: bool,
#[cfg(feature = "tlisp")]
#[arg(long)]
tlisp: Option<PathBuf>,
#[cfg(feature = "tlisp")]
#[arg(long = "tlisp-binding", value_name = "KEY=VALUE")]
tlisp_bindings: Vec<String>,
#[cfg(feature = "tlisp")]
#[arg(long)]
tlisp_gate: Option<String>,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
refresh: bool,
}
#[derive(clap::Args, Debug, Default)]
struct DestroyArgs {
#[arg(default_value = ".")]
dir: PathBuf,
#[arg(long)]
auto_approve: bool,
}
#[derive(Subcommand, Debug)]
enum StateCommand {
List,
Show {
address: String,
},
Mv {
from: String,
to: String,
},
Rm {
address: String,
},
ReplaceProvider {
from: String,
to: String,
},
}
#[derive(Subcommand, Debug)]
enum WorkspaceCommand {
Show,
List,
New { name: String },
Select { name: String },
Delete { name: String },
}
#[derive(clap::Args, Debug, Default)]
struct McpArgs {
#[arg(long)]
port: Option<u16>,
}
#[derive(Subcommand, Debug)]
enum AttestCommand {
Verify { plan_id: String },
}
#[derive(Subcommand, Debug)]
enum ConfigCommand {
Get {
key: String,
},
Set {
key: String,
value: String,
},
Edit,
}
#[derive(clap::Args, Debug, Default)]
struct FlowArgs {
flow: PathBuf,
}
fn main() -> ExitCode {
let invoke = detect_mode();
let tf_env = capture_tf_env();
let output = detect_output_mode(invoke);
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_env("TF_LOG")
.or_else(|_| tracing_subscriber::EnvFilter::try_from_default_env())
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
let cli = Cli::parse();
let runtime = match tokio::runtime::Runtime::new() {
Ok(r) => r,
Err(e) => {
eprintln!("magma: tokio runtime: {e}");
return ExitCode::from(1);
}
};
let result = runtime.block_on(run(cli, invoke, output, tf_env));
match result {
Ok(code) => ExitCode::from(code),
Err(e) => {
eprintln!("magma: {e:#}");
ExitCode::from(1)
}
}
}
async fn run(cli: Cli, invoke: InvokeMode, output: OutputMode, tf_env: TfEnv) -> Result<u8> {
let _ = (output, &tf_env);
let Some(cmd) = cli.cmd else {
print_banner(invoke);
return Ok(0);
};
match cmd {
Command::Init(args) => cmd_init(args),
Command::Plan(args) => cmd_plan(args, cli.detailed_exitcode).await,
Command::Apply(args) => cmd_apply(args).await,
Command::Destroy(args) => cmd_destroy(args).await,
Command::State(s) => cmd_state(s).await,
Command::Import => stub("import"),
Command::Workspace(w) => cmd_workspace(w),
Command::Output => stub("output"),
Command::Show => stub("show"),
Command::Refresh => stub("refresh"),
Command::Taint => stub("taint"),
Command::ForceUnlock => stub("force-unlock"),
Command::Get => stub("get"),
Command::Fmt => cmd_fmt(),
Command::Validate => stub("validate"),
Command::Console => stub("console"),
Command::Mcp(args) => cmd_mcp(args),
Command::Daemon => stub("daemon"),
Command::Watch => stub("watch"),
Command::Attest(a) => cmd_attest(a),
Command::Config(c) => cmd_config(c),
Command::Flow(args) => cmd_flow(args).await,
Command::Capabilities => cmd_capabilities(),
Command::Fixture(cmd) => cmd_fixture(cmd).await,
Command::Migrate(args) => cmd_migrate(args).await,
Command::Split(args) => cmd_split(args).await,
Command::Merge(args) => cmd_merge(args).await,
Command::Rubygems(cmd) => cmd_rubygems(cmd).await,
}
}
async fn cmd_rubygems(cmd: RubygemsCommand) -> Result<u8> {
use std::io::Read;
let read_input = |args: &RubygemsFileArgs| -> Result<String> {
if args.path == "-" {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
Ok(buf)
} else {
Ok(std::fs::read_to_string(&args.path)?)
}
};
match cmd {
RubygemsCommand::GemsetFromLock(args) => {
let source = read_input(&args)?;
let lock = magma_rubygems::lockfile::parse(&source)
.map_err(|e| anyhow::anyhow!("parse Gemfile.lock: {e}"))?;
let text = magma_rubygems::nix::emit_gemset(&lock)
.map_err(|e| anyhow::anyhow!("emit gemset.nix: {e}"))?;
print!("{text}");
Ok(0)
}
RubygemsCommand::AttestLock(args) => {
let source = read_input(&args)?;
let lock = magma_rubygems::lockfile::parse(&source)
.map_err(|e| anyhow::anyhow!("parse Gemfile.lock: {e}"))?;
let attestation = magma_rubygems::attestation::attest_lockfile(&lock);
println!("{attestation}");
Ok(0)
}
RubygemsCommand::ParseGemfile(args) => {
let source = read_input(&args)?;
let manifest = magma_rubygems::gemfile_parser::parse(&source)
.map_err(|e| anyhow::anyhow!("parse Gemfile: {e}"))?;
println!("{}", serde_json::to_string_pretty(&manifest)?);
Ok(0)
}
RubygemsCommand::ParseLock(args) => {
let source = read_input(&args)?;
let lock = magma_rubygems::lockfile::parse(&source)
.map_err(|e| anyhow::anyhow!("parse Gemfile.lock: {e}"))?;
println!("{}", serde_json::to_string_pretty(&lock)?);
Ok(0)
}
RubygemsCommand::ParseGemspec(args) => {
let source = read_input(&args)?;
let spec = magma_rubygems::gemspec_parser::parse(&source)
.map_err(|e| anyhow::anyhow!("parse gemspec: {e}"))?;
println!("{}", serde_json::to_string_pretty(&spec)?);
Ok(0)
}
RubygemsCommand::NixHashSha256(args) => {
let bytes = if args.path == "-" {
use std::io::Read;
let mut buf = Vec::new();
std::io::stdin().read_to_end(&mut buf)?;
buf
} else {
std::fs::read(&args.path)?
};
println!("{}", magma_rubygems::nixhash::sha256_nix(&bytes));
Ok(0)
}
}
}
async fn cmd_fixture(cmd: FixtureCommand) -> Result<u8> {
match cmd {
FixtureCommand::Verify(args) => {
let harness = magma_arch_test::WorkspaceTestHarness::new(args.path.clone());
match harness.verify().await {
Ok(report) => {
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(0)
}
Err(e) => {
eprintln!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"path": args.path,
"status": "failed",
"error": e.to_string(),
}))?,
);
Ok(1)
}
}
}
FixtureCommand::VerifyDir(args) => match magma_arch_test::verify_directory(&args.dir).await
{
Ok(agg) => {
println!("{}", serde_json::to_string_pretty(&agg)?);
Ok(if agg.failed == 0 { 0 } else { 1 })
}
Err(e) => {
eprintln!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"dir": args.dir,
"status": "failed",
"error": e.to_string(),
}))?,
);
Ok(1)
}
},
FixtureCommand::LawBattery(args) => {
let harness = magma_arch_test::WorkspaceTestHarness::new(args.path.clone());
match harness.assert_all_substrate_laws().await {
Ok(report) => {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"path": args.path,
"status": "passed",
"laws": ["architecture::assert_all_laws", "workspace::assert_all_laws"],
"report": report,
}))?,
);
Ok(0)
}
Err(e) => {
eprintln!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"path": args.path,
"status": "violated",
"error": e.to_string(),
}))?,
);
Ok(1)
}
}
}
FixtureCommand::LawBatteryDir(args) => {
match magma_arch_test::run_law_battery_directory(&args.dir).await {
Ok(agg) => {
println!("{}", serde_json::to_string_pretty(&agg)?);
Ok(if agg.failed == 0 { 0 } else { 1 })
}
Err(e) => {
eprintln!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"dir": args.dir,
"status": "failed",
"error": e.to_string(),
}))?,
);
Ok(1)
}
}
}
}
}
fn cmd_capabilities() -> Result<u8> {
let mcp_tools: Vec<String> = magma::mcp::tool_specs()
.into_iter()
.map(|t| t.name)
.collect();
let manifest = serde_json::json!({
"tool": "magma",
"version": env!("CARGO_PKG_VERSION"),
"schema_version": 1,
"supported_protocols": ["tfplugin5", "tfplugin6"],
"input_formats": ["pangea-ruby-inprocess", "terraform-json"],
"input_formats_excluded": ["hcl2"],
"backends": ["local", "s3 (planned M1)"],
"subcommands": [
"init", "plan", "apply", "destroy",
"state", "import", "workspace", "output", "show",
"refresh", "taint", "force-unlock", "get", "fmt",
"validate", "console",
"mcp", "daemon", "watch", "attest", "config", "flow",
"capabilities",
],
"interfaces": {
"drop_in_cli": { "argv0_modes": ["terraform", "tofu", "magma"] },
"native_cli": { "json_flag": true },
"mcp": { "transport": "stdin-stdout-jsonrpc2", "tools": mcp_tools.len() },
"library": { "umbrella_crate": "magma" }
},
"mcp_tools": mcp_tools,
"env_vars_honored": [
"TF_VAR_*", "TF_DATA_DIR", "TF_LOG", "TF_LOG_PATH",
"TF_CLI_ARGS_*", "TF_INPUT", "TF_IN_AUTOMATION",
"TF_TOKEN_*", "TF_PLUGIN_CACHE_DIR", "TF_WORKSPACE",
"MAGMA_OUTPUT", "MAGMA_INVOKE_AS"
],
"exit_codes": {
"0": "success",
"1": "error",
"2": "plan: changes pending (-detailed-exitcode)"
},
"workspace_primitive_supported": true,
"workspace_chain_supported": true,
"in_memory_pipeline_supported": true,
"shigoto_job_wrapping": "planned (M1)"
});
println!("{}", serde_json::to_string_pretty(&manifest)?);
Ok(0)
}
fn print_banner(invoke: InvokeMode) {
match invoke {
InvokeMode::TerraformCompat => {
println!(
"Terraform v1.5-compat (magma {})",
env!("CARGO_PKG_VERSION")
);
println!("Run `terraform --help` for usage.");
}
InvokeMode::OpenTofuCompat => {
println!("OpenTofu v1.7-compat (magma {})", env!("CARGO_PKG_VERSION"));
println!("Run `tofu --help` for usage.");
}
InvokeMode::MagmaNative => {
println!(
"magma {} — Rust-native Pangea-Ruby-first IaC executor",
env!("CARGO_PKG_VERSION"),
);
println!("See `magma --help` and theory/MAGMA.md for the destination spec.");
}
}
}
fn stub(name: &str) -> Result<u8> {
eprintln!("magma {name}: not yet implemented (M0 in flight — see theory/MAGMA.md §VI)");
Ok(1)
}
fn cmd_init(args: InitArgs) -> Result<u8> {
eprintln!(
"magma init: workspace {:?} (no-download={})",
args.dir, args.no_download
);
use magma::pangea::WorkspaceShape;
let shape = WorkspaceShape::discover(&args.dir)
.map_err(|e| anyhow::anyhow!("workspace discovery: {e}"))?;
match shape {
WorkspaceShape::PangeaRuby { ruby_files, .. } => {
println!(
"magma init: PangeaRuby workspace ({} .rb files)",
ruby_files.len()
);
}
WorkspaceShape::TerraformJson { json_files, .. } => {
println!(
"magma init: TerraformJson workspace ({} .tf.json files)",
json_files.len()
);
}
}
Ok(0)
}
type LoadedWorkspace = (
magma::config::Config,
magma::backend::LocalBackend,
magma::types::State,
);
macro_rules! impl_loaded_workspace {
($($args:ty),+ $(,)?) => { $(
impl $args {
#[cfg(feature = "tlisp")]
async fn load_workspace(&self) -> Result<LoadedWorkspace> {
match &self.tlisp {
Some(path) => {
synthesize_via_tlisp(
path,
&self.tlisp_bindings,
self.tlisp_gate.as_deref(),
&self.dir,
)
.await
}
None => load_workspace_and_state(&self.dir).await,
}
}
#[cfg(not(feature = "tlisp"))]
async fn load_workspace(&self) -> Result<LoadedWorkspace> {
load_workspace_and_state(&self.dir).await
}
}
)+ };
}
impl_loaded_workspace!(PlanArgs, ApplyArgs);
#[cfg(feature = "tlisp")]
async fn synthesize_via_tlisp(
tlisp_path: &std::path::Path,
bindings: &[String],
gate: Option<&str>,
state_dir: &std::path::Path,
) -> Result<(
magma::config::Config,
magma::backend::LocalBackend,
magma::types::State,
)> {
let mut typed_bindings: indexmap::IndexMap<String, magma_lava::Binding> =
indexmap::IndexMap::new();
for kv in bindings {
let (k, v) = kv
.split_once('=')
.ok_or_else(|| anyhow::anyhow!("--tlisp-binding must be KEY=VALUE: `{kv}`"))?;
typed_bindings.insert(k.to_string(), magma_lava::Binding::Scalar(v.to_string()));
}
let source = magma_lava::LavaSource::Path {
path: tlisp_path.to_path_buf(),
};
let plan = magma_lava::synthesize_source(&source, &typed_bindings, gate)
.map_err(|e| anyhow::anyhow!("magma-lava synthesize: {e}"))?;
let cfg = magma::config::Config::from_json(plan.terraform_json)
.map_err(|e| anyhow::anyhow!("parse synthesized terraform.json: {e}"))?;
let backend = magma::backend::LocalBackend::new(state_dir.to_path_buf());
let state = backend
.read_state()
.await
.map_err(|e| anyhow::anyhow!("read state: {e}"))?;
Ok((cfg, backend, state))
}
async fn load_workspace_and_state(
dir: &std::path::Path,
) -> Result<(
magma::config::Config,
magma::backend::LocalBackend,
magma::types::State,
)> {
let shape = magma::pangea::WorkspaceShape::discover(dir)
.map_err(|e| anyhow::anyhow!("discover: {e}"))?;
let loaded = magma::pangea::TerraformJsonLoader
.load(shape)
.await
.map_err(|e| anyhow::anyhow!("load: {e}"))?;
let cfg = magma::config::Config::from_json(loaded.rendered)
.map_err(|e| anyhow::anyhow!("parse: {e}"))?;
let backend = magma::backend::LocalBackend::new(dir.to_path_buf());
let state = backend
.read_state()
.await
.map_err(|e| anyhow::anyhow!("read state: {e}"))?;
Ok((cfg, backend, state))
}
fn report_refresh(report: &magma::apply::engine::RefreshReport) {
let observation = report.observation();
let coverage = observation.coverage();
if !coverage.supports_in_sync_claim() {
eprintln!(
"magma: plan-time refresh is {coverage} — {} of {} state instance(s) could not be \
read from the provider. This plan's `before` side is REMEMBERED state, not \
observed reality; an empty change set here is NOT evidence that anything matches.",
report.kept_on_error,
observation.counts().probed(),
);
}
let changed = report.refreshed > 0
|| report.dropped_instances > 0
|| report.dropped_resources > 0
|| report.suppressed_mass_drop > 0;
if !changed {
return;
}
eprintln!(
"magma: plan-time refresh — {} instance(s) updated from the real world, \
{} confirmed gone ({} resource(s) dropped from state), \
{} suppressed as a mass-drop anomaly (systemic read failure, not genuine drift)",
report.refreshed,
report.dropped_instances,
report.dropped_resources,
report.suppressed_mass_drop,
);
}
async fn cmd_plan(args: PlanArgs, detailed: bool) -> Result<u8> {
let (cfg, backend, mut state) = args.load_workspace().await?;
let refresh_ctx = args
.refresh
.then(|| magma::apply::engine::ApplyContext::new(args.dir.clone()));
let (plan, refresh_report) =
magma::apply::engine::refresh_then_plan(&cfg, &mut state, refresh_ctx.as_ref())
.await
.map_err(|e| anyhow::anyhow!("plan: {e}"))?;
if let Some(report) = &refresh_report {
report_refresh(report);
backend
.write_state(&state)
.await
.map_err(|e| anyhow::anyhow!("write refreshed state: {e}"))?;
}
let summary = serde_json::json!({
"plan_id": hex::encode(plan.id.0),
"created_at": plan.created_at,
"resource_changes": plan.resource_changes.len(),
"changes": plan.resource_changes,
"observation": plan.observation,
"verdict": plan.drift_verdict(),
});
if let Some(out) = args.out {
tokio::fs::write(&out, serde_json::to_vec_pretty(&plan)?).await?;
eprintln!("plan written to {}", out.display());
} else {
println!("{}", serde_json::to_string_pretty(&summary)?);
}
let has_changes = plan.resource_changes.iter().any(|c| {
!matches!(
c.action,
magma::types::Action::NoOp | magma::types::Action::Read
)
});
Ok(if detailed && has_changes { 2 } else { 0 })
}
async fn cmd_apply(args: ApplyArgs) -> Result<u8> {
let (cfg, backend, mut state) = args.load_workspace().await?;
let refresh_ctx = args
.refresh
.then(|| magma::apply::engine::ApplyContext::new(args.dir.clone()));
let (plan, refresh_report) =
magma::apply::engine::refresh_then_plan(&cfg, &mut state, refresh_ctx.as_ref())
.await
.map_err(|e| anyhow::anyhow!("plan: {e}"))?;
if let Some(report) = &refresh_report {
report_refresh(report);
backend
.write_state(&state)
.await
.map_err(|e| anyhow::anyhow!("write refreshed state: {e}"))?;
}
if !args.auto_approve {
eprintln!(
"magma apply: would apply {} resource changes. Re-run with --auto-approve to proceed.",
plan.resource_changes.len(),
);
return Ok(0);
}
let outcome =
magma::apply::run_plan(&plan, &mut state).map_err(|e| anyhow::anyhow!("apply: {e}"))?;
backend
.write_state(&state)
.await
.map_err(|e| anyhow::anyhow!("write state: {e}"))?;
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"plan_id": hex::encode(outcome.plan_id.0),
"applied": outcome.applied.len(),
"failed": outcome.failed.len(),
"started_at": outcome.started_at,
"finished_at": outcome.finished_at,
}))?,
);
Ok(if outcome.failed.is_empty() { 0 } else { 1 })
}
async fn cmd_destroy(args: DestroyArgs) -> Result<u8> {
use magma::types::{Action, ChangeReason, Plan as TPlan, PlanId, ResourceChange};
let backend = magma::backend::LocalBackend::new(args.dir.clone());
let mut state = backend
.read_state()
.await
.map_err(|e| anyhow::anyhow!("read state: {e}"))?;
let resource_changes: Vec<ResourceChange> = state
.resources
.iter()
.map(|r| ResourceChange {
address: r.address.clone(),
action: Action::Delete,
before: r.instances.first().map(|i| i.attributes.clone()),
after: None,
reasons: vec![ChangeReason::DeletedResource],
meta: Default::default(),
})
.collect();
let plan = TPlan {
id: PlanId([0u8; 32]),
created_at: chrono::Utc::now(),
config_root: args.dir.clone(),
variables: Default::default(),
resource_changes,
output_changes: vec![],
observation: magma::types::Observation::unrefreshed(),
};
if !args.auto_approve {
eprintln!(
"magma destroy: would destroy {} resources. Re-run with --auto-approve to proceed.",
plan.resource_changes.len(),
);
return Ok(0);
}
let outcome =
magma::apply::run_plan(&plan, &mut state).map_err(|e| anyhow::anyhow!("apply: {e}"))?;
backend
.write_state(&state)
.await
.map_err(|e| anyhow::anyhow!("write state: {e}"))?;
eprintln!("magma destroy: removed {} resources", outcome.applied.len());
Ok(if outcome.failed.is_empty() { 0 } else { 1 })
}
async fn cmd_state(cmd: StateCommand) -> Result<u8> {
let backend = magma::backend::LocalBackend::new(std::env::current_dir()?);
let state = backend
.read_state()
.await
.map_err(|e| anyhow::anyhow!("read state: {e}"))?;
match cmd {
StateCommand::List => {
for r in &state.resources {
println!(
"{}.{}{}",
r.address.type_id.0,
r.address.name,
match &r.address.key {
Some(k) => format!(" [{k:?}]"),
None => String::new(),
},
);
}
Ok(0)
}
StateCommand::Show { address } => {
let found = state
.resources
.iter()
.find(|r| format!("{}.{}", r.address.type_id.0, r.address.name) == address);
match found {
Some(r) => {
println!("{}", serde_json::to_string_pretty(r)?);
Ok(0)
}
None => {
eprintln!("magma state show: address {address} not found in state");
Ok(1)
}
}
}
StateCommand::Mv { .. } => stub("state mv"),
StateCommand::Rm { .. } => stub("state rm"),
StateCommand::ReplaceProvider { .. } => stub("state replace-provider"),
}
}
fn cmd_workspace(cmd: WorkspaceCommand) -> Result<u8> {
match cmd {
WorkspaceCommand::Show => {
println!("default");
Ok(0)
}
WorkspaceCommand::List => {
println!("* default");
Ok(0)
}
WorkspaceCommand::New { .. } => stub("workspace new"),
WorkspaceCommand::Select { .. } => stub("workspace select"),
WorkspaceCommand::Delete { .. } => stub("workspace delete"),
}
}
fn cmd_fmt() -> Result<u8> {
eprintln!("magma fmt: no-op (magma never reads HCL — see theory/MAGMA.md §IX)");
Ok(0)
}
fn cmd_mcp(args: McpArgs) -> Result<u8> {
eprintln!("magma mcp: starting MCP server (port={:?})", args.port);
eprintln!("[stub] M0.x: wire JSON-RPC 2.0 dispatch via magma-mcp::dispatch");
let _specs = magma::mcp::tool_specs();
eprintln!(
"(magma-mcp registered {} typed tools)",
magma::mcp::tool_specs().len()
);
Ok(0)
}
fn cmd_attest(cmd: AttestCommand) -> Result<u8> {
match cmd {
AttestCommand::Verify { plan_id } => {
eprintln!("magma attest verify: plan_id={plan_id}");
stub("attest verify")
}
}
}
fn cmd_config(cmd: ConfigCommand) -> Result<u8> {
match cmd {
ConfigCommand::Get { key } => {
eprintln!("magma config get: {key}");
stub("config get")
}
ConfigCommand::Set { key, value } => {
eprintln!("magma config set: {key}={value}");
stub("config set")
}
ConfigCommand::Edit => stub("config edit"),
}
}
async fn cmd_flow(args: FlowArgs) -> Result<u8> {
let bytes = tokio::fs::read(&args.flow).await?;
let is_lisp = args
.flow
.extension()
.and_then(|s| s.to_str())
.map(|s| s == "lisp" || s == "tatara" || s == "scm")
.unwrap_or(false);
let flow: magma_flow::FlowFile = if is_lisp {
let source = std::str::from_utf8(&bytes)
.map_err(|e| anyhow::anyhow!("invalid UTF-8 in lisp source: {e}"))?;
let value =
magma_tatara::parse_deforch(source).map_err(|e| anyhow::anyhow!("lisp parse: {e}"))?;
serde_json::from_value(value)?
} else {
serde_json::from_slice(&bytes)?
};
let report = magma_flow::run(&flow)
.await
.map_err(|e| anyhow::anyhow!("flow run: {e}"))?;
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(0)
}
async fn cmd_migrate(args: MigrateArgs) -> Result<u8> {
let raw = if args.plan.as_os_str() == "-" {
use tokio::io::AsyncReadExt as _;
let mut buf = Vec::new();
tokio::io::stdin().read_to_end(&mut buf).await?;
buf
} else {
tokio::fs::read(&args.plan).await?
};
let mut plan: magma_migrate::MigrationPlan = serde_json::from_slice(&raw)
.map_err(|e| anyhow::anyhow!("parse MigrationPlan JSON: {e}"))?;
if args.dry_run {
plan.dry_run = true;
}
let receipt = magma_migrate::run(plan)
.await
.map_err(|e| anyhow::anyhow!("migrate: {e}"))?;
println!("{}", serde_json::to_string_pretty(&receipt)?);
Ok(0)
}
fn migration_plan(
from_name: String,
from_state: PathBuf,
to_name: String,
to_state: PathBuf,
moves: Vec<magma_migrate::ResourceMove>,
dry_run: bool,
) -> magma_migrate::MigrationPlan {
magma_migrate::MigrationPlan {
from: magma_migrate::WorkspaceRef {
name: from_name,
state_path: from_state,
},
to: magma_migrate::WorkspaceRef {
name: to_name,
state_path: to_state,
},
moves,
preserve: magma_migrate::PreserveFlags::default(),
dry_run,
}
}
async fn cmd_split(args: SplitArgs) -> Result<u8> {
if args.resources.is_empty() {
anyhow::bail!("magma split: at least one --resource is required");
}
let moves: Vec<magma_migrate::ResourceMove> = args
.resources
.iter()
.map(|addr| magma_migrate::ResourceMove {
source_address: addr.clone(),
target_address: addr.clone(),
})
.collect();
let plan = migration_plan(
args.from,
args.from_state,
args.to,
args.to_state,
moves,
args.dry_run,
);
let receipt = magma_migrate::run(plan)
.await
.map_err(|e| anyhow::anyhow!("split: {e}"))?;
println!("{}", serde_json::to_string_pretty(&receipt)?);
Ok(0)
}
async fn cmd_merge(args: MergeArgs) -> Result<u8> {
let bytes = tokio::fs::read(&args.from_state)
.await
.map_err(|e| anyhow::anyhow!("read {:?}: {e}", args.from_state))?;
let from_state_json: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|e| anyhow::anyhow!("parse source state JSON: {e}"))?;
let resources = from_state_json
.get("resources")
.and_then(|r| r.as_array())
.ok_or_else(|| anyhow::anyhow!("source state has no resources array"))?;
let mut moves: Vec<magma_migrate::ResourceMove> = Vec::new();
for r in resources {
let kind = r
.get("type")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("resource missing `type`"))?;
let name = r
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("resource missing `name`"))?;
let addr = format!("{kind}.{name}");
moves.push(magma_migrate::ResourceMove {
source_address: addr.clone(),
target_address: addr,
});
}
let plan = migration_plan(
args.from,
args.from_state,
args.to,
args.to_state,
moves,
args.dry_run,
);
let receipt = magma_migrate::run(plan)
.await
.map_err(|e| anyhow::anyhow!("merge: {e}"))?;
println!("{}", serde_json::to_string_pretty(&receipt)?);
Ok(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_mode_via_env_override() {
unsafe {
env::set_var("MAGMA_INVOKE_AS", "terraform");
assert_eq!(detect_mode(), InvokeMode::TerraformCompat);
env::set_var("MAGMA_INVOKE_AS", "tofu");
assert_eq!(detect_mode(), InvokeMode::OpenTofuCompat);
env::set_var("MAGMA_INVOKE_AS", "magma");
assert_eq!(detect_mode(), InvokeMode::MagmaNative);
env::remove_var("MAGMA_INVOKE_AS");
}
}
#[test]
fn is_compat_mode_basic() {
assert!(is_compat_mode(InvokeMode::TerraformCompat));
assert!(is_compat_mode(InvokeMode::OpenTofuCompat));
assert!(!is_compat_mode(InvokeMode::MagmaNative));
}
}