Skip to content

Commit fb82725

Browse files
committed
feat: add optional JSON output for skill installation
1 parent 3db0297 commit fb82725

3 files changed

Lines changed: 79 additions & 41 deletions

File tree

src/cli/handlers.rs

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -826,32 +826,34 @@ pub fn run_install_skill(args: &[String]) -> Result<(), String> {
826826
use serde_json::json;
827827

828828
let global = args.iter().any(|arg| arg == "--global");
829+
let json_output = crate::cli::has_flag(args, "--json");
829830

830-
// Extract source argument (first non-flag argument, or None for default)
831831
let source = args
832832
.iter()
833833
.find(|arg| !arg.starts_with("--"))
834834
.map(|s| s.as_str());
835835

836-
// Install skill
837836
let installed = skills::install_skill(source, global).map_err(|e| e.to_string())?;
838837

839-
// Build JSON response
840-
let response = json!({
841-
"schemaVersion": "1.0",
842-
"type": "skill-installation",
843-
"ok": true,
844-
"skills": [
845-
{
846-
"name": installed.name,
847-
"path": installed.path,
848-
"source_type": installed.source_type,
849-
}
850-
]
851-
});
838+
if json_output {
839+
let response = json!({
840+
"schemaVersion": "1.0",
841+
"type": "skill-installation",
842+
"ok": true,
843+
"skills": [
844+
{
845+
"name": installed.name,
846+
"path": installed.path,
847+
"source_type": installed.source_type,
848+
}
849+
]
850+
});
852851

853-
// Output JSON to stdout
854-
println!("{}", serde_json::to_string_pretty(&response).unwrap());
852+
println!("{}", serde_json::to_string_pretty(&response).unwrap());
853+
} else {
854+
println!("Installed 1 {}skill:", if global { "global " } else { "" });
855+
println!("- {} -> {}", installed.name, installed.path);
856+
}
855857

856858
Ok(())
857859
}

