Skip to main content

harn_cli/commands/
mod.rs

1pub(crate) mod agents_conformance;
2pub(crate) mod bench;
3pub(crate) mod check;
4pub(crate) mod config_cmd;
5pub(crate) mod connect;
6pub(crate) mod connector;
7pub(crate) mod contracts;
8pub(crate) mod counterfactual;
9pub(crate) mod crystallize;
10pub mod demo;
11pub(crate) mod dev;
12pub(crate) mod diagnostics_catalog;
13pub(crate) mod doctor;
14pub(crate) mod dump_highlight_keywords;
15pub(crate) mod dump_protocol_artifacts;
16pub(crate) mod dump_trigger_quickref;
17pub mod eval_coding_agent;
18pub(crate) mod eval_coding_agent_preset;
19pub mod eval_context;
20pub(crate) mod eval_model_selector;
21pub mod eval_prompt;
22pub(crate) mod eval_prompt_context;
23pub(crate) mod eval_scope_triage;
24pub mod eval_skill_gate;
25pub(crate) mod eval_tool_calls;
26pub(crate) mod explain;
27pub(crate) mod fix;
28pub mod flow;
29pub(crate) mod graph;
30pub(crate) mod hardware;
31pub(crate) mod init;
32pub(crate) mod json_schemas;
33pub(crate) mod local;
34pub(crate) mod local_readiness;
35pub(crate) mod mcp;
36pub(crate) mod merge_captain;
37pub(crate) mod merge_captain_mock;
38pub(crate) mod models;
39pub mod orchestrator;
40pub mod pack;
41pub(crate) mod package_scaffold;
42pub(crate) mod parse_tokens;
43pub mod persona;
44pub mod persona_doctor;
45pub mod persona_scaffold;
46pub mod persona_supervision;
47pub(crate) mod pg_codegen;
48pub mod playground;
49pub(crate) mod portal;
50pub mod precompile;
51pub(crate) mod protocol_conformance;
52pub(crate) mod provider;
53pub(crate) mod provider_capabilities;
54pub(crate) mod provider_support;
55pub(crate) mod providers;
56pub(crate) mod quickstart;
57pub(crate) mod repl;
58pub(crate) mod replay;
59pub(crate) mod routes;
60pub mod run;
61pub(crate) mod scaffold_common;
62pub(crate) mod serve;
63pub(crate) mod session;
64pub(crate) mod skill;
65pub(crate) mod skills;
66pub(crate) mod supervisor;
67pub(crate) mod test;
68pub mod test_bench;
69pub mod time;
70pub(crate) mod tool;
71pub(crate) mod tool_mode_parity;
72pub(crate) mod trace;
73pub mod trigger;
74pub(crate) mod trust;
75pub(crate) mod try_cmd;
76pub(crate) mod upgrade;
77pub(crate) mod viz;
78pub(crate) mod workflow;
79
80use std::path::{Path, PathBuf};
81
82use ignore::WalkBuilder;
83
84const GENERATED_SOURCE_WALK_DIRS: &[&str] = &[
85    ".burin",
86    ".build",
87    ".claude",
88    ".codex",
89    ".git",
90    ".harn",
91    ".harn-runs",
92    ".next",
93    ".svelte-kit",
94    ".turbo",
95    ".venv",
96    "build",
97    "coverage",
98    "dist",
99    "node_modules",
100    "target",
101];
102
103pub(crate) fn should_skip_recursive_source_dir(dir: &Path) -> bool {
104    let Some(name) = dir.file_name().and_then(|name| name.to_str()) else {
105        return false;
106    };
107    GENERATED_SOURCE_WALK_DIRS.contains(&name) || name.starts_with(".harn-")
108}
109
110pub(crate) fn should_skip_recursive_source_file(file: &Path) -> bool {
111    let Some(name) = file.file_name().and_then(|name| name.to_str()) else {
112        return false;
113    };
114    name.starts_with(".harn-")
115}
116
117#[derive(Default)]
118pub(crate) struct SourceTargets {
119    pub(crate) harn: Vec<PathBuf>,
120    pub(crate) prompts: Vec<PathBuf>,
121}
122
123impl SourceTargets {
124    fn sort_and_dedup(&mut self) {
125        self.harn.sort();
126        self.harn.dedup();
127        self.prompts.sort();
128        self.prompts.dedup();
129    }
130}
131
132pub(crate) fn collect_source_targets(
133    targets: &[&str],
134    include_harn: bool,
135    include_prompts: bool,
136) -> SourceTargets {
137    let mut files = SourceTargets::default();
138    for target in targets {
139        let path = Path::new(target);
140        if path.is_dir() {
141            collect_source_targets_dir(path, include_harn, include_prompts, &mut files);
142        } else {
143            push_matching_source_target(path, include_harn, include_prompts, false, &mut files);
144        }
145    }
146    files.sort_and_dedup();
147    files
148}
149
150fn collect_source_targets_dir(
151    dir: &Path,
152    include_harn: bool,
153    include_prompts: bool,
154    files: &mut SourceTargets,
155) {
156    let root = dir.to_path_buf();
157    let mut walker = WalkBuilder::new(dir);
158    walker
159        .hidden(false)
160        .ignore(true)
161        .git_ignore(true)
162        .git_global(true)
163        .git_exclude(true)
164        .require_git(false)
165        .parents(true)
166        .follow_links(false)
167        .filter_entry(move |entry| {
168            let path = entry.path();
169            if path == root {
170                return true;
171            }
172            if path.is_dir() {
173                !should_skip_recursive_source_dir(path)
174            } else {
175                !should_skip_recursive_source_file(path)
176            }
177        });
178
179    for entry in walker.build().filter_map(Result::ok) {
180        let path = entry.path();
181        if entry
182            .file_type()
183            .is_some_and(|file_type| file_type.is_file())
184        {
185            push_matching_source_target(path, include_harn, include_prompts, true, files);
186        }
187    }
188}
189
190fn push_matching_source_target(
191    path: &Path,
192    include_harn: bool,
193    include_prompts: bool,
194    honor_skip_marker: bool,
195    files: &mut SourceTargets,
196) {
197    if include_prompts && is_harn_prompt_file(path) {
198        files.prompts.push(path.to_path_buf());
199    } else if include_harn && is_harn_program_file(path) {
200        let skip_marker = path.with_extension("conformance-skip");
201        if !honor_skip_marker || !skip_marker.exists() {
202            files.harn.push(path.to_path_buf());
203        }
204    }
205}
206
207pub(crate) fn is_harn_program_file(path: &Path) -> bool {
208    let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
209        return false;
210    };
211    if name.ends_with(".harn.prompt") || name.ends_with(".prompt") {
212        return false;
213    }
214    path.extension().is_some_and(|ext| ext == "harn")
215}
216
217pub(crate) fn is_harn_prompt_file(path: &Path) -> bool {
218    let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
219        return false;
220    };
221    name.ends_with(".harn.prompt") || name.ends_with(".prompt")
222}
223
224/// Recursively collect `.harn` files under `dir`, sorted by path. Files with a
225/// sibling `<name>.conformance-skip` marker are excluded — used to temporarily
226/// park tests that are tracking a known regression in an issue so `make test`
227/// + `harn test conformance` can stay green while the fix is in flight.
228pub(crate) fn collect_harn_files(dir: &Path, out: &mut Vec<PathBuf>) {
229    if let Ok(entries) = std::fs::read_dir(dir) {
230        let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
231        entries.sort_by_key(|e| e.path());
232        for entry in entries {
233            let path = entry.path();
234            if path.is_dir() {
235                if should_skip_recursive_source_dir(&path) {
236                    continue;
237                }
238                collect_harn_files(&path, out);
239            } else if should_skip_recursive_source_file(&path) {
240                continue;
241            } else if path.extension().is_some_and(|ext| ext == "harn") {
242                let skip_marker = path.with_extension("conformance-skip");
243                if skip_marker.exists() {
244                    continue;
245                }
246                out.push(path);
247            }
248        }
249    }
250}