@@ -85,6 +85,8 @@ export class Config {
8585
8686 private static PROJECT_INDICATORS = [
8787 '.git' ,
88+ '.herb' ,
89+ '.herb.yml' ,
8890 'Gemfile' ,
8991 'package.json' ,
9092 'Rakefile' ,
@@ -424,6 +426,71 @@ export class Config {
424426 }
425427 }
426428
429+ /**
430+ * Find the project root by walking up from a given path.
431+ * Looks for .herb.yml first, then falls back to project indicators
432+ * (.git, Gemfile, package.json, etc.)
433+ *
434+ * @param startPath - File or directory path to start searching from
435+ * @returns The project root directory path
436+ */
437+ static async findProjectRoot ( startPath : string ) : Promise < string > {
438+ const { projectRoot } = await this . findConfigFile ( startPath )
439+
440+ return projectRoot
441+ }
442+
443+ /**
444+ * Synchronous version of findProjectRoot for use in CLIs.
445+ *
446+ * @param startPath - File or directory path to start searching from
447+ * @returns The project root directory path
448+ */
449+ static findProjectRootSync ( startPath : string ) : string {
450+ const fsSync = require ( 'fs' )
451+ let currentPath = path . resolve ( startPath )
452+
453+ try {
454+ const stats = fsSync . statSync ( currentPath )
455+
456+ if ( stats . isFile ( ) ) {
457+ currentPath = path . dirname ( currentPath )
458+ }
459+ } catch {
460+ currentPath = path . resolve ( process . cwd ( ) )
461+ }
462+
463+ while ( true ) {
464+ const configPath = path . join ( currentPath , this . configPath )
465+
466+ try {
467+ fsSync . accessSync ( configPath )
468+
469+ return currentPath
470+ } catch {
471+ // Config not in this directory, continue
472+ }
473+
474+ for ( const indicator of this . PROJECT_INDICATORS ) {
475+ try {
476+ fsSync . accessSync ( path . join ( currentPath , indicator ) )
477+
478+ return currentPath
479+ } catch {
480+ // Indicator not found, continue checking
481+ }
482+ }
483+
484+ const parentPath = path . dirname ( currentPath )
485+
486+ if ( parentPath === currentPath ) {
487+ return process . cwd ( )
488+ }
489+
490+ currentPath = parentPath
491+ }
492+ }
493+
427494 /**
428495 * Read raw YAML content from a config file.
429496 * Handles both explicit .herb.yml paths and directory paths.
0 commit comments