Skip to main content

ninmu_compat_harness/
lib.rs

1use 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    /// Returns the repository root path.
22    #[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}