mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-12 19:14:51 +08:00
Make dump-manifests recoverable outside the inferred build tree
The backlog sweep found that the user-cited #21-#23 items were already closed, and the next real pain point was `claw dump-manifests` failing without a direct way to point at the upstream manifest source. This adds an explicit `--manifests-dir` path, upgrades the failure messages to say whether the source root or required files are missing, and updates the ROADMAP closeout to reflect that #45 is now fixed. Constraint: Preserve existing dump-manifests behavior when no explicit override is supplied Rejected: Require CLAUDE_CODE_UPSTREAM for every invocation | breaks existing build-tree workflows and is unnecessarily rigid Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep manifest-source override guidance centralized so future error-path edits do not drift Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace; architect review APPROVE Not-tested: Manual invocation against every legacy env-based manifest lookup layout
This commit is contained in:
@@ -177,7 +177,10 @@ fn merge_prompt_with_stdin(prompt: &str, stdin_content: Option<&str>) -> String
|
||||
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<String> = env::args().skip(1).collect();
|
||||
match parse_args(&args)? {
|
||||
CliAction::DumpManifests { output_format } => dump_manifests(output_format)?,
|
||||
CliAction::DumpManifests {
|
||||
output_format,
|
||||
manifests_dir,
|
||||
} => dump_manifests(manifests_dir.as_deref(), output_format)?,
|
||||
CliAction::BootstrapPlan { output_format } => print_bootstrap_plan(output_format)?,
|
||||
CliAction::Agents {
|
||||
args,
|
||||
@@ -274,6 +277,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
enum CliAction {
|
||||
DumpManifests {
|
||||
output_format: CliOutputFormat,
|
||||
manifests_dir: Option<PathBuf>,
|
||||
},
|
||||
BootstrapPlan {
|
||||
output_format: CliOutputFormat,
|
||||
@@ -623,7 +627,7 @@ 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 { output_format }),
|
||||
"dump-manifests" => parse_dump_manifests_args(&rest[1..], output_format),
|
||||
"bootstrap-plan" => Ok(CliAction::BootstrapPlan { output_format }),
|
||||
"agents" => Ok(CliAction::Agents {
|
||||
args: join_optional_args(&rest[1..]),
|
||||
@@ -1213,6 +1217,39 @@ fn parse_export_args(args: &[String], output_format: CliOutputFormat) -> Result<
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_dump_manifests_args(
|
||||
args: &[String],
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<CliAction, String> {
|
||||
let mut manifests_dir: Option<PathBuf> = None;
|
||||
let mut index = 0;
|
||||
while index < args.len() {
|
||||
let arg = &args[index];
|
||||
if arg == "--manifests-dir" {
|
||||
let value = args
|
||||
.get(index + 1)
|
||||
.ok_or_else(|| String::from("--manifests-dir requires a path"))?;
|
||||
manifests_dir = Some(PathBuf::from(value));
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
if let Some(value) = arg.strip_prefix("--manifests-dir=") {
|
||||
if value.is_empty() {
|
||||
return Err(String::from("--manifests-dir requires a path"));
|
||||
}
|
||||
manifests_dir = Some(PathBuf::from(value));
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
return Err(format!("unknown dump-manifests option: {arg}"));
|
||||
}
|
||||
|
||||
Ok(CliAction::DumpManifests {
|
||||
output_format,
|
||||
manifests_dir,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_resume_args(args: &[String], output_format: CliOutputFormat) -> Result<CliAction, String> {
|
||||
let (session_path, command_tokens): (PathBuf, &[String]) = match args.first() {
|
||||
None => (PathBuf::from(LATEST_SESSION_REFERENCE), &[]),
|
||||
@@ -1915,32 +1952,58 @@ fn looks_like_slash_command_token(token: &str) -> bool {
|
||||
.any(|spec| spec.name == name || spec.aliases.contains(&name))
|
||||
}
|
||||
|
||||
fn dump_manifests(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn dump_manifests(
|
||||
manifests_dir: Option<&Path>,
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let workspace_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
dump_manifests_at_path(&workspace_dir, output_format)
|
||||
dump_manifests_at_path(&workspace_dir, manifests_dir, output_format)
|
||||
}
|
||||
|
||||
const DUMP_MANIFESTS_OVERRIDE_HINT: &str =
|
||||
"Hint: set CLAUDE_CODE_UPSTREAM=/path/to/upstream or pass `claw dump-manifests --manifests-dir /path/to/upstream`.";
|
||||
|
||||
// Internal function for testing that accepts a workspace directory path.
|
||||
fn dump_manifests_at_path(
|
||||
workspace_dir: &std::path::Path,
|
||||
manifests_dir: Option<&Path>,
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Surface the resolved path in the error so users can diagnose missing
|
||||
// manifest files without guessing what path the binary expected.
|
||||
// ROADMAP #45: this path is only correct when running from the build tree;
|
||||
// a proper fix would ship manifests alongside the binary.
|
||||
let resolved = workspace_dir
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| workspace_dir.to_path_buf());
|
||||
let paths = if let Some(dir) = manifests_dir {
|
||||
let resolved = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
|
||||
UpstreamPaths::from_repo_root(resolved)
|
||||
} else {
|
||||
// Surface the resolved path in the error so users can diagnose missing
|
||||
// manifest files without guessing what path the binary expected.
|
||||
let resolved = workspace_dir
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| workspace_dir.to_path_buf());
|
||||
UpstreamPaths::from_workspace_dir(&resolved)
|
||||
};
|
||||
|
||||
let paths = UpstreamPaths::from_workspace_dir(&resolved);
|
||||
|
||||
// Pre-check: verify manifest directory exists
|
||||
let manifest_dir = paths.repo_root();
|
||||
if !manifest_dir.exists() {
|
||||
let source_root = paths.repo_root();
|
||||
if !source_root.exists() {
|
||||
return Err(format!(
|
||||
"Manifest files (commands.ts, tools.ts) define CLI commands and tools.\n Expected at: {}\n Run `claw init` to create them or specify --manifests-dir.",
|
||||
manifest_dir.display()
|
||||
"Manifest source directory does not exist.\n looked in: {}\n {DUMP_MANIFESTS_OVERRIDE_HINT}",
|
||||
source_root.display(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let required_paths = [
|
||||
("src/commands.ts", paths.commands_path()),
|
||||
("src/tools.ts", paths.tools_path()),
|
||||
("src/entrypoints/cli.tsx", paths.cli_path()),
|
||||
];
|
||||
let missing = required_paths
|
||||
.iter()
|
||||
.filter_map(|(label, path)| (!path.is_file()).then_some(*label))
|
||||
.collect::<Vec<_>>();
|
||||
if !missing.is_empty() {
|
||||
return Err(format!(
|
||||
"Manifest source files are missing.\n repo root: {}\n missing: {}\n {DUMP_MANIFESTS_OVERRIDE_HINT}",
|
||||
source_root.display(),
|
||||
missing.join(", "),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
@@ -1966,7 +2029,7 @@ fn dump_manifests_at_path(
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => Err(format!(
|
||||
"failed to extract manifests: {error}\n looked in: {path}",
|
||||
"failed to extract manifests: {error}\n looked in: {path}\n {DUMP_MANIFESTS_OVERRIDE_HINT}",
|
||||
path = paths.repo_root().display()
|
||||
)
|
||||
.into()),
|
||||
@@ -8048,7 +8111,7 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
out,
|
||||
" Diagnose local auth, config, workspace, and sandbox health"
|
||||
)?;
|
||||
writeln!(out, " claw dump-manifests")?;
|
||||
writeln!(out, " claw dump-manifests [--manifests-dir PATH]")?;
|
||||
writeln!(out, " claw bootstrap-plan")?;
|
||||
writeln!(out, " claw agents")?;
|
||||
writeln!(out, " claw mcp")?;
|
||||
@@ -9063,6 +9126,33 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dump_manifests_subcommand_accepts_explicit_manifest_dir() {
|
||||
assert_eq!(
|
||||
parse_args(&[
|
||||
"dump-manifests".to_string(),
|
||||
"--manifests-dir".to_string(),
|
||||
"/tmp/upstream".to_string(),
|
||||
])
|
||||
.expect("dump-manifests should parse"),
|
||||
CliAction::DumpManifests {
|
||||
output_format: CliOutputFormat::Text,
|
||||
manifests_dir: Some(PathBuf::from("/tmp/upstream")),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&[
|
||||
"dump-manifests".to_string(),
|
||||
"--manifests-dir=/tmp/upstream".to_string()
|
||||
])
|
||||
.expect("inline dump-manifests flag should parse"),
|
||||
CliAction::DumpManifests {
|
||||
output_format: CliOutputFormat::Text,
|
||||
manifests_dir: Some(PathBuf::from("/tmp/upstream")),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_command_help_flags_stay_on_the_local_parser_path() {
|
||||
assert_eq!(
|
||||
@@ -11554,43 +11644,78 @@ mod sandbox_report_tests {
|
||||
#[cfg(test)]
|
||||
mod dump_manifests_tests {
|
||||
use super::{dump_manifests_at_path, CliOutputFormat};
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn dump_manifests_shows_helpful_error_when_manifests_missing() {
|
||||
// Create a temp directory without manifest files
|
||||
let temp_dir = std::env::temp_dir().join(format!(
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"claw_test_missing_manifests_{}",
|
||||
std::process::id()
|
||||
));
|
||||
std::fs::create_dir_all(&temp_dir).expect("failed to create temp dir");
|
||||
let workspace = root.join("workspace");
|
||||
std::fs::create_dir_all(&workspace).expect("failed to create temp workspace");
|
||||
|
||||
// Clean up at the end of the test
|
||||
let _cleanup = std::panic::catch_unwind(|| {
|
||||
// Call dump_manifests_at_path with the temp directory
|
||||
let result = dump_manifests_at_path(&temp_dir, CliOutputFormat::Text);
|
||||
let result = dump_manifests_at_path(&workspace, None, CliOutputFormat::Text);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"expected an error when manifests are missing"
|
||||
);
|
||||
|
||||
// Assert that the call fails
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"expected an error when manifests are missing"
|
||||
);
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
error_msg.contains("Manifest source files are missing"),
|
||||
"error message should mention missing manifest sources: {error_msg}"
|
||||
);
|
||||
assert!(
|
||||
error_msg.contains(&root.display().to_string()),
|
||||
"error message should contain the resolved repo root path: {error_msg}"
|
||||
);
|
||||
assert!(
|
||||
error_msg.contains("src/commands.ts"),
|
||||
"error message should mention missing commands.ts: {error_msg}"
|
||||
);
|
||||
assert!(
|
||||
error_msg.contains("CLAUDE_CODE_UPSTREAM"),
|
||||
"error message should explain how to supply the upstream path: {error_msg}"
|
||||
);
|
||||
|
||||
// Assert the error message contains "Manifest files (commands.ts, tools.ts)"
|
||||
assert!(
|
||||
error_msg.contains("Manifest files (commands.ts, tools.ts)"),
|
||||
"error message should mention manifest files: {error_msg}"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
// Assert the error message contains the expected path
|
||||
assert!(
|
||||
error_msg.contains(&temp_dir.display().to_string()),
|
||||
"error message should contain the expected path: {error_msg}"
|
||||
);
|
||||
});
|
||||
#[test]
|
||||
fn dump_manifests_uses_explicit_manifest_dir() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"claw_test_explicit_manifest_dir_{}",
|
||||
std::process::id()
|
||||
));
|
||||
let workspace = root.join("workspace");
|
||||
let upstream = root.join("upstream");
|
||||
fs::create_dir_all(workspace.join("nested")).expect("workspace should exist");
|
||||
fs::create_dir_all(upstream.join("src/entrypoints"))
|
||||
.expect("upstream fixture should exist");
|
||||
fs::write(
|
||||
upstream.join("src/commands.ts"),
|
||||
"import FooCommand from './commands/foo'\n",
|
||||
)
|
||||
.expect("commands fixture should write");
|
||||
fs::write(
|
||||
upstream.join("src/tools.ts"),
|
||||
"import ReadTool from './tools/read'\n",
|
||||
)
|
||||
.expect("tools fixture should write");
|
||||
fs::write(
|
||||
upstream.join("src/entrypoints/cli.tsx"),
|
||||
"startupProfiler()\n",
|
||||
)
|
||||
.expect("cli fixture should write");
|
||||
|
||||
// Clean up temp directory
|
||||
let _ = std::fs::remove_dir_all(&temp_dir);
|
||||
let result = dump_manifests_at_path(&workspace, Some(&upstream), CliOutputFormat::Text);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"explicit manifest dir should succeed: {result:?}"
|
||||
);
|
||||
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,13 +174,15 @@ fn dump_manifests_and_init_emit_json_when_requested() {
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
|
||||
let upstream = write_upstream_fixture(&root);
|
||||
let manifests = assert_json_command_with_env(
|
||||
let manifests = assert_json_command(
|
||||
&root,
|
||||
&["--output-format", "json", "dump-manifests"],
|
||||
&[(
|
||||
"CLAUDE_CODE_UPSTREAM",
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"dump-manifests",
|
||||
"--manifests-dir",
|
||||
upstream.to_str().expect("utf8 upstream"),
|
||||
)],
|
||||
],
|
||||
);
|
||||
assert_eq!(manifests["kind"], "dump-manifests");
|
||||
assert_eq!(manifests["commands"], 1);
|
||||
|
||||
Reference in New Issue
Block a user