lib.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. use std::fs;
  2. use std::path::{Path, PathBuf};
  3. use commands::{CommandManifestEntry, CommandRegistry, CommandSource};
  4. use runtime::{BootstrapPhase, BootstrapPlan};
  5. use tools::{ToolManifestEntry, ToolRegistry, ToolSource};
  6. #[derive(Debug, Clone, PartialEq, Eq)]
  7. pub struct UpstreamPaths {
  8. repo_root: PathBuf,
  9. }
  10. impl UpstreamPaths {
  11. #[must_use]
  12. pub fn from_repo_root(repo_root: impl Into<PathBuf>) -> Self {
  13. Self {
  14. repo_root: repo_root.into(),
  15. }
  16. }
  17. #[must_use]
  18. pub fn from_workspace_dir(workspace_dir: impl AsRef<Path>) -> Self {
  19. let workspace_dir = workspace_dir
  20. .as_ref()
  21. .canonicalize()
  22. .unwrap_or_else(|_| workspace_dir.as_ref().to_path_buf());
  23. let primary_repo_root = workspace_dir
  24. .parent()
  25. .map_or_else(|| PathBuf::from(".."), Path::to_path_buf);
  26. let repo_root = resolve_upstream_repo_root(&primary_repo_root);
  27. Self { repo_root }
  28. }
  29. #[must_use]
  30. pub fn commands_path(&self) -> PathBuf {
  31. self.repo_root.join("src/commands.ts")
  32. }
  33. #[must_use]
  34. pub fn tools_path(&self) -> PathBuf {
  35. self.repo_root.join("src/tools.ts")
  36. }
  37. #[must_use]
  38. pub fn cli_path(&self) -> PathBuf {
  39. self.repo_root.join("src/entrypoints/cli.tsx")
  40. }
  41. }
  42. #[derive(Debug, Clone, PartialEq, Eq)]
  43. pub struct ExtractedManifest {
  44. pub commands: CommandRegistry,
  45. pub tools: ToolRegistry,
  46. pub bootstrap: BootstrapPlan,
  47. }
  48. fn resolve_upstream_repo_root(primary_repo_root: &Path) -> PathBuf {
  49. let candidates = upstream_repo_candidates(primary_repo_root);
  50. candidates
  51. .into_iter()
  52. .find(|candidate| candidate.join("src/commands.ts").is_file())
  53. .unwrap_or_else(|| primary_repo_root.to_path_buf())
  54. }
  55. fn upstream_repo_candidates(primary_repo_root: &Path) -> Vec<PathBuf> {
  56. let mut candidates = vec![primary_repo_root.to_path_buf()];
  57. if let Some(explicit) = std::env::var_os("CLAUDE_CODE_UPSTREAM") {
  58. candidates.push(PathBuf::from(explicit));
  59. }
  60. for ancestor in primary_repo_root.ancestors().take(4) {
  61. candidates.push(ancestor.join("claw-code"));
  62. candidates.push(ancestor.join("clawd-code"));
  63. }
  64. candidates.push(primary_repo_root.join("reference-source").join("claw-code"));
  65. candidates.push(primary_repo_root.join("vendor").join("claw-code"));
  66. let mut deduped = Vec::new();
  67. for candidate in candidates {
  68. if !deduped.iter().any(|seen: &PathBuf| seen == &candidate) {
  69. deduped.push(candidate);
  70. }
  71. }
  72. deduped
  73. }
  74. pub fn extract_manifest(paths: &UpstreamPaths) -> std::io::Result<ExtractedManifest> {
  75. let commands_source = fs::read_to_string(paths.commands_path())?;
  76. let tools_source = fs::read_to_string(paths.tools_path())?;
  77. let cli_source = fs::read_to_string(paths.cli_path())?;
  78. Ok(ExtractedManifest {
  79. commands: extract_commands(&commands_source),
  80. tools: extract_tools(&tools_source),
  81. bootstrap: extract_bootstrap_plan(&cli_source),
  82. })
  83. }
  84. #[must_use]
  85. pub fn extract_commands(source: &str) -> CommandRegistry {
  86. let mut entries = Vec::new();
  87. let mut in_internal_block = false;
  88. for raw_line in source.lines() {
  89. let line = raw_line.trim();
  90. if line.starts_with("export const INTERNAL_ONLY_COMMANDS = [") {
  91. in_internal_block = true;
  92. continue;
  93. }
  94. if in_internal_block {
  95. if line.starts_with(']') {
  96. in_internal_block = false;
  97. continue;
  98. }
  99. if let Some(name) = first_identifier(line) {
  100. entries.push(CommandManifestEntry {
  101. name,
  102. source: CommandSource::InternalOnly,
  103. });
  104. }
  105. continue;
  106. }
  107. if line.starts_with("import ") {
  108. for imported in imported_symbols(line) {
  109. entries.push(CommandManifestEntry {
  110. name: imported,
  111. source: CommandSource::Builtin,
  112. });
  113. }
  114. }
  115. if line.contains("feature('") && line.contains("./commands/") {
  116. if let Some(name) = first_assignment_identifier(line) {
  117. entries.push(CommandManifestEntry {
  118. name,
  119. source: CommandSource::FeatureGated,
  120. });
  121. }
  122. }
  123. }
  124. dedupe_commands(entries)
  125. }
  126. #[must_use]
  127. pub fn extract_tools(source: &str) -> ToolRegistry {
  128. let mut entries = Vec::new();
  129. for raw_line in source.lines() {
  130. let line = raw_line.trim();
  131. if line.starts_with("import ") && line.contains("./tools/") {
  132. for imported in imported_symbols(line) {
  133. if imported.ends_with("Tool") {
  134. entries.push(ToolManifestEntry {
  135. name: imported,
  136. source: ToolSource::Base,
  137. });
  138. }
  139. }
  140. }
  141. if line.contains("feature('") && line.contains("Tool") {
  142. if let Some(name) = first_assignment_identifier(line) {
  143. if name.ends_with("Tool") || name.ends_with("Tools") {
  144. entries.push(ToolManifestEntry {
  145. name,
  146. source: ToolSource::Conditional,
  147. });
  148. }
  149. }
  150. }
  151. }
  152. dedupe_tools(entries)
  153. }
  154. #[must_use]
  155. pub fn extract_bootstrap_plan(source: &str) -> BootstrapPlan {
  156. let mut phases = vec![BootstrapPhase::CliEntry];
  157. if source.contains("--version") {
  158. phases.push(BootstrapPhase::FastPathVersion);
  159. }
  160. if source.contains("startupProfiler") {
  161. phases.push(BootstrapPhase::StartupProfiler);
  162. }
  163. if source.contains("--dump-system-prompt") {
  164. phases.push(BootstrapPhase::SystemPromptFastPath);
  165. }
  166. if source.contains("--claude-in-chrome-mcp") {
  167. phases.push(BootstrapPhase::ChromeMcpFastPath);
  168. }
  169. if source.contains("--daemon-worker") {
  170. phases.push(BootstrapPhase::DaemonWorkerFastPath);
  171. }
  172. if source.contains("remote-control") {
  173. phases.push(BootstrapPhase::BridgeFastPath);
  174. }
  175. if source.contains("args[0] === 'daemon'") {
  176. phases.push(BootstrapPhase::DaemonFastPath);
  177. }
  178. if source.contains("args[0] === 'ps'") || source.contains("args.includes('--bg')") {
  179. phases.push(BootstrapPhase::BackgroundSessionFastPath);
  180. }
  181. if source.contains("args[0] === 'new' || args[0] === 'list' || args[0] === 'reply'") {
  182. phases.push(BootstrapPhase::TemplateFastPath);
  183. }
  184. if source.contains("environment-runner") {
  185. phases.push(BootstrapPhase::EnvironmentRunnerFastPath);
  186. }
  187. phases.push(BootstrapPhase::MainRuntime);
  188. BootstrapPlan::from_phases(phases)
  189. }
  190. fn imported_symbols(line: &str) -> Vec<String> {
  191. let Some(after_import) = line.strip_prefix("import ") else {
  192. return Vec::new();
  193. };
  194. let before_from = after_import
  195. .split(" from ")
  196. .next()
  197. .unwrap_or_default()
  198. .trim();
  199. if before_from.starts_with('{') {
  200. return before_from
  201. .trim_matches(|c| c == '{' || c == '}')
  202. .split(',')
  203. .filter_map(|part| {
  204. let trimmed = part.trim();
  205. if trimmed.is_empty() {
  206. return None;
  207. }
  208. Some(trimmed.split_whitespace().next()?.to_string())
  209. })
  210. .collect();
  211. }
  212. let first = before_from.split(',').next().unwrap_or_default().trim();
  213. if first.is_empty() {
  214. Vec::new()
  215. } else {
  216. vec![first.to_string()]
  217. }
  218. }
  219. fn first_assignment_identifier(line: &str) -> Option<String> {
  220. let trimmed = line.trim_start();
  221. let candidate = trimmed.split('=').next()?.trim();
  222. first_identifier(candidate)
  223. }
  224. fn first_identifier(line: &str) -> Option<String> {
  225. let mut out = String::new();
  226. for ch in line.chars() {
  227. if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
  228. out.push(ch);
  229. } else if !out.is_empty() {
  230. break;
  231. }
  232. }
  233. (!out.is_empty()).then_some(out)
  234. }
  235. fn dedupe_commands(entries: Vec<CommandManifestEntry>) -> CommandRegistry {
  236. let mut deduped = Vec::new();
  237. for entry in entries {
  238. let exists = deduped.iter().any(|seen: &CommandManifestEntry| {
  239. seen.name == entry.name && seen.source == entry.source
  240. });
  241. if !exists {
  242. deduped.push(entry);
  243. }
  244. }
  245. CommandRegistry::new(deduped)
  246. }
  247. fn dedupe_tools(entries: Vec<ToolManifestEntry>) -> ToolRegistry {
  248. let mut deduped = Vec::new();
  249. for entry in entries {
  250. let exists = deduped
  251. .iter()
  252. .any(|seen: &ToolManifestEntry| seen.name == entry.name && seen.source == entry.source);
  253. if !exists {
  254. deduped.push(entry);
  255. }
  256. }
  257. ToolRegistry::new(deduped)
  258. }
  259. #[cfg(test)]
  260. mod tests {
  261. use super::*;
  262. fn fixture_paths() -> UpstreamPaths {
  263. let workspace_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
  264. UpstreamPaths::from_workspace_dir(workspace_dir)
  265. }
  266. fn has_upstream_fixture(paths: &UpstreamPaths) -> bool {
  267. paths.commands_path().is_file()
  268. && paths.tools_path().is_file()
  269. && paths.cli_path().is_file()
  270. }
  271. #[test]
  272. fn extracts_non_empty_manifests_from_upstream_repo() {
  273. let paths = fixture_paths();
  274. if !has_upstream_fixture(&paths) {
  275. return;
  276. }
  277. let manifest = extract_manifest(&paths).expect("manifest should load");
  278. assert!(!manifest.commands.entries().is_empty());
  279. assert!(!manifest.tools.entries().is_empty());
  280. assert!(!manifest.bootstrap.phases().is_empty());
  281. }
  282. #[test]
  283. fn detects_known_upstream_command_symbols() {
  284. let paths = fixture_paths();
  285. if !paths.commands_path().is_file() {
  286. return;
  287. }
  288. let commands =
  289. extract_commands(&fs::read_to_string(paths.commands_path()).expect("commands.ts"));
  290. let names: Vec<_> = commands
  291. .entries()
  292. .iter()
  293. .map(|entry| entry.name.as_str())
  294. .collect();
  295. assert!(names.contains(&"addDir"));
  296. assert!(names.contains(&"review"));
  297. assert!(!names.contains(&"INTERNAL_ONLY_COMMANDS"));
  298. }
  299. #[test]
  300. fn detects_known_upstream_tool_symbols() {
  301. let paths = fixture_paths();
  302. if !paths.tools_path().is_file() {
  303. return;
  304. }
  305. let tools = extract_tools(&fs::read_to_string(paths.tools_path()).expect("tools.ts"));
  306. let names: Vec<_> = tools
  307. .entries()
  308. .iter()
  309. .map(|entry| entry.name.as_str())
  310. .collect();
  311. assert!(names.contains(&"AgentTool"));
  312. assert!(names.contains(&"BashTool"));
  313. }
  314. }