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