Skip to content

Commit 3e4b8da

Browse files
author
Greyforge Admin
committed
Add workspace unset command
1 parent 7954d02 commit 3e4b8da

2 files changed

Lines changed: 182 additions & 10 deletions

File tree

src/cortex-cli/src/workspace_cmd.rs

Lines changed: 94 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ pub enum WorkspaceSubcommand {
3030
/// Set workspace settings
3131
Set(WorkspaceSetArgs),
3232

33+
/// Unset (remove) workspace settings
34+
Unset(WorkspaceUnsetArgs),
35+
3336
/// Open workspace configuration in editor
3437
Edit(WorkspaceEditArgs),
3538
}
@@ -64,6 +67,13 @@ pub struct WorkspaceSetArgs {
6467
pub value: String,
6568
}
6669

70+
/// Arguments for workspace unset command.
71+
#[derive(Debug, Parser)]
72+
pub struct WorkspaceUnsetArgs {
73+
/// Configuration key to remove
74+
pub key: String,
75+
}
76+
6777
/// Arguments for workspace edit command.
6878
#[derive(Debug, Parser)]
6979
pub struct WorkspaceEditArgs {
@@ -104,6 +114,7 @@ impl WorkspaceCli {
104114
Some(WorkspaceSubcommand::Show(args)) => run_show(args).await,
105115
Some(WorkspaceSubcommand::Init(args)) => run_init(args).await,
106116
Some(WorkspaceSubcommand::Set(args)) => run_set(args).await,
117+
Some(WorkspaceSubcommand::Unset(args)) => run_unset(args).await,
107118
Some(WorkspaceSubcommand::Edit(args)) => run_edit(args).await,
108119
}
109120
}
@@ -322,6 +333,19 @@ async fn run_init(args: WorkspaceInitArgs) -> Result<()> {
322333
Ok(())
323334
}
324335

