@@ -3,6 +3,7 @@ package sandbox
33import (
44 "fmt"
55 "strings"
6+ "time"
67
78 "github.com/pterm/pterm"
89 "github.com/urfave/cli/v2"
@@ -37,6 +38,10 @@ func newEditCommand() *cli.Command {
3738 Name : "add-ssh-key" ,
3839 Usage : "Path to a public-key file to add (repeatable)" ,
3940 },
41+ & cli.StringFlag {
42+ Name : "auto-pause" ,
43+ Usage : "Pause automatically after this long with no activity (e.g. 10m, 1h). Use `off` to disable." ,
44+ },
4045 },
4146 Action : runEdit ,
4247 }
@@ -51,8 +56,8 @@ func runEdit(c *cli.Context) error {
5156 // urfave/cli v2 stops flag parsing at the first positional, so
5257 // `edit my-sb --ingress on` loses `--ingress`. Re-scan args by hand
5358 // so users can put flags anywhere.
54- ref , ingressFlag , sshFiles := parseEditArgs (c )
55- hasFlagChanges := ingressFlag != "" || len (sshFiles ) > 0
59+ ref , ingressFlag , autoPauseFlag , sshFiles := parseEditArgs (c )
60+ hasFlagChanges := ingressFlag != "" || autoPauseFlag != "" || len (sshFiles ) > 0
5661
5762 // Resolve the sandbox first — either from positional or via picker.
5863 id , label , err := resolveTarget (c , client , ref )
@@ -73,6 +78,11 @@ func runEdit(c *cli.Context) error {
7378 return err
7479 }
7580 }
81+ if autoPauseFlag != "" {
82+ if err := applyAutoPauseFlag (c , client , label , id , autoPauseFlag ); err != nil {
83+ return err
84+ }
85+ }
7686 if len (sshFiles ) > 0 {
7787 if err := applyAddSSHKeys (c , client , label , id , sshFiles ); err != nil {
7888 return err
@@ -83,7 +93,7 @@ func runEdit(c *cli.Context) error {
8393
8494 // No flags — interactive only.
8595 if ! terminal .IsInteractive () {
86- return fmt .Errorf ("nothing to do — pass --ingress or --add-ssh-key, or run again on a terminal for an interactive menu" )
96+ return fmt .Errorf ("nothing to do — pass --ingress, --auto-pause, or --add-ssh-key, or run again on a terminal for an interactive menu" )
8797 }
8898 return runEditMenu (c , client , label , id )
8999}
@@ -93,10 +103,9 @@ func runEdit(c *cli.Context) error {
93103// as the sandbox ref, and recognises `--ingress <value>`,
94104// `--ingress=<value>`, `--add-ssh-key <path>`, `--add-ssh-key=<path>`
95105// in any position.
96- func parseEditArgs (c * cli.Context ) (ref , ingressVal string , sshPaths []string ) {
97- // Start with whatever urfave/cli already parsed (covers
98- // flags-before-positional). Use as defaults.
106+ func parseEditArgs (c * cli.Context ) (ref , ingressVal , autoPauseVal string , sshPaths []string ) {
99107 ingressVal = strings .ToLower (strings .TrimSpace (c .String ("ingress" )))
108+ autoPauseVal = strings .TrimSpace (c .String ("auto-pause" ))
100109 sshPaths = append ([]string {}, c .StringSlice ("add-ssh-key" )... )
101110
102111 args := c .Args ().Slice ()
@@ -110,6 +119,13 @@ func parseEditArgs(c *cli.Context) (ref, ingressVal string, sshPaths []string) {
110119 }
111120 case strings .HasPrefix (a , "--ingress=" ):
112121 ingressVal = strings .ToLower (strings .TrimSpace (strings .TrimPrefix (a , "--ingress=" )))
122+ case a == "--auto-pause" :
123+ if i + 1 < len (args ) {
124+ autoPauseVal = strings .TrimSpace (args [i + 1 ])
125+ i ++
126+ }
127+ case strings .HasPrefix (a , "--auto-pause=" ):
128+ autoPauseVal = strings .TrimSpace (strings .TrimPrefix (a , "--auto-pause=" ))
113129 case a == "--add-ssh-key" :
114130 if i + 1 < len (args ) {
115131 sshPaths = append (sshPaths , strings .TrimSpace (args [i + 1 ]))
@@ -123,7 +139,7 @@ func parseEditArgs(c *cli.Context) (ref, ingressVal string, sshPaths []string) {
123139 }
124140 }
125141 }
126- return ref , ingressVal , sshPaths
142+ return ref , ingressVal , autoPauseVal , sshPaths
127143}
128144
129145// resolveTarget figures out which sandbox the user wants to edit. With
@@ -156,7 +172,11 @@ func runEditMenu(c *cli.Context, client *api.SandboxClient, label, id string) er
156172
157173 fmt .Println ()
158174 pterm .NewStyle (pterm .FgCyan , pterm .Bold ).Printfln (" Editing %s" , refLabel (label , id ))
159- header := fmt .Sprintf (" Public URL: %s SSH keys: %d" , onOff (sb .IngressEnabled ), len (sb .SSHPubkeys ))
175+ autoPauseLabel := "off"
176+ if sb .AutoPauseAfterSeconds != nil {
177+ autoPauseLabel = "pauses after " + formatDuration (time .Duration (* sb .AutoPauseAfterSeconds )* time .Second ) + " idle"
178+ }
179+ header := fmt .Sprintf (" Public URL: %s SSH keys: %d Auto-pause: %s" , onOff (sb .IngressEnabled ), len (sb .SSHPubkeys ), autoPauseLabel )
160180 if bw != nil {
161181 bwLine := fmt .Sprintf ("%s used of %s" , humanBytes (bw .UsedBytes ), humanBytes (bw .QuotaBytes ))
162182 if bw .Capped {
@@ -171,11 +191,12 @@ func runEditMenu(c *cli.Context, client *api.SandboxClient, label, id string) er
171191 optIngress = "Toggle public URL"
172192 optSSH = "Add an SSH key"
173193 optBandwidth = "Top up bandwidth"
194+ optAutoPause = "Auto-pause when idle"
174195 optDone = "Done"
175196 )
176197 for {
177198 choice , err := pterm .DefaultInteractiveSelect .
178- WithOptions ([]string {optIngress , optSSH , optBandwidth , optDone }).
199+ WithOptions ([]string {optIngress , optSSH , optBandwidth , optAutoPause , optDone }).
179200 WithDefaultText ("What would you like to change?" ).
180201 Show ()
181202 if err != nil {
@@ -258,6 +279,28 @@ func runEditMenu(c *cli.Context, client *api.SandboxClient, label, id string) er
258279 humanBytes (bytes ),
259280 humanBytes (updated .UsedBytes ), humanBytes (updated .QuotaBytes ),
260281 humanBytes (updated .RemainingBytes ))
282+ case optAutoPause :
283+ current := "off"
284+ if sb .AutoPauseAfterSeconds != nil {
285+ current = formatDuration (time .Duration (* sb .AutoPauseAfterSeconds ) * time .Second )
286+ }
287+ input , err := pterm .DefaultInteractiveTextInput .
288+ WithDefaultText (fmt .Sprintf ("Pause after how long with no activity? (current: %s — e.g. 10m, 1h, or 'off')" , current )).
289+ Show ()
290+ if err != nil {
291+ return fmt .Errorf ("could not read your input: %w" , err )
292+ }
293+ input = strings .TrimSpace (input )
294+ if input == "" {
295+ continue
296+ }
297+ if err := applyAutoPauseFlag (c , client , label , id , input ); err != nil {
298+ pterm .Error .Printfln ("%v" , err )
299+ continue
300+ }
301+ if refreshed , err := client .GetSandbox (c .Context , id ); err == nil {
302+ sb = refreshed
303+ }
261304 case optDone :
262305 return nil
263306 }
@@ -308,6 +351,32 @@ func applyAddSSHKeys(c *cli.Context, client *api.SandboxClient, label, id string
308351 return nil
309352}
310353
354+ // applyAutoPauseFlag handles --auto-pause <value>: "off" disables, a duration enables.
355+ func applyAutoPauseFlag (c * cli.Context , client * api.SandboxClient , label , id , value string ) error {
356+ var seconds * int
357+ switch strings .ToLower (value ) {
358+ case "off" , "disable" , "false" , "no" :
359+ // leave seconds nil → disable
360+ default :
361+ secs , err := parseDurationToSeconds (value )
362+ if err != nil {
363+ return fmt .Errorf ("--auto-pause %q: %w" , value , err )
364+ }
365+ seconds = & secs
366+ }
367+ updated , err := client .SetAutoPause (c .Context , id , seconds )
368+ if err != nil {
369+ return err
370+ }
371+ if updated .AutoPauseAfterSeconds != nil {
372+ d := time .Duration (* updated .AutoPauseAfterSeconds ) * time .Second
373+ pterm .Success .Printfln ("Auto-pause set to %s for %s" , formatDuration (d ), refLabel (label , id ))
374+ } else {
375+ pterm .Success .Printfln ("Auto-pause turned off for %s" , refLabel (label , id ))
376+ }
377+ return nil
378+ }
379+
311380// onOff renders true/false as the verb the user typed mentally.
312381func onOff (v bool ) string {
313382 if v {
0 commit comments