src/cli/introspection.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1292,6 +1292,13 @@ pub fn get_command_definitions() -> Vec<CommandDef> {
12921292
description: "Install to ~/.agents instead of ./.agents".to_string(),
12931293
default: Some("false".to_string()),
12941294
},
1295+
FlagDef {
1296+
name: "--json".to_string(),
1297+
flag_type: "boolean".to_string(),
1298+
required: false,
1299+
description: "Output installation result as JSON".to_string(),
1300+
default: Some("false".to_string()),
1301+
},
12951302
],
12961303
examples: vec![
12971304
ExampleDef {
@@ -1306,6 +1313,10 @@ pub fn get_command_definitions() -> Vec<CommandDef> {
13061313
description: "Install globally to ~/.agents".to_string(),
13071314
command: "slack-rs install-skills --global".to_string(),
13081315
},
1316+
ExampleDef {
1317+
description: "Output installation result as JSON".to_string(),
1318+
command: "slack-rs install-skills --json".to_string(),
1319+
},
13091320
],
13101321
exit_codes: vec![
13111322
ExitCodeDef {

tests/skill_installation_integration.rs

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ fn run_slack_rs(args: &[&str]) -> (i32, String, String) {
2222

2323
#[test]
2424
#[cfg(not(target_os = "windows"))]
25-
fn install_skill_outputs_required_json_fields() {
26-
// Override HOME to use temp directory for this test
25+
fn install_skill_outputs_human_readable_success_message() {
2726
let temp_dir = TempDir::new().unwrap();
2827
let temp_home = temp_dir.path();
2928

@@ -43,13 +42,43 @@ fn install_skill_outputs_required_json_fields() {
4342
})
4443
.unwrap();
4544

46-
// Should succeed
45+
assert_eq!(exit_code, 0, "Command failed: {}", stderr);
46+
assert!(
47+
stdout.contains("Installed 1 skill:"),
48+
"stdout should show success summary, got: {stdout}"
49+
);
50+
assert!(
51+
stdout.contains("slack-rs ->"),
52+
"stdout should show installed skill path, got: {stdout}"
53+
);
54+
}
55+
56+
#[test]
57+
#[cfg(not(target_os = "windows"))]
58+
fn install_skill_json_outputs_required_fields() {
59+
let temp_dir = TempDir::new().unwrap();
60+
let temp_home = temp_dir.path();
61+
62+
let mut cmd = Command::new(env!("CARGO_BIN_EXE_slack-rs"));
63+
cmd.args(["install-skills", "--json"])
64+
.env("HOME", temp_home)
65+
.current_dir(temp_home);
66+
67+
let (exit_code, stdout, stderr) = cmd
68+
.output()
69+
.map(|output| {
70+
(
71+
output.status.code().unwrap_or(-1),
72+
String::from_utf8_lossy(&output.stdout).to_string(),
73+
String::from_utf8_lossy(&output.stderr).to_string(),
74+
)
75+
})
76+
.unwrap();
77+
4778
assert_eq!(exit_code, 0, "Command failed: {}", stderr);
4879

49-
// Parse JSON output
5080
let json: Value = serde_json::from_str(&stdout).expect("Invalid JSON output");
5181

52-
// Verify required fields
5382
assert_eq!(
5483
json["schemaVersion"].as_str(),
5584
Some("1.0"),
@@ -62,7 +91,6 @@ fn install_skill_outputs_required_json_fields() {
6291
);
6392
assert_eq!(json["ok"].as_bool(), Some(true), "Missing or incorrect ok");
6493

65-
// Verify skills array
6694
let skills = json["skills"]
6795
.as_array()
6896
.expect("skills should be an array");
@@ -76,7 +104,6 @@ fn install_skill_outputs_required_json_fields() {
76104
"skill.source_type should be a string"
77105
);
78106

79-
// Verify defaults for self source
80107
assert_eq!(
81108
skill["name"].as_str(),
82109
Some("slack-rs"),
@@ -252,12 +279,14 @@ fn install_skill_with_local_source() {
252279
);
253280

254281
// Parse JSON
255-
let json: Value = serde_json::from_str(&stdout).expect("Invalid JSON output");
256-
assert_eq!(json["ok"].as_bool(), Some(true));
257-
258-
let skills = json["skills"].as_array().unwrap();
259-
assert_eq!(skills.len(), 1);
260-
assert_eq!(skills[0]["source_type"].as_str(), Some("local"));
282+
assert!(
283+
stdout.contains("Installed 1 skill:"),
284+
"stdout should show success summary, got: {stdout}"
285+
);
286+
assert!(
287+
stdout.contains("test skill") || stdout.contains(" -> "),
288+
"stdout should include installed item, got: {stdout}"
289+
);
261290
}
262291

263292
#[test]
@@ -283,19 +312,15 @@ fn install_skill_global_uses_home_agents_dir() {
283312

284313
assert_eq!(exit_code, 0, "Global install should succeed: {}", stderr);
285314

286-
let json: Value = serde_json::from_str(&stdout).expect("Invalid JSON output");
287-
let skill = &json["skills"].as_array().unwrap()[0];
288-
let path = skill["path"].as_str().unwrap();
289315
let expected_prefix = temp_home.path().join(".agents").join("skills");
316+
let expected_prefix_str = expected_prefix.display().to_string();
290317

291-
// Normalize paths for comparison (handle both Unix and Windows separators)
292-
let normalized_path = std::path::Path::new(path);
293-
let normalized_expected = expected_prefix.as_path();
294-
295318
assert!(
296-
normalized_path.starts_with(normalized_expected),
297-
"Global install should use ~/.agents/skills path, got: {} (expected prefix: {})",
298-
path,
299-
expected_prefix.display()
319+
stdout.contains("Installed 1 global skill:"),
320+
"stdout should indicate global install, got: {stdout}"
321+
);
322+
assert!(
323+
stdout.contains(&expected_prefix_str),
324+
"stdout should include global install path, got: {stdout}"
300325
);
301326
}

0 commit comments

Comments
 (0)