@@ -21,6 +21,7 @@ import (
2121
2222 "github.com/pardeike/gabs/internal/config"
2323 "github.com/pardeike/gabs/internal/mcp"
24+ "github.com/pardeike/gabs/internal/steam"
2425 "github.com/pardeike/gabs/internal/util"
2526 "github.com/pardeike/gabs/internal/version"
2627)
@@ -190,6 +191,8 @@ Game management:
190191 gabs games add <id> Add a new game configuration (interactive)
191192 gabs games remove <id> Remove a game configuration
192193 gabs games show <id> Show details for a game
194+ gabs games doctor <id> Diagnose one game configuration
195+ gabs games repair <id> Apply safe repairs for one game configuration
193196
194197Examples:
195198 # Start GABS MCP server (stdio)
@@ -305,6 +308,18 @@ func manageGames(ctx context.Context, log util.Logger, opts options, args []stri
305308 return 2
306309 }
307310 return showGame (log , args [1 ], opts .configDir )
311+ case "doctor" :
312+ if len (args ) < 2 {
313+ fmt .Fprintf (os .Stderr , "games doctor requires a game ID\n " )
314+ return 2
315+ }
316+ return doctorGame (log , args [1 ], opts .configDir )
317+ case "repair" :
318+ if len (args ) < 2 {
319+ fmt .Fprintf (os .Stderr , "games repair requires a game ID\n " )
320+ return 2
321+ }
322+ return repairGame (log , args [1 ], opts .configDir )
308323 default :
309324 fmt .Fprintf (os .Stderr , "unknown games action: %s\n " , action )
310325 return 2
@@ -370,18 +385,21 @@ func addGame(log util.Logger, gameID string, configDir string) int {
370385 game := config.GameConfig {
371386 ID : gameID ,
372387 Name : promptString ("Game Name" , gameID ),
373- LaunchMode : promptChoice ("Launch Mode" , "DirectPath" , []string {"DirectPath" , "SteamAppId" , "EpicAppId" , "CustomCommand" }),
388+ LaunchMode : promptChoice ("Launch Mode" , "DirectPath" , []string {"DirectPath" , "SteamManaged" , " SteamAppId" , "EpicAppId" , "CustomCommand" }),
374389 }
375390
376391 // Enhance target prompt for DirectPath mode with platform-specific help
377392 var targetPrompt string
378- if game .LaunchMode == "DirectPath" {
393+ switch game .LaunchMode {
394+ case "DirectPath" :
379395 if runtime .GOOS == "darwin" {
380396 targetPrompt = "Target (executable path or .app bundle)"
381397 } else {
382398 targetPrompt = "Target (executable path)"
383399 }
384- } else {
400+ case "SteamManaged" , "SteamAppId" :
401+ targetPrompt = "Target (Steam App ID)"
402+ default :
385403 targetPrompt = "Target (path/id)"
386404 }
387405
@@ -395,7 +413,7 @@ func addGame(log util.Logger, gameID string, configDir string) int {
395413 }
396414 }
397415
398- if game .LaunchMode == "DirectPath" || game .LaunchMode == "CustomCommand" {
416+ if game .LaunchMode == "DirectPath" || game .LaunchMode == "SteamManaged" || game . LaunchMode == " CustomCommand" {
399417 workingDir := promptString ("Working Directory (optional)" , "" )
400418 if workingDir != "" {
401419 game .WorkingDir = workingDir
@@ -486,13 +504,133 @@ func showGame(log util.Logger, gameID string, configDir string) int {
486504 if game .StopProcessName != "" {
487505 fmt .Printf (" Stop Process Name: %s\n " , game .StopProcessName )
488506 }
507+ if game .GABPMode != "" {
508+ fmt .Printf (" GABP Mode: %s\n " , game .GABPMode )
509+ }
489510 if game .Description != "" {
490511 fmt .Printf (" Description: %s\n " , game .Description )
491512 }
492513
493514 return 0
494515}
495516
517+ func doctorGame (log util.Logger , gameID string , configDir string ) int {
518+ gamesConfig , err := config .LoadGamesConfigFromDir (configDir )
519+ if err != nil {
520+ log .Errorw ("failed to load games config" , "error" , err )
521+ return 1
522+ }
523+
524+ game , exists := gamesConfig .GetGame (gameID )
525+ if ! exists {
526+ fmt .Printf ("Game '%s' not found.\n " , gameID )
527+ return 1
528+ }
529+
530+ fmt .Printf ("Game: %s\n " , game .ID )
531+ fmt .Printf ("Launch Mode: %s\n " , game .LaunchMode )
532+ if game .Target != "" {
533+ fmt .Printf ("Target: %s\n " , game .Target )
534+ }
535+
536+ if err := game .Validate (); err != nil {
537+ fmt .Printf ("Configuration: invalid (%v)\n " , err )
538+ } else {
539+ fmt .Println ("Configuration: valid" )
540+ }
541+
542+ switch game .LaunchMode {
543+ case "SteamAppId" :
544+ fmt .Println ("Steam launch: launcher URL mode" )
545+ fmt .Println ("Bridge environment: not guaranteed on the real game process" )
546+ app , err := steam .ResolveApp (game .Target )
547+ if err != nil {
548+ fmt .Printf ("Managed Steam readiness: failed (%v)\n " , err )
549+ return 1
550+ }
551+ printSteamAppResolution (app )
552+ fmt .Printf ("Recommended repair: gabs games repair %s\n " , game .ID )
553+ case "SteamManaged" :
554+ fmt .Println ("Steam launch: managed executable mode" )
555+ app , err := steam .ResolveApp (game .Target )
556+ if err != nil {
557+ fmt .Printf ("Managed Steam readiness: failed (%v)\n " , err )
558+ return 1
559+ }
560+ printSteamAppResolution (app )
561+ ok , content , err := steam .CheckAppIDFile (app )
562+ if err != nil {
563+ fmt .Printf ("Steam app id file: unreadable (%v)\n " , err )
564+ return 1
565+ }
566+ if ok {
567+ fmt .Printf ("Steam app id file: ready (%s)\n " , app .AppIDFilePath )
568+ } else if content == "" {
569+ fmt .Printf ("Steam app id file: missing (%s)\n " , app .AppIDFilePath )
570+ fmt .Printf ("Recommended repair: gabs games repair %s\n " , game .ID )
571+ } else {
572+ fmt .Printf ("Steam app id file: wrong id %q at %s\n " , content , app .AppIDFilePath )
573+ return 1
574+ }
575+ default :
576+ if game .Target != "" {
577+ if _ , err := os .Stat (game .Target ); err != nil {
578+ fmt .Printf ("Target path: not found (%v)\n " , err )
579+ return 1
580+ }
581+ fmt .Println ("Target path: found" )
582+ }
583+ }
584+
585+ return 0
586+ }
587+
588+ func repairGame (log util.Logger , gameID string , configDir string ) int {
589+ gamesConfig , err := config .LoadGamesConfigFromDir (configDir )
590+ if err != nil {
591+ log .Errorw ("failed to load games config" , "error" , err )
592+ return 1
593+ }
594+
595+ game , exists := gamesConfig .GetGame (gameID )
596+ if ! exists {
597+ fmt .Printf ("Game '%s' not found.\n " , gameID )
598+ return 1
599+ }
600+
601+ switch game .LaunchMode {
602+ case "SteamAppId" , "SteamManaged" :
603+ app , err := steam .ResolveApp (game .Target )
604+ if err != nil {
605+ fmt .Printf ("Steam repair failed: %v\n " , err )
606+ return 1
607+ }
608+ if err := steam .EnsureAppIDFile (app ); err != nil {
609+ fmt .Printf ("Steam app id repair failed: %v\n " , err )
610+ return 1
611+ }
612+ if game .LaunchMode == "SteamAppId" {
613+ game .LaunchMode = "SteamManaged"
614+ gamesConfig .Games [game .ID ] = * game
615+ if err := backupGamesConfig (configDir ); err != nil {
616+ fmt .Printf ("Failed to back up config: %v\n " , err )
617+ return 1
618+ }
619+ if err := config .SaveGamesConfigToDir (gamesConfig , configDir ); err != nil {
620+ log .Errorw ("failed to save games config" , "error" , err )
621+ return 1
622+ }
623+ fmt .Printf ("Updated '%s' from SteamAppId to SteamManaged.\n " , game .ID )
624+ }
625+ fmt .Printf ("Steam app id file ready: %s\n " , app .AppIDFilePath )
626+ printSteamAppResolution (app )
627+ return 0
628+ default :
629+ fmt .Printf ("No automatic repair available for launch mode %s.\n " , game .LaunchMode )
630+ return 0
631+ }
632+ }
633+
496634// === Helper Functions ===
497635
498636func showGamesUsage () {
@@ -501,15 +639,44 @@ func showGamesUsage() {
501639 gabs games add <id> Add a new game configuration (interactive)
502640 gabs games remove <id> Remove a game configuration
503641 gabs games show <id> Show details for a game
642+ gabs games doctor <id> Diagnose one game configuration
643+ gabs games repair <id> Apply safe repairs for one game configuration
504644
505645Examples:
506646 gabs games list # See game IDs only (AI-friendly)
507647 gabs games add factory # Add a new game called 'factory'
508648 gabs games show factory # View configuration for 'factory'
649+ gabs games doctor factory # Diagnose launch configuration
650+ gabs games repair factory # Apply safe launch repairs
509651 gabs games remove factory # Remove the 'factory' configuration
510652` )
511653}
512654
655+ func printSteamAppResolution (app steam.App ) {
656+ fmt .Printf ("Steam app: %s" , app .AppID )
657+ if app .Name != "" {
658+ fmt .Printf (" (%s)" , app .Name )
659+ }
660+ fmt .Println ()
661+ fmt .Printf ("Install path: %s\n " , app .InstallPath )
662+ fmt .Printf ("Executable: %s\n " , app .Executable )
663+ fmt .Printf ("Working directory: %s\n " , app .WorkingDir )
664+ }
665+
666+ func backupGamesConfig (configDir string ) error {
667+ cp , err := config .NewConfigPaths (configDir )
668+ if err != nil {
669+ return err
670+ }
671+ configPath := cp .GetMainConfigPath ()
672+ data , err := os .ReadFile (configPath )
673+ if err != nil {
674+ return err
675+ }
676+ backupPath := fmt .Sprintf ("%s.bak.%s" , configPath , time .Now ().Format ("20060102-150405" ))
677+ return os .WriteFile (backupPath , data , 0644 )
678+ }
679+
513680// isInteractive checks if the program is running in an interactive terminal
514681func isInteractive () bool {
515682 // Check if stdin is a terminal
0 commit comments