336+
fn workspace_config_key(key: &str) -> (&str, &str) {
337+
match key {
338+
"model" | "default_model" => ("model", "default"),
339+
"sandbox" | "sandbox_mode" => ("sandbox", "mode"),
340+
"approval" | "approval_mode" => ("approval", "mode"),
341+
k if k.contains('.') => {
342+
let parts: Vec<&str> = k.splitn(2, '.').collect();
343+
(parts[0], parts[1])
344+
}
345+
_ => ("", key),
346+
}
347+
}
348+
325349
async fn run_set(args: WorkspaceSetArgs) -> Result<()> {
326350
let root = find_workspace_root();
327351
let cortex_dir = root.join(".cortex");
@@ -345,16 +369,7 @@ async fn run_set(args: WorkspaceSetArgs) -> Result<()> {
345369
.unwrap_or_else(|_| toml_edit::DocumentMut::new());
346370

347371
// Map common keys to their TOML sections
348-
let (section, actual_key) = match args.key.as_str() {
349-
"model" | "default_model" => ("model", "default"),
350-
"sandbox" | "sandbox_mode" => ("sandbox", "mode"),
351-
"approval" | "approval_mode" => ("approval", "mode"),
352-
k if k.contains('.') => {
353-
let parts: Vec<&str> = k.splitn(2, '.').collect();
354-
(parts[0], parts[1])
355-
}
356-
_ => ("", args.key.as_str()),
357-
};
372+
let (section, actual_key) = workspace_config_key(&args.key);
358373

359374
if section.is_empty() {
360375
doc[actual_key] = toml_edit::value(&args.value);
@@ -373,6 +388,54 @@ async fn run_set(args: WorkspaceSetArgs) -> Result<()> {
373388
Ok(())
374389
}
375390

391+
async fn run_unset(args: WorkspaceUnsetArgs) -> Result<()> {
392+
let root = find_workspace_root();
393+
let cortex_dir = root.join(".cortex");
394+
let config_path = cortex_dir.join("config.toml");
395+
396+
if !config_path.exists() {
397+
bail!(
398+
"No workspace configuration file found at {}",
399+
config_path.display()
400+
);
401+
}
402+
403+
let content = std::fs::read_to_string(&config_path)?;
404+
let mut doc = content.parse::<toml_edit::DocumentMut>()?;
405+
let (section, actual_key) = workspace_config_key(&args.key);
406+
407+
let removed = if section.is_empty() {
408+
doc.as_table_mut().remove(actual_key).is_some()
409+
} else {
410+
let mut remove_empty_section = false;
411+
let removed = if let Some(table) = doc.get_mut(section).and_then(|item| item.as_table_mut())
412+
{
413+
let removed = table.remove(actual_key).is_some();
414+
remove_empty_section = removed && table.iter().next().is_none();
415+
removed
416+
} else {
417+
false
418+
};
419+
420+
if remove_empty_section {
421+
doc.as_table_mut().remove(section);
422+
}
423+
424+
removed
425+
};
426+
427+
if !removed {
428+
bail!("Key '{}' not found in workspace configuration", args.key);
429+
}
430+
431+
std::fs::write(&config_path, doc.to_string())?;
432+
433+
println!("Removed key: {}", args.key);
434+
println!("Config saved to: {}", config_path.display());
435+
436+
Ok(())
437+
}
438+
376439
async fn run_edit(args: WorkspaceEditArgs) -> Result<()> {
377440
let root = find_workspace_root();
378441
let cortex_dir = root.join(".cortex");
@@ -691,6 +754,27 @@ mod tests {
691754
assert_eq!(args.value, "full-access");
692755
}
693756

757+
// ==========================================================================
758+
// WorkspaceUnsetArgs tests
759+
// ==========================================================================
760+
761+
#[test]
762+
fn test_workspace_unset_args_simple_key() {
763+
let args = WorkspaceUnsetArgs {
764+
key: "model".to_string(),
765+
};
766+
767+
assert_eq!(args.key, "model");
768+
}
769+
770+
#[test]
771+
fn test_workspace_config_key_maps_aliases() {
772+
assert_eq!(workspace_config_key("model"), ("model", "default"));
773+
assert_eq!(workspace_config_key("sandbox_mode"), ("sandbox", "mode"));
774+
assert_eq!(workspace_config_key("approval.mode"), ("approval", "mode"));
775+
assert_eq!(workspace_config_key("custom_key"), ("", "custom_key"));
776+
}
777+
694778
// ==========================================================================
695779
// WorkspaceEditArgs tests
696780
// ==========================================================================
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
use std::fs;
2+
use std::process::Command;
3+
4+
use tempfile::tempdir;
5+
6+
fn combined_output(output: &std::process::Output) -> String {
7+
format!(
8+
"{}{}",
9+
String::from_utf8_lossy(&output.stdout),
10+
String::from_utf8_lossy(&output.stderr)
11+
)
12+
}
13+
14+
#[test]
15+
fn workspace_unset_removes_keys_from_workspace_config() {
16+
let repo = tempdir().unwrap();
17+
fs::create_dir(repo.path().join(".git")).unwrap();
18+
let cortex_dir = repo.path().join(".cortex");
19+
fs::create_dir_all(&cortex_dir).unwrap();
20+
let config_path = cortex_dir.join("config.toml");
21+
fs::write(
22+
&config_path,
23+
r#"remove_me = "gone"
24+
keep_me = "stays"
25+
26+
[model]
27+
default = "gpt-4o"
28+
29+
[sandbox]
30+
mode = "workspace-write"
31+
"#,
32+
)
33+
.unwrap();
34+
35+
let model_output = Command::new(env!("CARGO_BIN_EXE_Cortex"))
36+
.args(["workspace", "unset", "model"])
37+
.current_dir(repo.path())
38+
.output()
39+
.unwrap();
40+
assert!(
41+
model_output.status.success(),
42+
"workspace unset model failed:\n{}",
43+
combined_output(&model_output)
44+
);
45+
46+
let content = fs::read_to_string(&config_path).unwrap();
47+
assert!(!content.contains("[model]"));
48+
assert!(!content.contains("default ="));
49+
assert!(content.contains("keep_me = \"stays\""));
50+
assert!(content.contains("[sandbox]"));
51+
52+
let top_level_output = Command::new(env!("CARGO_BIN_EXE_Cortex"))
53+
.args(["workspace", "unset", "remove_me"])
54+
.current_dir(repo.path())
55+
.output()
56+
.unwrap();
57+
assert!(
58+
top_level_output.status.success(),
59+
"workspace unset remove_me failed:\n{}",
60+
combined_output(&top_level_output)
61+
);
62+
63+
let content = fs::read_to_string(&config_path).unwrap();
64+
assert!(!content.contains("remove_me"));
65+
assert!(content.contains("keep_me = \"stays\""));
66+
}
67+
68+
#[test]
69+
fn workspace_unset_reports_missing_key() {
70+
let repo = tempdir().unwrap();
71+
fs::create_dir(repo.path().join(".git")).unwrap();
72+
let cortex_dir = repo.path().join(".cortex");
73+
fs::create_dir_all(&cortex_dir).unwrap();
74+
fs::write(cortex_dir.join("config.toml"), "keep_me = \"stays\"\n").unwrap();
75+
76+
let output = Command::new(env!("CARGO_BIN_EXE_Cortex"))
77+
.args(["workspace", "unset", "missing"])
78+
.current_dir(repo.path())
79+
.output()
80+
.unwrap();
81+
82+
assert!(
83+
!output.status.success(),
84+
"workspace unset missing unexpectedly succeeded:\n{}",
85+
combined_output(&output)
86+
);
87+
assert!(combined_output(&output).contains("Key 'missing' not found"));
88+
}

0 commit comments

Comments
 (0)