@@ -3,6 +3,7 @@ package cli
33import (
44 "bytes"
55 "encoding/json"
6+ "errors"
67 "fmt"
78 "io"
89 "net/http"
@@ -18,6 +19,7 @@ import (
1819 "github.com/openbootdotdev/openboot/internal/httputil"
1920 "github.com/openbootdotdev/openboot/internal/installer"
2021 "github.com/openbootdotdev/openboot/internal/snapshot"
22+ syncpkg "github.com/openbootdotdev/openboot/internal/sync"
2123 "github.com/openbootdotdev/openboot/internal/ui"
2224 "github.com/spf13/cobra"
2325)
@@ -223,85 +225,82 @@ func uploadSnapshot(snap *snapshot.Snapshot) error {
223225 return fmt .Errorf ("no valid auth token found — please log in again" )
224226 }
225227
226- configName , configDesc , visibility , err := promptConfigDetails ()
228+ updateSlug , err := promptUpdateOrCreate ()
227229 if err != nil {
228230 return err
229231 }
230232
231- slug , err := postSnapshotToAPI ( snap , configName , configDesc , visibility , stored . Token , apiBase )
233+ configName , configDesc , visibility , err := promptPushDetails ( )
232234 if err != nil {
233235 return err
234236 }
235237
236- configURL := fmt .Sprintf ("%s/%s/%s" , apiBase , stored .Username , slug )
237- installURL := fmt .Sprintf ("openboot -u %s/%s" , stored .Username , slug )
238+ resultSlug , err := postSnapshotToAPI (snap , configName , configDesc , visibility , stored .Token , apiBase , updateSlug )
239+ if err != nil {
240+ return err
241+ }
242+
243+ configURL := fmt .Sprintf ("%s/%s/%s" , apiBase , stored .Username , resultSlug )
244+ installURL := fmt .Sprintf ("openboot -u %s/%s" , stored .Username , resultSlug )
238245
239246 fmt .Fprintln (os .Stderr )
240- fmt .Fprintln (os .Stderr , snapSuccessStyle .Render ("✓ Config uploaded successfully!" ))
247+ if updateSlug != "" {
248+ fmt .Fprintln (os .Stderr , snapSuccessStyle .Render ("✓ Config updated successfully!" ))
249+ } else {
250+ fmt .Fprintln (os .Stderr , snapSuccessStyle .Render ("✓ Config uploaded successfully!" ))
251+ }
241252 fmt .Fprintln (os .Stderr )
242253 showUploadedConfigInfo (visibility , configURL , installURL )
243254 fmt .Fprintln (os .Stderr )
244255
245256 return nil
246257}
247258
248- func promptConfigDetails () (string , string , string , error ) {
249- fmt .Fprintln (os .Stderr )
250- configName , err := ui .Input ("Config name" , "My Mac Setup" )
251- if err != nil {
252- return "" , "" , "" , fmt .Errorf ("get config name: %w" , err )
253- }
254- configName = strings .TrimSpace (configName )
255- if configName == "" {
256- configName = "My Mac Setup"
259+ func promptUpdateOrCreate () (string , error ) {
260+ source , err := syncpkg .LoadSource ()
261+ if err != nil || source == nil || source .Username == "" || source .Slug == "" {
262+ return "" , nil
257263 }
258264
259- fmt .Fprintln (os .Stderr )
260- configDesc , err := ui .Input ("Description (optional)" , "" )
261- if err != nil {
262- return "" , "" , "" , fmt .Errorf ("get config description: %w" , err )
263- }
264- configDesc = strings .TrimSpace (configDesc )
265+ label := fmt .Sprintf ("@%s/%s" , source .Username , source .Slug )
266+ updateOption := fmt .Sprintf ("Update existing config (%s)" , label )
267+ options := []string {updateOption , "Create new config" }
265268
266269 fmt .Fprintln (os .Stderr )
267- visibilityOptions := []string {
268- "Public - Anyone can discover and use this config" ,
269- "Unlisted - Only people with the link can access" ,
270- "Private - Only you can see this config" ,
271- }
272- visibilityChoice , err := ui .SelectOption ("Who can see this config?" , visibilityOptions )
270+ choice , err := ui .SelectOption ("Upload mode:" , options )
273271 if err != nil {
274- return "" , "" , "" , fmt .Errorf ("select visibility : %w" , err )
272+ return "" , fmt .Errorf ("select upload mode : %w" , err )
275273 }
276274
277- visibility := "unlisted"
278- if strings .HasPrefix (visibilityChoice , "Public" ) {
279- visibility = "public"
280- } else if strings .HasPrefix (visibilityChoice , "Private" ) {
281- visibility = "private"
275+ if choice == updateOption {
276+ return source .Slug , nil
282277 }
283-
284- return configName , configDesc , visibility , nil
278+ return "" , nil
285279}
286280
287- func postSnapshotToAPI (snap * snapshot.Snapshot , configName , configDesc , visibility , token , apiBase string ) (string , error ) {
281+ func postSnapshotToAPI (snap * snapshot.Snapshot , configName , configDesc , visibility , token , apiBase , slug string ) (string , error ) {
282+ method := "POST"
288283 reqBody := map [string ]interface {}{
289284 "name" : configName ,
290285 "description" : configDesc ,
291286 "snapshot" : snap ,
292287 "visibility" : visibility ,
293288 }
289+ if slug != "" {
290+ method = "PUT"
291+ reqBody ["config_slug" ] = slug
292+ }
294293 bodyBytes , err := json .Marshal (reqBody )
295294 if err != nil {
296295 return "" , fmt .Errorf ("marshal request: %w" , err )
297296 }
298297
299298 uploadURL := fmt .Sprintf ("%s/api/configs/from-snapshot" , apiBase )
300- req , err := http .NewRequest ("POST" , uploadURL , bytes .NewReader (bodyBytes ))
299+ req , err := http .NewRequest (method , uploadURL , bytes .NewReader (bodyBytes ))
301300 if err != nil {
302301 return "" , fmt .Errorf ("create upload request: %w" , err )
303302 }
304- req .Header .Set ("Authorization" , fmt . Sprintf ( "Bearer %s" , token ) )
303+ req .Header .Set ("Authorization" , "Bearer " + token )
305304 req .Header .Set ("Content-Type" , "application/json" )
306305
307306 client := & http.Client {Timeout : 30 * time .Second }
@@ -312,9 +311,28 @@ func postSnapshotToAPI(snap *snapshot.Snapshot, configName, configDesc, visibili
312311 defer resp .Body .Close ()
313312
314313 if resp .StatusCode != http .StatusOK && resp .StatusCode != http .StatusCreated {
315- respBody , err := io .ReadAll (io .LimitReader (resp .Body , 64 << 10 )) // 64 KB for error responses
316- if err != nil {
317- return "" , fmt .Errorf ("upload failed (status %d): read response: %w" , resp .StatusCode , err )
314+ respBody , readErr := io .ReadAll (io .LimitReader (resp .Body , 64 << 10 ))
315+ if readErr != nil {
316+ return "" , fmt .Errorf ("upload failed (status %d): read response: %w" , resp .StatusCode , readErr )
317+ }
318+ if resp .StatusCode == http .StatusConflict {
319+ var errResp struct {
320+ Message string `json:"message"`
321+ Error string `json:"error"`
322+ }
323+ if jsonErr := json .Unmarshal (respBody , & errResp ); jsonErr == nil {
324+ msg := errResp .Message
325+ if msg == "" {
326+ msg = errResp .Error
327+ }
328+ if msg != "" && strings .Contains (strings .ToLower (msg ), "maximum" ) {
329+ return "" , errors .New ("config limit reached (max 20): delete an existing config with 'openboot delete <slug>' first" )
330+ }
331+ if msg != "" {
332+ return "" , errors .New (msg )
333+ }
334+ }
335+ return "" , fmt .Errorf ("conflict: %s" , string (respBody ))
318336 }
319337 return "" , fmt .Errorf ("upload failed (status %d): %s" , resp .StatusCode , string (respBody ))
320338 }
@@ -325,7 +343,11 @@ func postSnapshotToAPI(snap *snapshot.Snapshot, configName, configDesc, visibili
325343 if err := json .NewDecoder (resp .Body ).Decode (& result ); err != nil {
326344 return "" , fmt .Errorf ("parse upload response: %w" , err )
327345 }
328- return result .Slug , nil
346+ resultSlug := result .Slug
347+ if resultSlug == "" {
348+ resultSlug = slug
349+ }
350+ return resultSlug , nil
329351}
330352
331353func showUploadedConfigInfo (visibility , configURL , installURL string ) {
0 commit comments