use crate::*;
use carryctx::adapter::config::ConfigLoader;
use carryctx::adapter::xdg::XdgPaths;
use carryctx::application::runtime::InvocationContext;
use carryctx::error::ExitCode;
use clap::Parser;
#[derive(Parser, Debug)]
pub enum ConfigCommand {
List {
#[arg(long)]
global: bool,
},
Get { key: String },
Set {
key: String,
value: String,
#[arg(long)]
global: bool,
#[arg(long = "cfg-project")]
cfg_project: bool,
#[arg(long)]
local: bool,
},
Unset {
key: String,
#[arg(long)]
global: bool,
#[arg(long = "cfg-project")]
cfg_project: bool,
#[arg(long)]
local: bool,
},
Validate,
Sources,
Path {
#[arg(long)]
global: bool,
#[arg(long = "cfg-project")]
cfg_project: bool,
},
}
#[derive(Parser, Debug)]
pub struct ConfigArgs {
#[command(subcommand)]
pub command: ConfigCommand,
}
pub fn handle_config(
args: &ConfigArgs,
ctx: &InvocationContext,
is_json: bool,
) -> Result<ExitCode, ExitCode> {
if let Some(result) = check_dry_run(ctx, &format!("config {:?}", args.command)) {
return result;
}
let xdg = XdgPaths::new();
let work_dir = resolve_work_dir(ctx);
match &args.command {
ConfigCommand::List { global } => {
let cfg_path = if *global {
xdg.global_config()
} else {
work_dir.join(".carryctx").join("config.toml")
};
let config = if cfg_path.exists() {
std::fs::read_to_string(&cfg_path).unwrap_or_default()
} else {
String::new()
};
let data = serde_json::json!({
"path": cfg_path.to_string_lossy(),
"content": config,
});
render_and_print("config.list", Ok(data), is_json, ctx.quiet)
}
ConfigCommand::Get { key } => {
let cfg_loader = ConfigLoader::new(xdg);
let config = cfg_loader.load(Some(work_dir)).map_err(|e| e.exit_code)?;
let value = {
let config_str = toml::to_string(&config).unwrap_or_default();
let mut val = "".to_string();
for line in config_str.lines() {
if line.starts_with(key) || line.starts_with(&format!("{key} =")) {
val = line.to_string();
}
}
val
};
let data = serde_json::json!({ "key": key, "value": value });
render_and_print("config.get", Ok(data), is_json, ctx.quiet)
}
ConfigCommand::Set {
key,
value,
global,
cfg_project,
local: _,
} => {
let cfg_path = if *global {
xdg.global_config()
} else if *cfg_project {
work_dir.join(".carryctx").join("config.toml")
} else {
return Err(ExitCode::InvalidArguments);
};
if let Some(parent) = cfg_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let mut content = if cfg_path.exists() {
std::fs::read_to_string(&cfg_path).unwrap_or_default()
} else {
String::new()
};
let line = format!("{key} = \"{value}\"");
let key_prefix = format!("{key} ");
let found = content.lines().any(|l| l.trim().starts_with(&key_prefix));
if found {
let mut new_lines: Vec<String> = content
.lines()
.map(|l| {
if l.trim().starts_with(&key_prefix) {
line.clone()
} else {
l.to_string()
}
})
.collect();
new_lines.push(String::new());
content = new_lines.join("\n");
} else {
if !content.ends_with('\n') {
content.push('\n');
}
content.push_str(&line);
content.push('\n');
}
std::fs::write(&cfg_path, &content).map_err(|e| {
eprintln!("Failed to write config: {e}");
ExitCode::Configuration
})?;
let data = serde_json::json!({
"path": cfg_path.to_string_lossy(),
"key": key,
"value": value,
});
render_and_print("config.set", Ok(data), is_json, ctx.quiet)
}
ConfigCommand::Unset {
key,
global,
cfg_project,
local: _,
} => {
let cfg_path = if *global {
xdg.global_config()
} else if *cfg_project {
work_dir.join(".carryctx").join("config.toml")
} else {
return Err(ExitCode::InvalidArguments);
};
if cfg_path.exists() {
let content = std::fs::read_to_string(&cfg_path).unwrap_or_default();
let key_prefix = format!("{key} ");
let new_content: String = content
.lines()
.filter(|l| !l.trim().starts_with(&key_prefix))
.collect::<Vec<_>>()
.join("\n");
std::fs::write(&cfg_path, &new_content).map_err(|e| {
eprintln!("Failed to write config: {e}");
ExitCode::Configuration
})?;
}
render_and_print(
"config.unset",
Ok(serde_json::json!({ "key": key })),
is_json,
ctx.quiet,
)
}
ConfigCommand::Validate => {
let cfg_loader = ConfigLoader::new(xdg);
let result = cfg_loader.load(Some(work_dir));
match result {
Ok(config) => {
let data = serde_json::json!({
"valid": true,
"project": config.project,
"sources": ["global", "project", "env"]
});
render_and_print("config.validate", Ok(data), is_json, ctx.quiet)
}
Err(e) => render_and_print::<serde_json::Value>(
"config.validate",
Err(e),
is_json,
ctx.quiet,
),
}
}
ConfigCommand::Sources => {
let sources = serde_json::json!([
{ "name": "global", "path": xdg.global_config().to_string_lossy() },
{ "name": "project", "path": work_dir.join(".carryctx/config.toml").to_string_lossy() },
{ "name": "env", "prefix": "CARRYCTX_" },
]);
render_and_print("config.sources", Ok(sources), is_json, ctx.quiet)
}
ConfigCommand::Path {
global,
cfg_project,
} => {
#[allow(clippy::if_same_then_else)]
let path = if *global {
xdg.global_config()
} else if *cfg_project {
work_dir.join(".carryctx").join("config.toml")
} else {
work_dir.join(".carryctx").join("config.toml")
};
let data = serde_json::json!({ "path": path.to_string_lossy() });
render_and_print("config.path", Ok(data), is_json, ctx.quiet)
}
}
}