66 "net/url"
77 "os"
88 "os/exec"
9+ "strconv"
910 "strings"
1011
1112 "github.com/github/gh-aw/pkg/console"
@@ -146,7 +147,7 @@ func newDefaultsGetCommand() *cobra.Command {
146147
147148func newDefaultsUpdateCommand () * cobra.Command {
148149 var scope , repo , org , enterprise string
149- var yes bool
150+ var yes , dryRun bool
150151
151152 cmd := & cobra.Command {
152153 Use : "update [file]" ,
@@ -161,7 +162,7 @@ func newDefaultsUpdateCommand() *cobra.Command {
161162 if err != nil {
162163 return err
163164 }
164- return defaultsUpdateFromFile (target , inputFile , yes )
165+ return defaultsUpdateFromFile (target , inputFile , yes , dryRun )
165166 },
166167 }
167168
@@ -170,6 +171,7 @@ func newDefaultsUpdateCommand() *cobra.Command {
170171 cmd .Flags ().StringVar (& org , "org" , "" , "Target organization (required for --scope org unless inferable from --repo/current repo)" )
171172 cmd .Flags ().StringVar (& enterprise , "enterprise" , "" , "Target enterprise slug (required for --scope ent)" )
172173 cmd .Flags ().BoolVarP (& yes , "yes" , "y" , false , "Skip confirmation prompt" )
174+ cmd .Flags ().BoolVar (& dryRun , "dry-run" , false , "Preview updates without applying any changes" )
173175 _ = cmd .MarkFlagRequired ("scope" )
174176 return cmd
175177}
@@ -200,18 +202,26 @@ func defaultsGetToFile(target defaultsTarget, outputFile string) error {
200202 return nil
201203}
202204
203- func defaultsUpdateFromFile (target defaultsTarget , inputFile string , skipConfirmation bool ) error {
205+ func defaultsUpdateFromFile (target defaultsTarget , inputFile string , skipConfirmation , dryRun bool ) error {
204206 data , err := os .ReadFile (inputFile )
205207 if err != nil {
206208 return fmt .Errorf ("failed to read defaults file %q: %w" , inputFile , err )
207209 }
208210
209- var file defaultsFile
210- if err := yaml .Unmarshal (data , & file ); err != nil {
211- return fmt .Errorf ("failed to parse defaults file %q: %w" , inputFile , err )
211+ file , err := defaultsParseFile (inputFile , data )
212+ if err != nil {
213+ return err
214+ }
215+ if err := defaultsValidateFile (& file ); err != nil {
216+ return err
212217 }
213218
214219 changes := defaultsBuildUpdateChanges (& file )
220+ if dryRun {
221+ renderDefaultsUpdatePreview (target , inputFile , changes )
222+ fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Dry-run mode enabled; no variables were changed." ))
223+ return nil
224+ }
215225 if err := confirmDefaultsUpdate (target , inputFile , changes , skipConfirmation , console .ConfirmAction ); err != nil {
216226 return err
217227 }
@@ -232,6 +242,60 @@ func defaultsUpdateFromFile(target defaultsTarget, inputFile string, skipConfirm
232242 return nil
233243}
234244
245+ func defaultsParseFile (inputFile string , data []byte ) (defaultsFile , error ) {
246+ var file defaultsFile
247+ if err := yaml .UnmarshalWithOptions (data , & file , yaml .DisallowUnknownField ()); err != nil {
248+ return defaultsFile {}, fmt .Errorf ("failed to parse defaults file %q: %w" , inputFile , err )
249+ }
250+ return file , nil
251+ }
252+
253+ func defaultsValidateFile (file * defaultsFile ) error {
254+ var validationErrors []string
255+
256+ validateNonZeroInt := func (field string , value * string ) {
257+ if value == nil {
258+ return
259+ }
260+ trimmed := strings .TrimSpace (* value )
261+ parsed , err := strconv .ParseInt (trimmed , 10 , 64 )
262+ if err != nil || parsed == 0 {
263+ validationErrors = append (validationErrors , fmt .Sprintf ("%s must be a non-zero integer when set" , field ))
264+ }
265+ }
266+ validatePositiveInt := func (field string , value * string ) {
267+ if value == nil {
268+ return
269+ }
270+ trimmed := strings .TrimSpace (* value )
271+ parsed , err := strconv .ParseInt (trimmed , 10 , 64 )
272+ if err != nil || parsed <= 0 {
273+ validationErrors = append (validationErrors , fmt .Sprintf ("%s must be a positive integer when set" , field ))
274+ }
275+ }
276+ validateNonEmpty := func (field string , value * string ) {
277+ if value == nil {
278+ return
279+ }
280+ if strings .TrimSpace (* value ) == "" {
281+ validationErrors = append (validationErrors , fmt .Sprintf ("%s cannot be empty when set" , field ))
282+ }
283+ }
284+
285+ validateNonZeroInt ("default_max_effective_tokens" , file .DefaultMaxEffectiveTokens )
286+ validatePositiveInt ("default_max_turns" , file .DefaultMaxTurns )
287+ validatePositiveInt ("default_timeout_minutes" , file .DefaultTimeoutMinutes )
288+ validateNonEmpty ("default_detection_model" , file .DefaultDetectionModel )
289+ validateNonEmpty ("default_model_copilot" , file .DefaultModelCopilot )
290+ validateNonEmpty ("default_model_claude" , file .DefaultModelClaude )
291+ validateNonEmpty ("default_model_codex" , file .DefaultModelCodex )
292+
293+ if len (validationErrors ) > 0 {
294+ return fmt .Errorf ("invalid defaults file: %s" , strings .Join (validationErrors , "; " ))
295+ }
296+ return nil
297+ }
298+
235299func defaultsBuildUpdateChanges (file * defaultsFile ) []defaultsUpdateChange {
236300 changes := make ([]defaultsUpdateChange , 0 , len (defaultsBindings ))
237301 for _ , binding := range defaultsBindings {
@@ -261,17 +325,7 @@ func confirmDefaultsUpdate(
261325 skipConfirmation bool ,
262326 confirmAction func (title , affirmative , negative string ) (bool , error ),
263327) error {
264- fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Defaults update preview:" ))
265- fmt .Fprint (os .Stderr , console .RenderStruct (defaultsUpdatePreview {
266- Scope : target .scope ,
267- Target : target .displayName (),
268- File : inputFile ,
269- Fields : len (changes ),
270- }))
271- fmt .Fprintln (os .Stderr )
272- fmt .Fprintln (os .Stderr )
273- fmt .Fprint (os .Stderr , console .RenderStruct (defaultsUpdateRows (changes )))
274- fmt .Fprintln (os .Stderr )
328+ renderDefaultsUpdatePreview (target , inputFile , changes )
275329
276330 if skipConfirmation {
277331 fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Skipping confirmation because --yes was provided." ))
@@ -292,6 +346,20 @@ func confirmDefaultsUpdate(
292346 return nil
293347}
294348
349+ func renderDefaultsUpdatePreview (target defaultsTarget , inputFile string , changes []defaultsUpdateChange ) {
350+ fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Defaults update preview:" ))
351+ fmt .Fprint (os .Stderr , console .RenderStruct (defaultsUpdatePreview {
352+ Scope : target .scope ,
353+ Target : target .displayName (),
354+ File : inputFile ,
355+ Fields : len (changes ),
356+ }))
357+ fmt .Fprintln (os .Stderr )
358+ fmt .Fprintln (os .Stderr )
359+ fmt .Fprint (os .Stderr , console .RenderStruct (defaultsUpdateRows (changes )))
360+ fmt .Fprintln (os .Stderr )
361+ }
362+
295363func defaultsUpdateRows (changes []defaultsUpdateChange ) []defaultsUpdateRow {
296364 rows := make ([]defaultsUpdateRow , 0 , len (changes ))
297365 for _ , change := range changes {
0 commit comments