lib.rs 11 KB

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