@@ -5,39 +5,53 @@ use anyhow::{Context, bail};
55use crate :: DslLanguage ;
66
77/// Detect the DSL language used in a project by scanning `.hm/` for file
8- /// extensions.
9- ///
10- /// A repo carrying **both** a `.py` and a `.ts` pipeline is rejected rather
11- /// than silently tie-broken: `hm run` (local) and the backend discovery path
12- /// (`hm pipelines` / `hm render`) both route through this one resolver, so a
13- /// tie-break here would let the local build run one language while cloud
14- /// discovery built the other. Failing fast keeps the two in lockstep. Keep a
15- /// single pipeline language per `.hm/` directory.
8+ /// extensions. Prefers **TypeScript** when both are present (the `hm run`
9+ /// default).
1610///
1711/// # Errors
1812///
1913/// - The `.hm/` directory does not exist.
2014/// - No `.py` or `.ts` files are found inside `.hm/`.
21- /// - Both `.py` and `.ts` files are present (ambiguous language).
2215pub fn detect_language ( repo_root : & Path ) -> anyhow:: Result < DslLanguage > {
2316 let harmont_dir = repo_root. join ( ".hm" ) ;
2417 if !harmont_dir. is_dir ( ) {
2518 bail ! ( "no .hm/ directory found in {}" , repo_root. display( ) ) ;
2619 }
2720 let langs = scan_extensions ( repo_root) ?;
28- match ( langs. has_py , langs. has_ts ) {
29- ( true , true ) => bail ! (
30- "ambiguous pipeline language in {dir}: found both Python (.py) and \
31- TypeScript (.ts) pipeline files\n \
32- → keep exactly one pipeline language per .hm/ directory; remove the \
33- extra .py or .ts files\n \
34- (otherwise `hm run` and cloud discovery could resolve different \
35- languages for the same repo)",
36- dir = harmont_dir. display( )
37- ) ,
38- ( false , true ) => Ok ( DslLanguage :: TypeScript ) ,
39- ( true , false ) => Ok ( DslLanguage :: Python ) ,
40- ( false , false ) => bail ! ( "no .py or .ts files found in {}" , harmont_dir. display( ) ) ,
21+ if langs. has_ts {
22+ // When both languages are present, prefer TypeScript.
23+ Ok ( DslLanguage :: TypeScript )
24+ } else if langs. has_py {
25+ Ok ( DslLanguage :: Python )
26+ } else {
27+ bail ! ( "no .py or .ts files found in {}" , harmont_dir. display( ) )
28+ }
29+ }
30+
31+ /// Like [`detect_language`] but prefers **Python** when both are present.
32+ ///
33+ /// Used by the machine-facing `hm pipelines` / `hm render` commands that the
34+ /// backend shells out to: the Python path is the fully-supported one (the
35+ /// discovery envelope is Python-only today), so a repo carrying both a `.py`
36+ /// and a redundant `.ts` resolves to Python rather than the unsupported TS
37+ /// registry. `hm run` keeps the TypeScript-preferring [`detect_language`].
38+ ///
39+ /// # Errors
40+ ///
41+ /// - The `.hm/` directory does not exist.
42+ /// - No `.py` or `.ts` files are found inside `.hm/`.
43+ pub fn detect_language_python_first ( repo_root : & Path ) -> anyhow:: Result < DslLanguage > {
44+ let harmont_dir = repo_root. join ( ".hm" ) ;
45+ if !harmont_dir. is_dir ( ) {
46+ bail ! ( "no .hm/ directory found in {}" , repo_root. display( ) ) ;
47+ }
48+ let langs = scan_extensions ( repo_root) ?;
49+ if langs. has_py {
50+ Ok ( DslLanguage :: Python )
51+ } else if langs. has_ts {
52+ Ok ( DslLanguage :: TypeScript )
53+ } else {
54+ bail ! ( "no .py or .ts files found in {}" , harmont_dir. display( ) )
4155 }
4256}
4357
@@ -46,7 +60,7 @@ pub fn detect_language(repo_root: &Path) -> anyhow::Result<DslLanguage> {
4660/// The backend fans pipeline discovery out across every repo in an
4761/// installation, most of which declare no pipelines at all. Those repos should
4862/// yield an empty registry, not an error — callers use this to short-circuit to
49- /// an empty envelope instead of calling [`detect_language `].
63+ /// an empty envelope instead of calling [`detect_language_python_first `].
5064#[ must_use]
5165pub fn has_pipeline_files ( repo_root : & Path ) -> bool {
5266 matches ! ( scan_extensions( repo_root) , Ok ( langs) if langs. has_py || langs. has_ts)
@@ -120,18 +134,10 @@ mod tests {
120134 }
121135
122136 #[ test]
123- fn mixed_languages_is_ambiguous_error ( ) {
124- // A repo declaring both .py and .ts must fail loudly rather than
125- // silently tie-break — otherwise local `hm run` and cloud discovery
126- // could resolve different languages for the same repo.
137+ fn mixed_languages_prefers_typescript ( ) {
127138 let tmp = setup ( & [ "ci.py" , "deploy.ts" ] ) ;
128- let err = detect_language ( tmp. path ( ) ) . unwrap_err ( ) ;
129- let msg = err. to_string ( ) ;
130- assert ! (
131- msg. contains( "ambiguous pipeline language" ) ,
132- "unexpected error: {msg}"
133- ) ;
134- assert ! ( msg. contains( ".py" ) && msg. contains( ".ts" ) , "msg: {msg}" ) ;
139+ let lang = detect_language ( tmp. path ( ) ) . unwrap ( ) ;
140+ assert_eq ! ( lang, DslLanguage :: TypeScript ) ;
135141 }
136142
137143 #[ test]
@@ -155,6 +161,34 @@ mod tests {
155161 ) ;
156162 }
157163
164+ #[ test]
165+ fn python_first_prefers_python_when_mixed ( ) {
166+ let tmp = setup ( & [ "ci.py" , "deploy.ts" ] ) ;
167+ assert_eq ! (
168+ detect_language_python_first( tmp. path( ) ) . unwrap( ) ,
169+ DslLanguage :: Python
170+ ) ;
171+ }
172+
173+ #[ test]
174+ fn python_first_falls_back_to_typescript_when_only_ts ( ) {
175+ let tmp = setup ( & [ "ci.ts" ] ) ;
176+ assert_eq ! (
177+ detect_language_python_first( tmp. path( ) ) . unwrap( ) ,
178+ DslLanguage :: TypeScript
179+ ) ;
180+ }
181+
182+ #[ test]
183+ fn python_first_no_harmont_dir_is_error ( ) {
184+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
185+ let err = detect_language_python_first ( tmp. path ( ) ) . unwrap_err ( ) ;
186+ assert ! (
187+ err. to_string( ) . contains( "no .hm/ directory" ) ,
188+ "unexpected error: {err}"
189+ ) ;
190+ }
191+
158192 #[ test]
159193 fn has_pipeline_files_true_for_py_and_ts ( ) {
160194 assert ! ( has_pipeline_files( setup( & [ "ci.py" ] ) . path( ) ) ) ;
0 commit comments