mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-06 16:14:49 +08:00
Compare commits
1 Commits
fix/p011-c
...
fix/p013-o
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00b557c8b7 |
@@ -1954,46 +1954,22 @@ pub struct PluginsCommandResult {
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum DefinitionSource {
|
||||
ProjectClaw,
|
||||
ProjectCodex,
|
||||
ProjectClaude,
|
||||
UserClawConfigHome,
|
||||
UserCodexHome,
|
||||
UserClaw,
|
||||
UserCodex,
|
||||
UserClaude,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum DefinitionScope {
|
||||
Project,
|
||||
UserConfigHome,
|
||||
UserHome,
|
||||
}
|
||||
|
||||
impl DefinitionScope {
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Project => "Project (.claw)",
|
||||
Self::UserConfigHome => "User ($CLAW_CONFIG_HOME)",
|
||||
Self::UserHome => "User (~/.claw)",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DefinitionSource {
|
||||
fn report_scope(self) -> DefinitionScope {
|
||||
match self {
|
||||
Self::ProjectClaw | Self::ProjectCodex | Self::ProjectClaude => {
|
||||
DefinitionScope::Project
|
||||
}
|
||||
Self::UserClawConfigHome | Self::UserCodexHome => DefinitionScope::UserConfigHome,
|
||||
Self::UserClaw | Self::UserCodex | Self::UserClaude => DefinitionScope::UserHome,
|
||||
}
|
||||
}
|
||||
|
||||
fn label(self) -> &'static str {
|
||||
self.report_scope().label()
|
||||
match self {
|
||||
Self::ProjectCodex => "Project (.codex)",
|
||||
Self::ProjectClaude => "Project (.claude)",
|
||||
Self::UserCodexHome => "User ($CODEX_HOME)",
|
||||
Self::UserCodex => "User (~/.codex)",
|
||||
Self::UserClaude => "User (~/.claude)",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2326,11 +2302,6 @@ fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, P
|
||||
let mut roots = Vec::new();
|
||||
|
||||
for ancestor in cwd.ancestors() {
|
||||
push_unique_root(
|
||||
&mut roots,
|
||||
DefinitionSource::ProjectClaw,
|
||||
ancestor.join(".claw").join(leaf),
|
||||
);
|
||||
push_unique_root(
|
||||
&mut roots,
|
||||
DefinitionSource::ProjectCodex,
|
||||
@@ -2343,14 +2314,6 @@ fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, P
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(claw_config_home) = env::var("CLAW_CONFIG_HOME") {
|
||||
push_unique_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserClawConfigHome,
|
||||
PathBuf::from(claw_config_home).join(leaf),
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(codex_home) = env::var("CODEX_HOME") {
|
||||
push_unique_root(
|
||||
&mut roots,
|
||||
@@ -2361,11 +2324,6 @@ fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, P
|
||||
|
||||
if let Some(home) = env::var_os("HOME") {
|
||||
let home = PathBuf::from(home);
|
||||
push_unique_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserClaw,
|
||||
home.join(".claw").join(leaf),
|
||||
);
|
||||
push_unique_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserCodex,
|
||||
@@ -2385,12 +2343,6 @@ fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
|
||||
let mut roots = Vec::new();
|
||||
|
||||
for ancestor in cwd.ancestors() {
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::ProjectClaw,
|
||||
ancestor.join(".claw").join("skills"),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::ProjectCodex,
|
||||
@@ -2403,12 +2355,6 @@ fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
|
||||
ancestor.join(".claude").join("skills"),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::ProjectClaw,
|
||||
ancestor.join(".claw").join("commands"),
|
||||
SkillOrigin::LegacyCommandsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::ProjectCodex,
|
||||
@@ -2423,22 +2369,6 @@ fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(claw_config_home) = env::var("CLAW_CONFIG_HOME") {
|
||||
let claw_config_home = PathBuf::from(claw_config_home);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserClawConfigHome,
|
||||
claw_config_home.join("skills"),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserClawConfigHome,
|
||||
claw_config_home.join("commands"),
|
||||
SkillOrigin::LegacyCommandsDir,
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(codex_home) = env::var("CODEX_HOME") {
|
||||
let codex_home = PathBuf::from(codex_home);
|
||||
push_unique_skill_root(
|
||||
@@ -2457,18 +2387,6 @@ fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
|
||||
|
||||
if let Some(home) = env::var_os("HOME") {
|
||||
let home = PathBuf::from(home);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserClaw,
|
||||
home.join(".claw").join("skills"),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserClaw,
|
||||
home.join(".claw").join("commands"),
|
||||
SkillOrigin::LegacyCommandsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserCodex,
|
||||
@@ -2549,18 +2467,15 @@ fn install_skill_into(
|
||||
}
|
||||
|
||||
fn default_skill_install_root() -> std::io::Result<PathBuf> {
|
||||
if let Ok(claw_config_home) = env::var("CLAW_CONFIG_HOME") {
|
||||
return Ok(PathBuf::from(claw_config_home).join("skills"));
|
||||
}
|
||||
if let Ok(codex_home) = env::var("CODEX_HOME") {
|
||||
return Ok(PathBuf::from(codex_home).join("skills"));
|
||||
}
|
||||
if let Some(home) = env::var_os("HOME") {
|
||||
return Ok(PathBuf::from(home).join(".claw").join("skills"));
|
||||
return Ok(PathBuf::from(home).join(".codex").join("skills"));
|
||||
}
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"unable to resolve a skills install root; set CLAW_CONFIG_HOME or HOME",
|
||||
"unable to resolve a skills install root; set CODEX_HOME or HOME",
|
||||
))
|
||||
}
|
||||
|
||||
@@ -2926,20 +2841,22 @@ fn render_agents_report(agents: &[AgentSummary]) -> String {
|
||||
String::new(),
|
||||
];
|
||||
|
||||
for scope in [
|
||||
DefinitionScope::Project,
|
||||
DefinitionScope::UserConfigHome,
|
||||
DefinitionScope::UserHome,
|
||||
for source in [
|
||||
DefinitionSource::ProjectCodex,
|
||||
DefinitionSource::ProjectClaude,
|
||||
DefinitionSource::UserCodexHome,
|
||||
DefinitionSource::UserCodex,
|
||||
DefinitionSource::UserClaude,
|
||||
] {
|
||||
let group = agents
|
||||
.iter()
|
||||
.filter(|agent| agent.source.report_scope() == scope)
|
||||
.filter(|agent| agent.source == source)
|
||||
.collect::<Vec<_>>();
|
||||
if group.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
lines.push(format!("{}:", scope.label()));
|
||||
lines.push(format!("{}:", source.label()));
|
||||
for agent in group {
|
||||
let detail = agent_detail(agent);
|
||||
match agent.shadowed_by {
|
||||
@@ -2982,20 +2899,22 @@ fn render_skills_report(skills: &[SkillSummary]) -> String {
|
||||
String::new(),
|
||||
];
|
||||
|
||||
for scope in [
|
||||
DefinitionScope::Project,
|
||||
DefinitionScope::UserConfigHome,
|
||||
DefinitionScope::UserHome,
|
||||
for source in [
|
||||
DefinitionSource::ProjectCodex,
|
||||
DefinitionSource::ProjectClaude,
|
||||
DefinitionSource::UserCodexHome,
|
||||
DefinitionSource::UserCodex,
|
||||
DefinitionSource::UserClaude,
|
||||
] {
|
||||
let group = skills
|
||||
.iter()
|
||||
.filter(|skill| skill.source.report_scope() == scope)
|
||||
.filter(|skill| skill.source == source)
|
||||
.collect::<Vec<_>>();
|
||||
if group.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
lines.push(format!("{}:", scope.label()));
|
||||
lines.push(format!("{}:", source.label()));
|
||||
for skill in group {
|
||||
let mut parts = vec![skill.name.clone()];
|
||||
if let Some(description) = &skill.description {
|
||||
@@ -3161,7 +3080,7 @@ fn render_agents_usage(unexpected: Option<&str>) -> String {
|
||||
"Agents".to_string(),
|
||||
" Usage /agents [list|help]".to_string(),
|
||||
" Direct CLI claw agents".to_string(),
|
||||
" Sources .claw/agents, ~/.claw/agents, $CLAW_CONFIG_HOME/agents".to_string(),
|
||||
" Sources .codex/agents, .claude/agents, $CODEX_HOME/agents".to_string(),
|
||||
];
|
||||
if let Some(args) = unexpected {
|
||||
lines.push(format!(" Unexpected {args}"));
|
||||
@@ -3174,8 +3093,8 @@ fn render_skills_usage(unexpected: Option<&str>) -> String {
|
||||
"Skills".to_string(),
|
||||
" Usage /skills [list|install <path>|help]".to_string(),
|
||||
" Direct CLI claw skills [list|install <path>|help]".to_string(),
|
||||
" Install root $CLAW_CONFIG_HOME/skills or ~/.claw/skills".to_string(),
|
||||
" Sources .claw/skills, ~/.claw/skills, legacy /commands".to_string(),
|
||||
" Install root $CODEX_HOME/skills or ~/.codex/skills".to_string(),
|
||||
" Sources .codex/skills, .claude/skills, legacy /commands".to_string(),
|
||||
];
|
||||
if let Some(args) = unexpected {
|
||||
lines.push(format!(" Unexpected {args}"));
|
||||
@@ -4014,7 +3933,7 @@ mod tests {
|
||||
let workspace = temp_dir("agents-workspace");
|
||||
let project_agents = workspace.join(".codex").join("agents");
|
||||
let user_home = temp_dir("agents-home");
|
||||
let user_agents = user_home.join(".claude").join("agents");
|
||||
let user_agents = user_home.join(".codex").join("agents");
|
||||
|
||||
write_agent(
|
||||
&project_agents,
|
||||
@@ -4047,10 +3966,10 @@ mod tests {
|
||||
|
||||
assert!(report.contains("Agents"));
|
||||
assert!(report.contains("2 active agents"));
|
||||
assert!(report.contains("Project (.claw):"));
|
||||
assert!(report.contains("Project (.codex):"));
|
||||
assert!(report.contains("planner · Project planner · gpt-5.4 · medium"));
|
||||
assert!(report.contains("User (~/.claw):"));
|
||||
assert!(report.contains("(shadowed by Project (.claw)) planner · User planner"));
|
||||
assert!(report.contains("User (~/.codex):"));
|
||||
assert!(report.contains("(shadowed by Project (.codex)) planner · User planner"));
|
||||
assert!(report.contains("verifier · Verification agent · gpt-5.4-mini · high"));
|
||||
|
||||
let _ = fs::remove_dir_all(workspace);
|
||||
@@ -4092,11 +4011,12 @@ mod tests {
|
||||
|
||||
assert!(report.contains("Skills"));
|
||||
assert!(report.contains("3 available skills"));
|
||||
assert!(report.contains("Project (.claw):"));
|
||||
assert!(report.contains("Project (.codex):"));
|
||||
assert!(report.contains("plan · Project planning guidance"));
|
||||
assert!(report.contains("Project (.claude):"));
|
||||
assert!(report.contains("deploy · Legacy deployment guidance · legacy /commands"));
|
||||
assert!(report.contains("User (~/.claw):"));
|
||||
assert!(report.contains("(shadowed by Project (.claw)) plan · User planning guidance"));
|
||||
assert!(report.contains("User (~/.codex):"));
|
||||
assert!(report.contains("(shadowed by Project (.codex)) plan · User planning guidance"));
|
||||
assert!(report.contains("help · Help guidance"));
|
||||
|
||||
let _ = fs::remove_dir_all(workspace);
|
||||
@@ -4111,8 +4031,6 @@ mod tests {
|
||||
super::handle_agents_slash_command(Some("help"), &cwd).expect("agents help");
|
||||
assert!(agents_help.contains("Usage /agents [list|help]"));
|
||||
assert!(agents_help.contains("Direct CLI claw agents"));
|
||||
assert!(agents_help
|
||||
.contains("Sources .claw/agents, ~/.claw/agents, $CLAW_CONFIG_HOME/agents"));
|
||||
|
||||
let agents_unexpected =
|
||||
super::handle_agents_slash_command(Some("show planner"), &cwd).expect("agents usage");
|
||||
@@ -4121,7 +4039,7 @@ mod tests {
|
||||
let skills_help =
|
||||
super::handle_skills_slash_command(Some("--help"), &cwd).expect("skills help");
|
||||
assert!(skills_help.contains("Usage /skills [list|install <path>|help]"));
|
||||
assert!(skills_help.contains("Install root $CLAW_CONFIG_HOME/skills or ~/.claw/skills"));
|
||||
assert!(skills_help.contains("Install root $CODEX_HOME/skills or ~/.codex/skills"));
|
||||
assert!(skills_help.contains("legacy /commands"));
|
||||
|
||||
let skills_unexpected =
|
||||
@@ -4295,7 +4213,7 @@ mod tests {
|
||||
let listed = render_skills_report(
|
||||
&load_skills_from_roots(&roots).expect("installed skills should load"),
|
||||
);
|
||||
assert!(listed.contains("User ($CLAW_CONFIG_HOME):"));
|
||||
assert!(listed.contains("User ($CODEX_HOME):"));
|
||||
assert!(listed.contains("help · Helpful skill"));
|
||||
|
||||
let _ = fs::remove_dir_all(workspace);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const STARTER_CLAW_JSON: &str = concat!(
|
||||
const STARTER_CLAUDE_JSON: &str = concat!(
|
||||
"{\n",
|
||||
" \"permissions\": {\n",
|
||||
" \"defaultMode\": \"dontAsk\"\n",
|
||||
@@ -9,7 +9,7 @@ const STARTER_CLAW_JSON: &str = concat!(
|
||||
"}\n",
|
||||
);
|
||||
const GITIGNORE_COMMENT: &str = "# Claw Code local artifacts";
|
||||
const GITIGNORE_ENTRIES: [&str; 2] = [".claw/settings.local.json", ".claw/sessions/"];
|
||||
const GITIGNORE_ENTRIES: [&str; 2] = [".claude/settings.local.json", ".claude/sessions/"];
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum InitStatus {
|
||||
@@ -80,16 +80,16 @@ struct RepoDetection {
|
||||
pub(crate) fn initialize_repo(cwd: &Path) -> Result<InitReport, Box<dyn std::error::Error>> {
|
||||
let mut artifacts = Vec::new();
|
||||
|
||||
let claw_dir = cwd.join(".claw");
|
||||
let claude_dir = cwd.join(".claude");
|
||||
artifacts.push(InitArtifact {
|
||||
name: ".claw/",
|
||||
status: ensure_dir(&claw_dir)?,
|
||||
name: ".claude/",
|
||||
status: ensure_dir(&claude_dir)?,
|
||||
});
|
||||
|
||||
let claw_json = cwd.join(".claw.json");
|
||||
let claude_json = cwd.join(".claude.json");
|
||||
artifacts.push(InitArtifact {
|
||||
name: ".claw.json",
|
||||
status: write_file_if_missing(&claw_json, STARTER_CLAW_JSON)?,
|
||||
name: ".claude.json",
|
||||
status: write_file_if_missing(&claude_json, STARTER_CLAUDE_JSON)?,
|
||||
});
|
||||
|
||||
let gitignore = cwd.join(".gitignore");
|
||||
@@ -209,7 +209,7 @@ pub(crate) fn render_init_claude_md(cwd: &Path) -> String {
|
||||
|
||||
lines.push("## Working agreement".to_string());
|
||||
lines.push("- Prefer small, reviewable changes and keep generated bootstrap files aligned with actual repo workflows.".to_string());
|
||||
lines.push("- Keep shared defaults in `.claw.json`; reserve `.claw/settings.local.json` for machine-local overrides.".to_string());
|
||||
lines.push("- Keep shared defaults in `.claude.json`; reserve `.claude/settings.local.json` for machine-local overrides.".to_string());
|
||||
lines.push("- Do not overwrite existing `CLAUDE.md` content automatically; update it intentionally when repo workflows change.".to_string());
|
||||
lines.push(String::new());
|
||||
|
||||
@@ -354,16 +354,15 @@ mod tests {
|
||||
|
||||
let report = initialize_repo(&root).expect("init should succeed");
|
||||
let rendered = report.render();
|
||||
assert!(rendered.contains(".claw/"));
|
||||
assert!(rendered.contains(".claw.json"));
|
||||
assert!(rendered.contains("created"));
|
||||
assert!(rendered.contains(".claude/ created"));
|
||||
assert!(rendered.contains(".claude.json created"));
|
||||
assert!(rendered.contains(".gitignore created"));
|
||||
assert!(rendered.contains("CLAUDE.md created"));
|
||||
assert!(root.join(".claw").is_dir());
|
||||
assert!(root.join(".claw.json").is_file());
|
||||
assert!(root.join(".claude").is_dir());
|
||||
assert!(root.join(".claude.json").is_file());
|
||||
assert!(root.join("CLAUDE.md").is_file());
|
||||
assert_eq!(
|
||||
fs::read_to_string(root.join(".claw.json")).expect("read claw json"),
|
||||
fs::read_to_string(root.join(".claude.json")).expect("read claude json"),
|
||||
concat!(
|
||||
"{\n",
|
||||
" \"permissions\": {\n",
|
||||
@@ -373,8 +372,8 @@ mod tests {
|
||||
)
|
||||
);
|
||||
let gitignore = fs::read_to_string(root.join(".gitignore")).expect("read gitignore");
|
||||
assert!(gitignore.contains(".claw/settings.local.json"));
|
||||
assert!(gitignore.contains(".claw/sessions/"));
|
||||
assert!(gitignore.contains(".claude/settings.local.json"));
|
||||
assert!(gitignore.contains(".claude/sessions/"));
|
||||
let claude_md = fs::read_to_string(root.join("CLAUDE.md")).expect("read claude md");
|
||||
assert!(claude_md.contains("Languages: Rust."));
|
||||
assert!(claude_md.contains("cargo clippy --workspace --all-targets -- -D warnings"));
|
||||
@@ -387,7 +386,8 @@ mod tests {
|
||||
let root = temp_dir();
|
||||
fs::create_dir_all(&root).expect("create root");
|
||||
fs::write(root.join("CLAUDE.md"), "custom guidance\n").expect("write existing claude md");
|
||||
fs::write(root.join(".gitignore"), ".claw/settings.local.json\n").expect("write gitignore");
|
||||
fs::write(root.join(".gitignore"), ".claude/settings.local.json\n")
|
||||
.expect("write gitignore");
|
||||
|
||||
let first = initialize_repo(&root).expect("first init should succeed");
|
||||
assert!(first
|
||||
@@ -395,9 +395,8 @@ mod tests {
|
||||
.contains("CLAUDE.md skipped (already exists)"));
|
||||
let second = initialize_repo(&root).expect("second init should succeed");
|
||||
let second_rendered = second.render();
|
||||
assert!(second_rendered.contains(".claw/"));
|
||||
assert!(second_rendered.contains(".claw.json"));
|
||||
assert!(second_rendered.contains("skipped (already exists)"));
|
||||
assert!(second_rendered.contains(".claude/ skipped (already exists)"));
|
||||
assert!(second_rendered.contains(".claude.json skipped (already exists)"));
|
||||
assert!(second_rendered.contains(".gitignore skipped (already exists)"));
|
||||
assert!(second_rendered.contains("CLAUDE.md skipped (already exists)"));
|
||||
assert_eq!(
|
||||
@@ -405,8 +404,8 @@ mod tests {
|
||||
"custom guidance\n"
|
||||
);
|
||||
let gitignore = fs::read_to_string(root.join(".gitignore")).expect("read gitignore");
|
||||
assert_eq!(gitignore.matches(".claw/settings.local.json").count(), 1);
|
||||
assert_eq!(gitignore.matches(".claw/sessions/").count(), 1);
|
||||
assert_eq!(gitignore.matches(".claude/settings.local.json").count(), 1);
|
||||
assert_eq!(gitignore.matches(".claude/sessions/").count(), 1);
|
||||
|
||||
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||
}
|
||||
|
||||
@@ -107,13 +107,26 @@ Run `claw --help` for usage."
|
||||
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<String> = env::args().skip(1).collect();
|
||||
match parse_args(&args)? {
|
||||
CliAction::DumpManifests => dump_manifests(),
|
||||
CliAction::BootstrapPlan => print_bootstrap_plan(),
|
||||
CliAction::Agents { args } => LiveCli::print_agents(args.as_deref())?,
|
||||
CliAction::Mcp { args } => LiveCli::print_mcp(args.as_deref())?,
|
||||
CliAction::Skills { args } => LiveCli::print_skills(args.as_deref())?,
|
||||
CliAction::PrintSystemPrompt { cwd, date } => print_system_prompt(cwd, date),
|
||||
CliAction::Version => print_version(),
|
||||
CliAction::DumpManifests { output_format } => dump_manifests(output_format),
|
||||
CliAction::BootstrapPlan { output_format } => print_bootstrap_plan(output_format)?,
|
||||
CliAction::Agents {
|
||||
args,
|
||||
output_format,
|
||||
} => LiveCli::print_agents(args.as_deref(), output_format)?,
|
||||
CliAction::Mcp {
|
||||
args,
|
||||
output_format,
|
||||
} => LiveCli::print_mcp(args.as_deref(), output_format)?,
|
||||
CliAction::Skills {
|
||||
args,
|
||||
output_format,
|
||||
} => LiveCli::print_skills(args.as_deref(), output_format)?,
|
||||
CliAction::PrintSystemPrompt {
|
||||
cwd,
|
||||
date,
|
||||
output_format,
|
||||
} => print_system_prompt(cwd, date, output_format)?,
|
||||
CliAction::Version { output_format } => print_version(output_format)?,
|
||||
CliAction::ResumeSession {
|
||||
session_path,
|
||||
commands,
|
||||
@@ -133,37 +146,47 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
permission_mode,
|
||||
} => LiveCli::new(model, true, allowed_tools, permission_mode)?
|
||||
.run_turn_with_output(&prompt, output_format)?,
|
||||
CliAction::Login => run_login()?,
|
||||
CliAction::Logout => run_logout()?,
|
||||
CliAction::Init => run_init()?,
|
||||
CliAction::Login { output_format } => run_login(output_format)?,
|
||||
CliAction::Logout { output_format } => run_logout(output_format)?,
|
||||
CliAction::Init { output_format } => run_init(output_format)?,
|
||||
CliAction::Repl {
|
||||
model,
|
||||
allowed_tools,
|
||||
permission_mode,
|
||||
} => run_repl(model, allowed_tools, permission_mode)?,
|
||||
CliAction::Help => print_help(),
|
||||
CliAction::Help { output_format } => print_help(output_format)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum CliAction {
|
||||
DumpManifests,
|
||||
BootstrapPlan,
|
||||
DumpManifests {
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
BootstrapPlan {
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
Agents {
|
||||
args: Option<String>,
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
Mcp {
|
||||
args: Option<String>,
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
Skills {
|
||||
args: Option<String>,
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
PrintSystemPrompt {
|
||||
cwd: PathBuf,
|
||||
date: String,
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
Version {
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
Version,
|
||||
ResumeSession {
|
||||
session_path: PathBuf,
|
||||
commands: Vec<String>,
|
||||
@@ -184,16 +207,24 @@ enum CliAction {
|
||||
allowed_tools: Option<AllowedToolSet>,
|
||||
permission_mode: PermissionMode,
|
||||
},
|
||||
Login,
|
||||
Logout,
|
||||
Init,
|
||||
Login {
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
Logout {
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
Init {
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
Repl {
|
||||
model: String,
|
||||
allowed_tools: Option<AllowedToolSet>,
|
||||
permission_mode: PermissionMode,
|
||||
},
|
||||
// prompt-mode formatting is only supported for non-interactive runs
|
||||
Help,
|
||||
Help {
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -327,11 +358,11 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
}
|
||||
|
||||
if wants_help {
|
||||
return Ok(CliAction::Help);
|
||||
return Ok(CliAction::Help { output_format });
|
||||
}
|
||||
|
||||
if wants_version {
|
||||
return Ok(CliAction::Version);
|
||||
return Ok(CliAction::Version { output_format });
|
||||
}
|
||||
|
||||
let allowed_tools = normalize_allowed_tools(&allowed_tool_values)?;
|
||||
@@ -356,21 +387,24 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
let permission_mode = permission_mode_override.unwrap_or_else(default_permission_mode);
|
||||
|
||||
match rest[0].as_str() {
|
||||
"dump-manifests" => Ok(CliAction::DumpManifests),
|
||||
"bootstrap-plan" => Ok(CliAction::BootstrapPlan),
|
||||
"dump-manifests" => Ok(CliAction::DumpManifests { output_format }),
|
||||
"bootstrap-plan" => Ok(CliAction::BootstrapPlan { output_format }),
|
||||
"agents" => Ok(CliAction::Agents {
|
||||
args: join_optional_args(&rest[1..]),
|
||||
output_format,
|
||||
}),
|
||||
"mcp" => Ok(CliAction::Mcp {
|
||||
args: join_optional_args(&rest[1..]),
|
||||
output_format,
|
||||
}),
|
||||
"skills" => Ok(CliAction::Skills {
|
||||
args: join_optional_args(&rest[1..]),
|
||||
output_format,
|
||||
}),
|
||||
"system-prompt" => parse_system_prompt_args(&rest[1..]),
|
||||
"login" => Ok(CliAction::Login),
|
||||
"logout" => Ok(CliAction::Logout),
|
||||
"init" => Ok(CliAction::Init),
|
||||
"system-prompt" => parse_system_prompt_args(&rest[1..], output_format),
|
||||
"login" => Ok(CliAction::Login { output_format }),
|
||||
"logout" => Ok(CliAction::Logout { output_format }),
|
||||
"init" => Ok(CliAction::Init { output_format }),
|
||||
"prompt" => {
|
||||
let prompt = rest[1..].join(" ");
|
||||
if prompt.trim().is_empty() {
|
||||
@@ -384,7 +418,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
permission_mode,
|
||||
})
|
||||
}
|
||||
other if other.starts_with('/') => parse_direct_slash_cli_action(&rest),
|
||||
other if other.starts_with('/') => parse_direct_slash_cli_action(&rest, output_format),
|
||||
_other => Ok(CliAction::Prompt {
|
||||
prompt: rest.join(" "),
|
||||
model,
|
||||
@@ -406,8 +440,8 @@ fn parse_single_word_command_alias(
|
||||
}
|
||||
|
||||
match rest[0].as_str() {
|
||||
"help" => Some(Ok(CliAction::Help)),
|
||||
"version" => Some(Ok(CliAction::Version)),
|
||||
"help" => Some(Ok(CliAction::Help { output_format })),
|
||||
"version" => Some(Ok(CliAction::Version { output_format })),
|
||||
"status" => Some(Ok(CliAction::Status {
|
||||
model: model.to_string(),
|
||||
permission_mode: permission_mode_override.unwrap_or_else(default_permission_mode),
|
||||
@@ -455,11 +489,17 @@ fn join_optional_args(args: &[String]) -> Option<String> {
|
||||
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
||||
}
|
||||
|
||||
fn parse_direct_slash_cli_action(rest: &[String]) -> Result<CliAction, String> {
|
||||
fn parse_direct_slash_cli_action(
|
||||
rest: &[String],
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<CliAction, String> {
|
||||
let raw = rest.join(" ");
|
||||
match SlashCommand::parse(&raw) {
|
||||
Ok(Some(SlashCommand::Help)) => Ok(CliAction::Help),
|
||||
Ok(Some(SlashCommand::Agents { args })) => Ok(CliAction::Agents { args }),
|
||||
Ok(Some(SlashCommand::Help)) => Ok(CliAction::Help { output_format }),
|
||||
Ok(Some(SlashCommand::Agents { args })) => Ok(CliAction::Agents {
|
||||
args,
|
||||
output_format,
|
||||
}),
|
||||
Ok(Some(SlashCommand::Mcp { action, target })) => Ok(CliAction::Mcp {
|
||||
args: match (action, target) {
|
||||
(None, None) => None,
|
||||
@@ -467,8 +507,12 @@ fn parse_direct_slash_cli_action(rest: &[String]) -> Result<CliAction, String> {
|
||||
(Some(action), Some(target)) => Some(format!("{action} {target}")),
|
||||
(None, Some(target)) => Some(target),
|
||||
},
|
||||
output_format,
|
||||
}),
|
||||
Ok(Some(SlashCommand::Skills { args })) => Ok(CliAction::Skills {
|
||||
args,
|
||||
output_format,
|
||||
}),
|
||||
Ok(Some(SlashCommand::Skills { args })) => Ok(CliAction::Skills { args }),
|
||||
Ok(Some(SlashCommand::Unknown(name))) => Err(format_unknown_direct_slash_command(&name)),
|
||||
Ok(Some(command)) => Err({
|
||||
let _ = command;
|
||||
@@ -679,7 +723,10 @@ fn filter_tool_specs(
|
||||
tool_registry.definitions(allowed_tools)
|
||||
}
|
||||
|
||||
fn parse_system_prompt_args(args: &[String]) -> Result<CliAction, String> {
|
||||
fn parse_system_prompt_args(
|
||||
args: &[String],
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<CliAction, String> {
|
||||
let mut cwd = env::current_dir().map_err(|error| error.to_string())?;
|
||||
let mut date = DEFAULT_DATE.to_string();
|
||||
let mut index = 0;
|
||||
@@ -704,7 +751,11 @@ fn parse_system_prompt_args(args: &[String]) -> Result<CliAction, String> {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CliAction::PrintSystemPrompt { cwd, date })
|
||||
Ok(CliAction::PrintSystemPrompt {
|
||||
cwd,
|
||||
date,
|
||||
output_format,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_resume_args(args: &[String], output_format: CliOutputFormat) -> Result<CliAction, String> {
|
||||
@@ -775,25 +826,73 @@ fn looks_like_slash_command_token(token: &str) -> bool {
|
||||
.any(|spec| spec.name == name || spec.aliases.contains(&name))
|
||||
}
|
||||
|
||||
fn dump_manifests() {
|
||||
fn print_json_value(value: &Value) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("{}", serialize_json_output(value)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn json_error_payload(kind: &str, error: &dyn std::fmt::Display) -> Value {
|
||||
json!({
|
||||
"kind": kind,
|
||||
"error": error.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn render_help_report() -> io::Result<String> {
|
||||
let mut buffer = Vec::new();
|
||||
print_help_to(&mut buffer)?;
|
||||
String::from_utf8(buffer)
|
||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error.utf8_error()))
|
||||
}
|
||||
|
||||
fn dump_manifests(output_format: CliOutputFormat) {
|
||||
let workspace_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let paths = UpstreamPaths::from_workspace_dir(&workspace_dir);
|
||||
match extract_manifest(&paths) {
|
||||
Ok(manifest) => {
|
||||
println!("commands: {}", manifest.commands.entries().len());
|
||||
println!("tools: {}", manifest.tools.entries().len());
|
||||
println!("bootstrap phases: {}", manifest.bootstrap.phases().len());
|
||||
}
|
||||
Ok(manifest) => match output_format {
|
||||
CliOutputFormat::Text => {
|
||||
println!("commands: {}", manifest.commands.entries().len());
|
||||
println!("tools: {}", manifest.tools.entries().len());
|
||||
println!("bootstrap phases: {}", manifest.bootstrap.phases().len());
|
||||
}
|
||||
CliOutputFormat::Json => {
|
||||
let _ = print_json_value(&json!({
|
||||
"kind": "dump-manifests",
|
||||
"commands": manifest.commands.entries().len(),
|
||||
"tools": manifest.tools.entries().len(),
|
||||
"bootstrap_phases": manifest.bootstrap.phases().len(),
|
||||
}));
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
eprintln!("failed to extract manifests: {error}");
|
||||
match output_format {
|
||||
CliOutputFormat::Text => eprintln!("failed to extract manifests: {error}"),
|
||||
CliOutputFormat::Json => {
|
||||
let _ = print_json_value(&json_error_payload("dump-manifests", &error));
|
||||
}
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_bootstrap_plan() {
|
||||
for phase in runtime::BootstrapPlan::claude_code_default().phases() {
|
||||
println!("- {phase:?}");
|
||||
fn print_bootstrap_plan(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let phases = runtime::BootstrapPlan::claude_code_default()
|
||||
.phases()
|
||||
.iter()
|
||||
.map(|phase| format!("{phase:?}"))
|
||||
.collect::<Vec<_>>();
|
||||
match output_format {
|
||||
CliOutputFormat::Text => {
|
||||
for phase in &phases {
|
||||
println!("- {phase}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
CliOutputFormat::Json => print_json_value(&json!({
|
||||
"kind": "bootstrap-plan",
|
||||
"phases": phases,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,7 +911,7 @@ fn default_oauth_config() -> OAuthConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn run_login() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn run_login(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cwd = env::current_dir()?;
|
||||
let config = ConfigLoader::default_for(&cwd).load()?;
|
||||
let default_oauth = default_oauth_config();
|
||||
@@ -825,11 +924,20 @@ fn run_login() -> Result<(), Box<dyn std::error::Error>> {
|
||||
OAuthAuthorizationRequest::from_config(oauth, redirect_uri.clone(), state.clone(), &pkce)
|
||||
.build_url();
|
||||
|
||||
println!("Starting Claude OAuth login...");
|
||||
println!("Listening for callback on {redirect_uri}");
|
||||
if matches!(output_format, CliOutputFormat::Text) {
|
||||
println!("Starting Claude OAuth login...");
|
||||
println!("Listening for callback on {redirect_uri}");
|
||||
}
|
||||
if let Err(error) = open_browser(&authorize_url) {
|
||||
eprintln!("warning: failed to open browser automatically: {error}");
|
||||
println!("Open this URL manually:\n{authorize_url}");
|
||||
if matches!(output_format, CliOutputFormat::Text) {
|
||||
eprintln!("warning: failed to open browser automatically: {error}");
|
||||
println!("Open this URL manually:\n{authorize_url}");
|
||||
} else {
|
||||
return Err(io::Error::other(format!(
|
||||
"failed to open browser automatically: {error}; authorization URL: {authorize_url}"
|
||||
))
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
let callback = wait_for_oauth_callback(callback_port)?;
|
||||
@@ -850,8 +958,13 @@ fn run_login() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
let client = AnthropicClient::from_auth(AuthSource::None).with_base_url(api::read_base_url());
|
||||
let exchange_request =
|
||||
OAuthTokenExchangeRequest::from_config(oauth, code, state, pkce.verifier, redirect_uri);
|
||||
let exchange_request = OAuthTokenExchangeRequest::from_config(
|
||||
oauth,
|
||||
code,
|
||||
state,
|
||||
pkce.verifier,
|
||||
redirect_uri.clone(),
|
||||
);
|
||||
let runtime = tokio::runtime::Runtime::new()?;
|
||||
let token_set = runtime.block_on(client.exchange_oauth_code(oauth, &exchange_request))?;
|
||||
save_oauth_credentials(&runtime::OAuthTokenSet {
|
||||
@@ -860,14 +973,33 @@ fn run_login() -> Result<(), Box<dyn std::error::Error>> {
|
||||
expires_at: token_set.expires_at,
|
||||
scopes: token_set.scopes,
|
||||
})?;
|
||||
println!("Claude OAuth login complete.");
|
||||
Ok(())
|
||||
match output_format {
|
||||
CliOutputFormat::Text => {
|
||||
println!("Claude OAuth login complete.");
|
||||
Ok(())
|
||||
}
|
||||
CliOutputFormat::Json => print_json_value(&json!({
|
||||
"kind": "login",
|
||||
"message": "Claude OAuth login complete.",
|
||||
"authorize_url": authorize_url,
|
||||
"redirect_uri": redirect_uri,
|
||||
"callback_port": callback_port,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_logout() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn run_logout(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
clear_oauth_credentials()?;
|
||||
println!("Claude OAuth credentials cleared.");
|
||||
Ok(())
|
||||
match output_format {
|
||||
CliOutputFormat::Text => {
|
||||
println!("Claude OAuth credentials cleared.");
|
||||
Ok(())
|
||||
}
|
||||
CliOutputFormat::Json => print_json_value(&json!({
|
||||
"kind": "logout",
|
||||
"message": "Claude OAuth credentials cleared.",
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_browser(url: &str) -> io::Result<()> {
|
||||
@@ -924,18 +1056,49 @@ fn wait_for_oauth_callback(
|
||||
Ok(callback)
|
||||
}
|
||||
|
||||
fn print_system_prompt(cwd: PathBuf, date: String) {
|
||||
fn print_system_prompt(
|
||||
cwd: PathBuf,
|
||||
date: String,
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match load_system_prompt(cwd, date, env::consts::OS, "unknown") {
|
||||
Ok(sections) => println!("{}", sections.join("\n\n")),
|
||||
Ok(sections) => match output_format {
|
||||
CliOutputFormat::Text => {
|
||||
println!("{}", sections.join("\n\n"));
|
||||
Ok(())
|
||||
}
|
||||
CliOutputFormat::Json => print_json_value(&json!({
|
||||
"kind": "system-prompt",
|
||||
"message": sections.join("\n\n"),
|
||||
})),
|
||||
},
|
||||
Err(error) => {
|
||||
eprintln!("failed to build system prompt: {error}");
|
||||
match output_format {
|
||||
CliOutputFormat::Text => eprintln!("failed to build system prompt: {error}"),
|
||||
CliOutputFormat::Json => {
|
||||
print_json_value(&json_error_payload("system-prompt", &error))?;
|
||||
}
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_version() {
|
||||
println!("{}", render_version_report());
|
||||
fn print_version(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match output_format {
|
||||
CliOutputFormat::Text => {
|
||||
println!("{}", render_version_report());
|
||||
Ok(())
|
||||
}
|
||||
CliOutputFormat::Json => print_json_value(&json!({
|
||||
"kind": "version",
|
||||
"message": render_version_report(),
|
||||
"version": VERSION,
|
||||
"git_sha": GIT_SHA.unwrap_or("unknown"),
|
||||
"target": BUILD_TARGET.unwrap_or("unknown"),
|
||||
"build_date": DEFAULT_DATE,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn resume_session(session_path: &Path, commands: &[String], output_format: CliOutputFormat) {
|
||||
@@ -1006,7 +1169,7 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
|
||||
if let Some(message) = message {
|
||||
match output_format {
|
||||
CliOutputFormat::Text => {
|
||||
println!("{}", render_resume_text_output(&message))
|
||||
println!("{}", render_resume_text_output(&message));
|
||||
}
|
||||
CliOutputFormat::Json => json_outputs.push(message),
|
||||
}
|
||||
@@ -2467,7 +2630,7 @@ impl LiveCli {
|
||||
(Some(action), Some(target)) => Some(format!("{action} {target}")),
|
||||
(None, Some(target)) => Some(target.to_string()),
|
||||
};
|
||||
Self::print_mcp(args.as_deref())?;
|
||||
Self::print_mcp(args.as_deref(), CliOutputFormat::Text)?;
|
||||
false
|
||||
}
|
||||
SlashCommand::Memory => {
|
||||
@@ -2475,7 +2638,7 @@ impl LiveCli {
|
||||
false
|
||||
}
|
||||
SlashCommand::Init => {
|
||||
run_init()?;
|
||||
run_init(CliOutputFormat::Text)?;
|
||||
false
|
||||
}
|
||||
SlashCommand::Diff => {
|
||||
@@ -2483,7 +2646,7 @@ impl LiveCli {
|
||||
false
|
||||
}
|
||||
SlashCommand::Version => {
|
||||
Self::print_version();
|
||||
Self::print_version()?;
|
||||
false
|
||||
}
|
||||
SlashCommand::Export { path } => {
|
||||
@@ -2497,11 +2660,11 @@ impl LiveCli {
|
||||
self.handle_plugins_command(action.as_deref(), target.as_deref())?
|
||||
}
|
||||
SlashCommand::Agents { args } => {
|
||||
Self::print_agents(args.as_deref())?;
|
||||
Self::print_agents(args.as_deref(), CliOutputFormat::Text)?;
|
||||
false
|
||||
}
|
||||
SlashCommand::Skills { args } => {
|
||||
Self::print_skills(args.as_deref())?;
|
||||
Self::print_skills(args.as_deref(), CliOutputFormat::Text)?;
|
||||
false
|
||||
}
|
||||
SlashCommand::Doctor
|
||||
@@ -2776,22 +2939,61 @@ impl LiveCli {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_agents(args: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn print_agents(
|
||||
args: Option<&str>,
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cwd = env::current_dir()?;
|
||||
println!("{}", handle_agents_slash_command(args, &cwd)?);
|
||||
Ok(())
|
||||
let message = handle_agents_slash_command(args, &cwd)?;
|
||||
match output_format {
|
||||
CliOutputFormat::Text => {
|
||||
println!("{message}");
|
||||
Ok(())
|
||||
}
|
||||
CliOutputFormat::Json => print_json_value(&json!({
|
||||
"kind": "agents",
|
||||
"message": message,
|
||||
"args": args,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_mcp(args: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn print_mcp(
|
||||
args: Option<&str>,
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cwd = env::current_dir()?;
|
||||
println!("{}", handle_mcp_slash_command(args, &cwd)?);
|
||||
Ok(())
|
||||
let message = handle_mcp_slash_command(args, &cwd)?;
|
||||
match output_format {
|
||||
CliOutputFormat::Text => {
|
||||
println!("{message}");
|
||||
Ok(())
|
||||
}
|
||||
CliOutputFormat::Json => print_json_value(&json!({
|
||||
"kind": "mcp",
|
||||
"message": message,
|
||||
"args": args,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_skills(args: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn print_skills(
|
||||
args: Option<&str>,
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cwd = env::current_dir()?;
|
||||
println!("{}", handle_skills_slash_command(args, &cwd)?);
|
||||
Ok(())
|
||||
let message = handle_skills_slash_command(args, &cwd)?;
|
||||
match output_format {
|
||||
CliOutputFormat::Text => {
|
||||
println!("{message}");
|
||||
Ok(())
|
||||
}
|
||||
CliOutputFormat::Json => print_json_value(&json!({
|
||||
"kind": "skills",
|
||||
"message": message,
|
||||
"args": args,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_diff() -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -2799,8 +3001,8 @@ impl LiveCli {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_version() {
|
||||
println!("{}", render_version_report());
|
||||
fn print_version() -> Result<(), Box<dyn std::error::Error>> {
|
||||
crate::print_version(CliOutputFormat::Text)
|
||||
}
|
||||
|
||||
fn export_session(
|
||||
@@ -3709,9 +3911,18 @@ fn init_claude_md() -> Result<String, Box<dyn std::error::Error>> {
|
||||
Ok(initialize_repo(&cwd)?.render())
|
||||
}
|
||||
|
||||
fn run_init() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("{}", init_claude_md()?);
|
||||
Ok(())
|
||||
fn run_init(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let init_report = init_claude_md()?;
|
||||
match output_format {
|
||||
CliOutputFormat::Text => {
|
||||
println!("{init_report}");
|
||||
Ok(())
|
||||
}
|
||||
CliOutputFormat::Json => print_json_value(&json!({
|
||||
"kind": "init",
|
||||
"message": init_report,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_permission_mode(mode: &str) -> Option<&'static str> {
|
||||
@@ -5846,8 +6057,17 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
let _ = print_help_to(&mut io::stdout());
|
||||
fn print_help(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match output_format {
|
||||
CliOutputFormat::Text => {
|
||||
print_help_to(&mut io::stdout())?;
|
||||
Ok(())
|
||||
}
|
||||
CliOutputFormat::Json => print_json_value(&json!({
|
||||
"kind": "help",
|
||||
"message": render_help_report()?,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -6156,11 +6376,15 @@ mod tests {
|
||||
fn parses_version_flags_without_initializing_prompt_mode() {
|
||||
assert_eq!(
|
||||
parse_args(&["--version".to_string()]).expect("args should parse"),
|
||||
CliAction::Version
|
||||
CliAction::Version {
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["-V".to_string()]).expect("args should parse"),
|
||||
CliAction::Version
|
||||
CliAction::Version {
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6222,6 +6446,7 @@ mod tests {
|
||||
CliAction::PrintSystemPrompt {
|
||||
cwd: PathBuf::from("/tmp/project"),
|
||||
date: "2026-04-01".to_string(),
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -6230,33 +6455,49 @@ mod tests {
|
||||
fn parses_login_and_logout_subcommands() {
|
||||
assert_eq!(
|
||||
parse_args(&["login".to_string()]).expect("login should parse"),
|
||||
CliAction::Login
|
||||
CliAction::Login {
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["logout".to_string()]).expect("logout should parse"),
|
||||
CliAction::Logout
|
||||
CliAction::Logout {
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["init".to_string()]).expect("init should parse"),
|
||||
CliAction::Init
|
||||
CliAction::Init {
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["agents".to_string()]).expect("agents should parse"),
|
||||
CliAction::Agents { args: None }
|
||||
CliAction::Agents {
|
||||
args: None,
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["mcp".to_string()]).expect("mcp should parse"),
|
||||
CliAction::Mcp { args: None }
|
||||
CliAction::Mcp {
|
||||
args: None,
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["skills".to_string()]).expect("skills should parse"),
|
||||
CliAction::Skills { args: None }
|
||||
CliAction::Skills {
|
||||
args: None,
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["agents".to_string(), "--help".to_string()])
|
||||
.expect("agents help should parse"),
|
||||
CliAction::Agents {
|
||||
args: Some("--help".to_string())
|
||||
args: Some("--help".to_string()),
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -6267,11 +6508,15 @@ mod tests {
|
||||
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
|
||||
assert_eq!(
|
||||
parse_args(&["help".to_string()]).expect("help should parse"),
|
||||
CliAction::Help
|
||||
CliAction::Help {
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["version".to_string()]).expect("version should parse"),
|
||||
CliAction::Version
|
||||
CliAction::Version {
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["status".to_string()]).expect("status should parse"),
|
||||
@@ -6339,24 +6584,32 @@ mod tests {
|
||||
fn parses_direct_agents_mcp_and_skills_slash_commands() {
|
||||
assert_eq!(
|
||||
parse_args(&["/agents".to_string()]).expect("/agents should parse"),
|
||||
CliAction::Agents { args: None }
|
||||
CliAction::Agents {
|
||||
args: None,
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["/mcp".to_string(), "show".to_string(), "demo".to_string()])
|
||||
.expect("/mcp show demo should parse"),
|
||||
CliAction::Mcp {
|
||||
args: Some("show demo".to_string())
|
||||
args: Some("show demo".to_string()),
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["/skills".to_string()]).expect("/skills should parse"),
|
||||
CliAction::Skills { args: None }
|
||||
CliAction::Skills {
|
||||
args: None,
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["/skills".to_string(), "help".to_string()])
|
||||
.expect("/skills help should parse"),
|
||||
CliAction::Skills {
|
||||
args: Some("help".to_string())
|
||||
args: Some("help".to_string()),
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -6367,7 +6620,8 @@ mod tests {
|
||||
])
|
||||
.expect("/skills install should parse"),
|
||||
CliAction::Skills {
|
||||
args: Some("install ./fixtures/help-skill".to_string())
|
||||
args: Some("install ./fixtures/help-skill".to_string()),
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
let error = parse_args(&["/status".to_string()])
|
||||
|
||||
449
rust/crates/rusty-claude-cli/tests/output_format_contract.rs
Normal file
449
rust/crates/rusty-claude-cli/tests/output_format_contract.rs
Normal file
@@ -0,0 +1,449 @@
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpListener;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::thread;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use mock_anthropic_service::{MockAnthropicService, SCENARIO_PREFIX};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
#[test]
|
||||
fn help_emits_json_when_requested() {
|
||||
let root = unique_temp_dir("help-json");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
let envs = isolated_env(&root);
|
||||
|
||||
let parsed = assert_json_command(&root, &["--output-format", "json", "help"], &envs);
|
||||
|
||||
assert_eq!(parsed["kind"], "help");
|
||||
assert!(parsed["message"]
|
||||
.as_str()
|
||||
.expect("help message")
|
||||
.contains("Usage:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_emits_json_when_requested() {
|
||||
let root = unique_temp_dir("version-json");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
let envs = isolated_env(&root);
|
||||
|
||||
let parsed = assert_json_command(&root, &["--output-format", "json", "version"], &envs);
|
||||
|
||||
assert_eq!(parsed["kind"], "version");
|
||||
assert_eq!(parsed["version"], env!("CARGO_PKG_VERSION"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_emits_json_when_requested() {
|
||||
let root = unique_temp_dir("status-json");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
let envs = isolated_env(&root);
|
||||
|
||||
let parsed = assert_json_command(&root, &["--output-format", "json", "status"], &envs);
|
||||
|
||||
assert_eq!(parsed["kind"], "status");
|
||||
assert!(parsed["workspace"]["cwd"].as_str().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_emits_json_when_requested() {
|
||||
let root = unique_temp_dir("sandbox-json");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
let envs = isolated_env(&root);
|
||||
|
||||
let parsed = assert_json_command(&root, &["--output-format", "json", "sandbox"], &envs);
|
||||
|
||||
assert_eq!(parsed["kind"], "sandbox");
|
||||
assert!(parsed["sandbox"].is_object());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dump_manifests_emits_json_when_requested() {
|
||||
let root = unique_temp_dir("dump-manifests-json");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
let upstream = write_upstream_fixture(&root);
|
||||
let mut envs = isolated_env(&root);
|
||||
envs.push((
|
||||
"CLAUDE_CODE_UPSTREAM".to_string(),
|
||||
upstream.display().to_string(),
|
||||
));
|
||||
|
||||
let parsed = assert_json_command(&root, &["--output-format", "json", "dump-manifests"], &envs);
|
||||
|
||||
assert_eq!(parsed["kind"], "dump-manifests");
|
||||
assert_eq!(parsed["commands"], 1);
|
||||
assert_eq!(parsed["tools"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bootstrap_plan_emits_json_when_requested() {
|
||||
let root = unique_temp_dir("bootstrap-plan-json");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
let envs = isolated_env(&root);
|
||||
|
||||
let parsed = assert_json_command(&root, &["--output-format", "json", "bootstrap-plan"], &envs);
|
||||
|
||||
assert_eq!(parsed["kind"], "bootstrap-plan");
|
||||
assert!(parsed["phases"].as_array().expect("phases array").len() > 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agents_emits_json_when_requested() {
|
||||
let root = unique_temp_dir("agents-json");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
let envs = isolated_env(&root);
|
||||
|
||||
let parsed = assert_json_command(&root, &["--output-format", "json", "agents"], &envs);
|
||||
|
||||
assert_eq!(parsed["kind"], "agents");
|
||||
assert!(!parsed["message"].as_str().expect("agents text").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_emits_json_when_requested() {
|
||||
let root = unique_temp_dir("mcp-json");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
let envs = isolated_env(&root);
|
||||
|
||||
let parsed = assert_json_command(&root, &["--output-format", "json", "mcp"], &envs);
|
||||
|
||||
assert_eq!(parsed["kind"], "mcp");
|
||||
assert!(parsed["message"]
|
||||
.as_str()
|
||||
.expect("mcp text")
|
||||
.contains("MCP"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skills_emits_json_when_requested() {
|
||||
let root = unique_temp_dir("skills-json");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
let envs = isolated_env(&root);
|
||||
|
||||
let parsed = assert_json_command(&root, &["--output-format", "json", "skills"], &envs);
|
||||
|
||||
assert_eq!(parsed["kind"], "skills");
|
||||
assert!(!parsed["message"].as_str().expect("skills text").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_prompt_emits_json_when_requested() {
|
||||
let root = unique_temp_dir("system-prompt-json");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
let envs = isolated_env(&root);
|
||||
|
||||
let parsed = assert_json_command(&root, &["--output-format", "json", "system-prompt"], &envs);
|
||||
|
||||
assert_eq!(parsed["kind"], "system-prompt");
|
||||
assert!(parsed["message"]
|
||||
.as_str()
|
||||
.expect("system prompt text")
|
||||
.contains("You are an interactive agent"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_emits_json_when_requested() {
|
||||
let root = unique_temp_dir("login-json");
|
||||
let workspace = root.join("workspace");
|
||||
fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||
let mut envs = isolated_env(&root);
|
||||
let callback_port = reserve_port();
|
||||
let token_port = reserve_port();
|
||||
|
||||
fs::create_dir_all(workspace.join(".claw")).expect("config dir should exist");
|
||||
fs::write(
|
||||
workspace.join(".claw").join("settings.json"),
|
||||
json!({
|
||||
"oauth": {
|
||||
"clientId": "test-client",
|
||||
"authorizeUrl": format!("http://127.0.0.1:{token_port}/authorize"),
|
||||
"tokenUrl": format!("http://127.0.0.1:{token_port}/token"),
|
||||
"callbackPort": callback_port,
|
||||
"scopes": ["user:test"]
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.expect("oauth config should write");
|
||||
|
||||
let token_server = thread::spawn(move || {
|
||||
let listener = TcpListener::bind(("127.0.0.1", token_port)).expect("token server bind");
|
||||
let (mut stream, _) = listener.accept().expect("token request");
|
||||
let mut request = [0_u8; 4096];
|
||||
let _ = stream
|
||||
.read(&mut request)
|
||||
.expect("token request should read");
|
||||
let body = json!({
|
||||
"access_token": "test-access-token",
|
||||
"refresh_token": "test-refresh-token",
|
||||
"expires_at": 9_999_999_999_u64,
|
||||
"scopes": ["user:test"]
|
||||
})
|
||||
.to_string();
|
||||
let response = format!(
|
||||
"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
stream
|
||||
.write_all(response.as_bytes())
|
||||
.expect("token response should write");
|
||||
});
|
||||
|
||||
let bin_dir = root.join("bin");
|
||||
fs::create_dir_all(&bin_dir).expect("bin dir should exist");
|
||||
let opener_path = bin_dir.join("xdg-open");
|
||||
fs::write(
|
||||
&opener_path,
|
||||
format!(
|
||||
"#!/usr/bin/env python3\nimport http.client\nimport sys\nimport urllib.parse\nurl = sys.argv[1]\nquery = urllib.parse.parse_qs(urllib.parse.urlparse(url).query)\nstate = query['state'][0]\nconn = http.client.HTTPConnection('127.0.0.1', {callback_port}, timeout=5)\nconn.request('GET', f\"/callback?code=test-code&state={{urllib.parse.quote(state)}}\")\nresp = conn.getresponse()\nresp.read()\nconn.close()\n"
|
||||
),
|
||||
)
|
||||
.expect("xdg-open wrapper should write");
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut permissions = fs::metadata(&opener_path)
|
||||
.expect("wrapper metadata")
|
||||
.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
fs::set_permissions(&opener_path, permissions).expect("wrapper permissions");
|
||||
}
|
||||
let original_path = envs
|
||||
.iter()
|
||||
.find(|(key, _)| key == "PATH")
|
||||
.map(|(_, value)| value.clone())
|
||||
.unwrap_or_default();
|
||||
for (key, value) in &mut envs {
|
||||
if key == "PATH" {
|
||||
*value = format!("{}:{original_path}", bin_dir.display());
|
||||
}
|
||||
}
|
||||
|
||||
let parsed = assert_json_command(&workspace, &["--output-format", "json", "login"], &envs);
|
||||
|
||||
token_server.join().expect("token server should finish");
|
||||
assert_eq!(parsed["kind"], "login");
|
||||
assert_eq!(parsed["callback_port"], callback_port);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logout_emits_json_when_requested() {
|
||||
let root = unique_temp_dir("logout-json");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
let envs = isolated_env(&root);
|
||||
|
||||
let parsed = assert_json_command(&root, &["--output-format", "json", "logout"], &envs);
|
||||
|
||||
assert_eq!(parsed["kind"], "logout");
|
||||
assert!(parsed["message"]
|
||||
.as_str()
|
||||
.expect("logout text")
|
||||
.contains("cleared"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_emits_json_when_requested() {
|
||||
let root = unique_temp_dir("init-json");
|
||||
let workspace = root.join("workspace");
|
||||
fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||
let envs = isolated_env(&root);
|
||||
|
||||
let parsed = assert_json_command(&workspace, &["--output-format", "json", "init"], &envs);
|
||||
|
||||
assert_eq!(parsed["kind"], "init");
|
||||
assert!(workspace.join("CLAUDE.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_subcommand_emits_json_when_requested() {
|
||||
let root = unique_temp_dir("prompt-subcommand-json");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
let mut envs = isolated_env(&root);
|
||||
let runtime = tokio::runtime::Runtime::new().expect("tokio runtime should build");
|
||||
let server = runtime
|
||||
.block_on(MockAnthropicService::spawn())
|
||||
.expect("mock service should start");
|
||||
envs.push(("ANTHROPIC_API_KEY".to_string(), "test-key".to_string()));
|
||||
envs.push(("ANTHROPIC_BASE_URL".to_string(), server.base_url()));
|
||||
|
||||
let prompt = format!("{SCENARIO_PREFIX}streaming_text");
|
||||
let args = vec![
|
||||
"--model".to_string(),
|
||||
"sonnet".to_string(),
|
||||
"--permission-mode".to_string(),
|
||||
"read-only".to_string(),
|
||||
"--output-format".to_string(),
|
||||
"json".to_string(),
|
||||
"prompt".to_string(),
|
||||
prompt,
|
||||
];
|
||||
let output = run_claw_with_env_owned(&root, &args, &envs);
|
||||
let parsed = parse_json_stdout(&output);
|
||||
|
||||
assert_eq!(parsed["model"], "claude-sonnet-4-6");
|
||||
assert!(parsed["message"]
|
||||
.as_str()
|
||||
.expect("assistant text")
|
||||
.contains("streaming"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_prompt_mode_emits_json_when_requested() {
|
||||
let root = unique_temp_dir("bare-prompt-json");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
let mut envs = isolated_env(&root);
|
||||
let runtime = tokio::runtime::Runtime::new().expect("tokio runtime should build");
|
||||
let server = runtime
|
||||
.block_on(MockAnthropicService::spawn())
|
||||
.expect("mock service should start");
|
||||
envs.push(("ANTHROPIC_API_KEY".to_string(), "test-key".to_string()));
|
||||
envs.push(("ANTHROPIC_BASE_URL".to_string(), server.base_url()));
|
||||
|
||||
let prompt = format!("{SCENARIO_PREFIX}streaming_text");
|
||||
let args = vec![
|
||||
"--model".to_string(),
|
||||
"sonnet".to_string(),
|
||||
"--permission-mode".to_string(),
|
||||
"read-only".to_string(),
|
||||
"--output-format".to_string(),
|
||||
"json".to_string(),
|
||||
prompt,
|
||||
];
|
||||
let output = run_claw_with_env_owned(&root, &args, &envs);
|
||||
let parsed = parse_json_stdout(&output);
|
||||
|
||||
assert_eq!(parsed["model"], "claude-sonnet-4-6");
|
||||
assert!(parsed["message"]
|
||||
.as_str()
|
||||
.expect("assistant text")
|
||||
.contains("streaming"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_restore_emits_json_when_requested() {
|
||||
let root = unique_temp_dir("resume-json");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
let envs = isolated_env(&root);
|
||||
let session_path = root.join("session.jsonl");
|
||||
fs::write(
|
||||
&session_path,
|
||||
"{\"type\":\"session_meta\",\"version\":3,\"session_id\":\"resume-json\",\"created_at_ms\":0,\"updated_at_ms\":0}\n{\"type\":\"message\",\"message\":{\"role\":\"user\",\"blocks\":[{\"type\":\"text\",\"text\":\"hello\"}]}}\n",
|
||||
)
|
||||
.expect("session should write");
|
||||
|
||||
let args = vec![
|
||||
"--output-format".to_string(),
|
||||
"json".to_string(),
|
||||
"--resume".to_string(),
|
||||
session_path.display().to_string(),
|
||||
];
|
||||
let output = run_claw_with_env_owned(&root, &args, &envs);
|
||||
let parsed = parse_json_stdout(&output);
|
||||
|
||||
assert_eq!(parsed["kind"], "resume");
|
||||
assert_eq!(parsed["messages"], 1);
|
||||
}
|
||||
|
||||
fn assert_json_command(current_dir: &Path, args: &[&str], envs: &[(String, String)]) -> Value {
|
||||
let output = run_claw_with_env(current_dir, args, envs);
|
||||
parse_json_stdout(&output)
|
||||
}
|
||||
|
||||
fn parse_json_stdout(output: &Output) -> Value {
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"stdout:\n{}\n\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
serde_json::from_slice(&output.stdout).expect("stdout should be json")
|
||||
}
|
||||
|
||||
fn run_claw_with_env(current_dir: &Path, args: &[&str], envs: &[(String, String)]) -> Output {
|
||||
let owned_args = args
|
||||
.iter()
|
||||
.map(|value| (*value).to_string())
|
||||
.collect::<Vec<_>>();
|
||||
run_claw_with_env_owned(current_dir, &owned_args, envs)
|
||||
}
|
||||
|
||||
fn run_claw_with_env_owned(
|
||||
current_dir: &Path,
|
||||
args: &[String],
|
||||
envs: &[(String, String)],
|
||||
) -> Output {
|
||||
let mut command = Command::new(env!("CARGO_BIN_EXE_claw"));
|
||||
command.current_dir(current_dir).args(args).env_clear();
|
||||
for (key, value) in envs {
|
||||
command.env(key, value);
|
||||
}
|
||||
command.output().expect("claw should launch")
|
||||
}
|
||||
|
||||
fn isolated_env(root: &Path) -> Vec<(String, String)> {
|
||||
let config_home = root.join("config-home");
|
||||
let home = root.join("home");
|
||||
fs::create_dir_all(&config_home).expect("config home should exist");
|
||||
fs::create_dir_all(&home).expect("home should exist");
|
||||
vec![
|
||||
(
|
||||
"CLAW_CONFIG_HOME".to_string(),
|
||||
config_home.display().to_string(),
|
||||
),
|
||||
("HOME".to_string(), home.display().to_string()),
|
||||
(
|
||||
"PATH".to_string(),
|
||||
std::env::var("PATH").unwrap_or_else(|_| "/usr/bin:/bin".to_string()),
|
||||
),
|
||||
("NO_COLOR".to_string(), "1".to_string()),
|
||||
]
|
||||
}
|
||||
|
||||
fn write_upstream_fixture(root: &Path) -> PathBuf {
|
||||
let upstream = root.join("claw-code");
|
||||
let src = upstream.join("src");
|
||||
let entrypoints = src.join("entrypoints");
|
||||
fs::create_dir_all(&entrypoints).expect("upstream entrypoints dir should exist");
|
||||
fs::write(
|
||||
src.join("commands.ts"),
|
||||
"import FooCommand from './commands/foo'\n",
|
||||
)
|
||||
.expect("commands fixture should write");
|
||||
fs::write(
|
||||
src.join("tools.ts"),
|
||||
"import ReadTool from './tools/read'\n",
|
||||
)
|
||||
.expect("tools fixture should write");
|
||||
fs::write(
|
||||
entrypoints.join("cli.tsx"),
|
||||
"if (args[0] === '--version') {}\nstartupProfiler()\n",
|
||||
)
|
||||
.expect("cli fixture should write");
|
||||
upstream
|
||||
}
|
||||
|
||||
fn reserve_port() -> u16 {
|
||||
let listener = TcpListener::bind(("127.0.0.1", 0)).expect("ephemeral port should bind");
|
||||
let port = listener.local_addr().expect("local addr").port();
|
||||
drop(listener);
|
||||
port
|
||||
}
|
||||
|
||||
fn unique_temp_dir(label: &str) -> PathBuf {
|
||||
let millis = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("clock should be after epoch")
|
||||
.as_millis();
|
||||
let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
std::env::temp_dir().join(format!(
|
||||
"claw-output-format-{label}-{}-{millis}-{counter}",
|
||||
std::process::id()
|
||||
))
|
||||
}
|
||||
@@ -2976,21 +2976,15 @@ fn resolve_skill_path(skill: &str) -> Result<std::path::PathBuf, String> {
|
||||
}
|
||||
|
||||
let mut candidates = Vec::new();
|
||||
if let Ok(claw_config_home) = std::env::var("CLAW_CONFIG_HOME") {
|
||||
candidates.push(std::path::PathBuf::from(claw_config_home).join("skills"));
|
||||
}
|
||||
if let Ok(codex_home) = std::env::var("CODEX_HOME") {
|
||||
candidates.push(std::path::PathBuf::from(codex_home).join("skills"));
|
||||
}
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
let home = std::path::PathBuf::from(home);
|
||||
candidates.push(home.join(".claw").join("skills"));
|
||||
candidates.push(home.join(".agents").join("skills"));
|
||||
candidates.push(home.join(".config").join("opencode").join("skills"));
|
||||
candidates.push(home.join(".codex").join("skills"));
|
||||
candidates.push(home.join(".claude").join("skills"));
|
||||
}
|
||||
candidates.push(std::path::PathBuf::from("/home/bellman/.claw/skills"));
|
||||
candidates.push(std::path::PathBuf::from("/home/bellman/.codex/skills"));
|
||||
|
||||
for root in candidates {
|
||||
|
||||
Reference in New Issue
Block a user