@@ -5,43 +5,83 @@ use anyhow::{Context, bail};
55use crate :: DslLanguage ;
66
77/// Detect the DSL language used in a project by scanning `.harmont/` for file
8- /// extensions.
8+ /// extensions. Prefers **TypeScript** when both are present (the `hm run`
9+ /// default).
910///
1011/// # Errors
1112///
1213/// - The `.harmont/` directory does not exist.
1314/// - No `.py` or `.ts` files are found inside `.harmont/`.
14- /// - Both `.py` and `.ts` files are present (mixed languages).
1515pub fn detect_language ( repo_root : & Path ) -> anyhow:: Result < DslLanguage > {
1616 let harmont_dir = repo_root. join ( ".harmont" ) ;
17+ if !harmont_dir. is_dir ( ) {
18+ bail ! ( "no .harmont/ directory found in {}" , repo_root. display( ) ) ;
19+ }
20+ match scan_extensions ( repo_root) ? {
21+ // When both languages are present, prefer TypeScript.
22+ ( _, true ) => Ok ( DslLanguage :: TypeScript ) ,
23+ ( true , false ) => Ok ( DslLanguage :: Python ) ,
24+ ( false , false ) => bail ! ( "no .py or .ts files found in {}" , harmont_dir. display( ) ) ,
25+ }
26+ }
1727
28+ /// Like [`detect_language`] but prefers **Python** when both are present.
29+ ///
30+ /// Used by the machine-facing `hm pipelines` / `hm render` commands that the
31+ /// backend shells out to: the Python path is the fully-supported one (the
32+ /// discovery envelope is Python-only today), so a repo carrying both a `.py`
33+ /// and a redundant `.ts` resolves to Python rather than the unsupported TS
34+ /// registry. `hm run` keeps the TypeScript-preferring [`detect_language`].
35+ ///
36+ /// # Errors
37+ ///
38+ /// - The `.harmont/` directory does not exist.
39+ /// - No `.py` or `.ts` files are found inside `.harmont/`.
40+ pub fn detect_language_python_first ( repo_root : & Path ) -> anyhow:: Result < DslLanguage > {
41+ let harmont_dir = repo_root. join ( ".harmont" ) ;
1842 if !harmont_dir. is_dir ( ) {
1943 bail ! ( "no .harmont/ directory found in {}" , repo_root. display( ) ) ;
2044 }
45+ match scan_extensions ( repo_root) ? {
46+ ( true , _) => Ok ( DslLanguage :: Python ) ,
47+ ( false , true ) => Ok ( DslLanguage :: TypeScript ) ,
48+ ( false , false ) => bail ! ( "no .py or .ts files found in {}" , harmont_dir. display( ) ) ,
49+ }
50+ }
51+
52+ /// True when `.harmont/` exists and holds at least one `.py` or `.ts` file.
53+ ///
54+ /// The backend fans pipeline discovery out across every repo in an
55+ /// installation, most of which declare no pipelines at all. Those repos should
56+ /// yield an empty registry, not an error — callers use this to short-circuit to
57+ /// an empty envelope instead of calling [`detect_language_python_first`].
58+ #[ must_use]
59+ pub fn has_pipeline_files ( repo_root : & Path ) -> bool {
60+ matches ! ( scan_extensions( repo_root) , Ok ( ( py, ts) ) if py || ts)
61+ }
62+
63+ /// Scan `.harmont/` and report `(has_py, has_ts)`. A missing `.harmont/`
64+ /// directory yields `(false, false)`; an unreadable one is an error.
65+ fn scan_extensions ( repo_root : & Path ) -> anyhow:: Result < ( bool , bool ) > {
66+ let harmont_dir = repo_root. join ( ".harmont" ) ;
67+ if !harmont_dir. is_dir ( ) {
68+ return Ok ( ( false , false ) ) ;
69+ }
2170
2271 let entries = std:: fs:: read_dir ( & harmont_dir)
2372 . with_context ( || format ! ( "failed to read {}" , harmont_dir. display( ) ) ) ?;
2473
2574 let mut has_py = false ;
2675 let mut has_ts = false ;
27-
2876 for entry in entries {
2977 let entry = entry?;
30- let path = entry. path ( ) ;
31-
32- match path. extension ( ) . and_then ( |e| e. to_str ( ) ) {
78+ match entry. path ( ) . extension ( ) . and_then ( |e| e. to_str ( ) ) {
3379 Some ( "py" ) => has_py = true ,
3480 Some ( "ts" ) => has_ts = true ,
3581 _ => { }
3682 }
3783 }
38-
39- match ( has_py, has_ts) {
40- // When both languages are present, prefer TypeScript.
41- ( _, true ) => Ok ( DslLanguage :: TypeScript ) ,
42- ( true , false ) => Ok ( DslLanguage :: Python ) ,
43- ( false , false ) => bail ! ( "no .py or .ts files found in {}" , harmont_dir. display( ) ) ,
44- }
84+ Ok ( ( has_py, has_ts) )
4585}
4686
4787#[ cfg( test) ]
@@ -107,4 +147,47 @@ mod tests {
107147 "unexpected error: {msg}"
108148 ) ;
109149 }
150+
151+ #[ test]
152+ fn python_first_prefers_python_when_mixed ( ) {
153+ let tmp = setup ( & [ "ci.py" , "deploy.ts" ] ) ;
154+ assert_eq ! (
155+ detect_language_python_first( tmp. path( ) ) . unwrap( ) ,
156+ DslLanguage :: Python
157+ ) ;
158+ }
159+
160+ #[ test]
161+ fn python_first_falls_back_to_typescript_when_only_ts ( ) {
162+ let tmp = setup ( & [ "ci.ts" ] ) ;
163+ assert_eq ! (
164+ detect_language_python_first( tmp. path( ) ) . unwrap( ) ,
165+ DslLanguage :: TypeScript
166+ ) ;
167+ }
168+
169+ #[ test]
170+ fn python_first_no_harmont_dir_is_error ( ) {
171+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
172+ let err = detect_language_python_first ( tmp. path ( ) ) . unwrap_err ( ) ;
173+ assert ! (
174+ err. to_string( ) . contains( "no .harmont/ directory" ) ,
175+ "unexpected error: {err}"
176+ ) ;
177+ }
178+
179+ #[ test]
180+ fn has_pipeline_files_true_for_py_and_ts ( ) {
181+ assert ! ( has_pipeline_files( setup( & [ "ci.py" ] ) . path( ) ) ) ;
182+ assert ! ( has_pipeline_files( setup( & [ "ci.ts" ] ) . path( ) ) ) ;
183+ assert ! ( has_pipeline_files( setup( & [ "ci.py" , "deploy.ts" ] ) . path( ) ) ) ;
184+ }
185+
186+ #[ test]
187+ fn has_pipeline_files_false_for_missing_or_empty_harmont ( ) {
188+ // No .harmont/ directory at all.
189+ assert ! ( !has_pipeline_files( TempDir :: new( ) . unwrap( ) . path( ) ) ) ;
190+ // .harmont/ exists but declares no .py/.ts files.
191+ assert ! ( !has_pipeline_files( setup( & [ "README.md" ] ) . path( ) ) ) ;
192+ }
110193}
0 commit comments