@@ -59,7 +59,8 @@ func init() {
5959}
6060
6161// runPushAuto captures the current system snapshot and uploads it to openboot.dev.
62- // If a sync source is configured, it updates that config; otherwise, creates a new one.
62+ // If a sync source is configured, it updates that config silently; otherwise, it
63+ // presents an interactive picker so the user can choose an existing config or create a new one.
6364func runPushAuto (slugOverride , message string ) error {
6465 apiBase := auth .GetAPIBase ()
6566
@@ -93,13 +94,19 @@ func runPushAuto(slugOverride, message string) error {
9394 return fmt .Errorf ("marshal snapshot: %w" , err )
9495 }
9596
96- // Determine slug: use override, then sync source, then blank (create new)
97+ // Determine slug: --slug flag → sync source → interactive picker
9798 slug := slugOverride
9899 if slug == "" {
99100 if source , loadErr := syncpkg .LoadSource (); loadErr == nil && source != nil && source .Slug != "" {
100101 slug = source .Slug
101102 }
102103 }
104+ if slug == "" {
105+ slug , err = pickOrCreateConfig (stored .Token , apiBase )
106+ if err != nil {
107+ return err
108+ }
109+ }
103110
104111 return pushSnapshot (data , slug , message , stored .Token , stored .Username , apiBase )
105112}
@@ -149,16 +156,26 @@ func pushSnapshot(data []byte, slug, message, token, username, apiBase string) e
149156 return fmt .Errorf ("parse snapshot: %w" , err )
150157 }
151158
152- name , desc , visibility , err := promptPushDetails ("" )
153- if err != nil {
154- return err
159+ // Updating an existing config: skip all prompts.
160+ // Creating a new config: ask for name, description, visibility.
161+ var name , desc , visibility string
162+ if slug == "" {
163+ var err error
164+ name , desc , visibility , err = promptPushDetails ("" )
165+ if err != nil {
166+ return err
167+ }
155168 }
156169
157170 reqBody := map [string ]interface {}{
158- "name" : name ,
159- "description" : desc ,
160- "snapshot" : snap ,
161- "visibility" : visibility ,
171+ "snapshot" : snap ,
172+ "visibility" : visibility ,
173+ }
174+ if name != "" {
175+ reqBody ["name" ] = name
176+ }
177+ if desc != "" {
178+ reqBody ["description" ] = desc
162179 }
163180 if slug != "" {
164181 reqBody ["config_slug" ] = slug
@@ -319,6 +336,76 @@ func doUpload(url string, body []byte, token, username, slug string) error {
319336 return nil
320337}
321338
339+ type remoteConfigSummary struct {
340+ Slug string `json:"slug"`
341+ Name string `json:"name"`
342+ }
343+
344+ // fetchUserConfigs calls GET /api/configs and returns the user's existing configs.
345+ func fetchUserConfigs (token , apiBase string ) ([]remoteConfigSummary , error ) {
346+ req , err := http .NewRequest (http .MethodGet , apiBase + "/api/configs" , nil )
347+ if err != nil {
348+ return nil , fmt .Errorf ("build request: %w" , err )
349+ }
350+ req .Header .Set ("Authorization" , "Bearer " + token )
351+
352+ client := & http.Client {Timeout : 10 * time .Second }
353+ resp , err := httputil .Do (client , req )
354+ if err != nil {
355+ return nil , fmt .Errorf ("fetch configs: %w" , err )
356+ }
357+ defer resp .Body .Close ()
358+
359+ if resp .StatusCode != http .StatusOK {
360+ return nil , nil // non-fatal — fall through to create-new flow
361+ }
362+
363+ var result struct {
364+ Configs []remoteConfigSummary `json:"configs"`
365+ }
366+ if err := json .NewDecoder (resp .Body ).Decode (& result ); err != nil {
367+ return nil , nil
368+ }
369+ return result .Configs , nil
370+ }
371+
372+ const createNewOption = "+ Create a new config"
373+
374+ // pickOrCreateConfig shows an interactive list of the user's existing configs plus a
375+ // "Create new" option. Returns the chosen slug (non-empty = update existing), or ""
376+ // (= create new, caller must ask for name/desc/visibility).
377+ func pickOrCreateConfig (token , apiBase string ) (string , error ) {
378+ configs , _ := fetchUserConfigs (token , apiBase ) // ignore fetch errors — just show create-new
379+
380+ if len (configs ) == 0 {
381+ return "" , nil // no existing configs — skip picker, go straight to create-new
382+ }
383+
384+ options := make ([]string , 0 , len (configs )+ 1 )
385+ for _ , c := range configs {
386+ label := c .Slug
387+ if c .Name != "" && c .Name != c .Slug {
388+ label = fmt .Sprintf ("%s — %s" , c .Slug , c .Name )
389+ }
390+ options = append (options , label )
391+ }
392+ options = append (options , createNewOption )
393+
394+ fmt .Fprintln (os .Stderr )
395+ choice , err := ui .SelectOption ("Push to which config?" , options )
396+ if err != nil {
397+ return "" , fmt .Errorf ("select config: %w" , err )
398+ }
399+
400+ if choice == createNewOption {
401+ return "" , nil // caller will prompt for name/desc/visibility
402+ }
403+
404+ // Extract slug from "slug — Name" label
405+ slug := strings .SplitN (choice , " — " , 2 )[0 ]
406+ return slug , nil
407+ }
408+
322409func promptPushDetails (defaultName string ) (string , string , string , error ) {
323410 fmt .Fprintln (os .Stderr )
324411 var name string
0 commit comments