diff --git a/internal/cmd/profiles.go b/internal/cmd/profiles.go index d89beb7..3f6c1dc 100644 --- a/internal/cmd/profiles.go +++ b/internal/cmd/profiles.go @@ -1,14 +1,22 @@ package cmd import ( + "bytes" + "encoding/json" "fmt" + "io" + "net/http" + "net/url" "github.com/spf13/cobra" "github.com/nottelabs/notte-cli/internal/api" ) -var profileID string +var ( + profileID string + profileDuplicateName string +) var profilesCmd = &cobra.Command{ Use: "profiles", @@ -42,6 +50,13 @@ var profilesDeleteCmd = &cobra.Command{ RunE: runProfileDelete, } +var profilesDuplicateCmd = &cobra.Command{ + Use: "duplicate", + Short: "Duplicate a profile and its persisted browser state", + Args: cobra.NoArgs, + RunE: runProfileDuplicate, +} + func init() { rootCmd.AddCommand(profilesCmd) profilesCmd.AddCommand(profilesListCmd) @@ -51,6 +66,7 @@ func init() { profilesCmd.AddCommand(profilesCreateCmd) profilesCmd.AddCommand(profilesShowCmd) profilesCmd.AddCommand(profilesDeleteCmd) + profilesCmd.AddCommand(profilesDuplicateCmd) // Create command flags (auto-generated) RegisterProfileCreateFlags(profilesCreateCmd) @@ -62,6 +78,11 @@ func init() { // Delete command flags profilesDeleteCmd.Flags().StringVar(&profileID, "profile-id", "", "Profile ID (required)") _ = profilesDeleteCmd.MarkFlagRequired("profile-id") + + // Duplicate command flags + profilesDuplicateCmd.Flags().StringVar(&profileID, "profile-id", "", "Source profile ID (required)") + profilesDuplicateCmd.Flags().StringVar(&profileDuplicateName, "name", "", "Optional name for the duplicate") + _ = profilesDuplicateCmd.MarkFlagRequired("profile-id") } func runProfilesList(cmd *cobra.Command, args []string) error { @@ -196,3 +217,49 @@ func runProfileDelete(cmd *cobra.Command, args []string) error { "status": "deleted", }) } + +func runProfileDuplicate(cmd *cobra.Command, args []string) error { + client, err := GetClient() + if err != nil { + return err + } + + ctx, cancel := GetContextWithTimeout(cmd.Context()) + defer cancel() + + requestBody := map[string]string{} + if cmd.Flags().Changed("name") { + requestBody["name"] = profileDuplicateName + } + bodyJSON, err := json.Marshal(requestBody) + if err != nil { + return fmt.Errorf("failed to marshal duplicate request: %w", err) + } + + endpoint := fmt.Sprintf("%s/profiles/%s/duplicate", client.BaseURL(), url.PathEscape(profileID)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(bodyJSON)) + if err != nil { + return fmt.Errorf("failed to create duplicate request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := client.HTTPClient().Do(req) + if err != nil { + return fmt.Errorf("API request failed: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %w", err) + } + if err := HandleAPIResponse(resp, body); err != nil { + return err + } + + var duplicate api.ProfileResponse + if err := json.Unmarshal(body, &duplicate); err != nil { + return fmt.Errorf("failed to parse duplicate profile: %w", err) + } + return GetFormatter().Print(duplicate) +} diff --git a/internal/cmd/profiles_test.go b/internal/cmd/profiles_test.go index bd3e5f6..e28ecd7 100644 --- a/internal/cmd/profiles_test.go +++ b/internal/cmd/profiles_test.go @@ -2,6 +2,8 @@ package cmd import ( "context" + "encoding/json" + "net/http" "strings" "testing" @@ -171,3 +173,46 @@ func TestRunProfileDelete(t *testing.T) { t.Errorf("expected delete message, got %q", stdout) } } + +func TestRunProfileDuplicate(t *testing.T) { + server := setupProfileTest(t) + path := "/profiles/" + profileIDTest + "/duplicate" + server.AddResponse(path, http.StatusOK, `{"profile_id":"notte-profile-copy123","name":"Copied Profile","created_at":"2020-01-01T00:00:00Z","updated_at":"2020-01-01T00:00:00Z"}`) + + originalName := profileDuplicateName + profileDuplicateName = "Copied Profile" + t.Cleanup(func() { profileDuplicateName = originalName }) + + originalFormat := outputFormat + outputFormat = "json" + t.Cleanup(func() { outputFormat = originalFormat }) + + cmd := &cobra.Command{} + cmd.Flags().String("name", "", "") + _ = cmd.Flags().Set("name", profileDuplicateName) + cmd.SetContext(context.Background()) + + stdout, _ := testutil.CaptureOutput(func() { + if err := runProfileDuplicate(cmd, nil); err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + + if !strings.Contains(stdout, "notte-profile-copy123") { + t.Errorf("expected duplicate profile output, got %q", stdout) + } + requests := server.Requests(path) + if len(requests) != 1 { + t.Fatalf("expected one duplicate request, got %d", len(requests)) + } + if requests[0].Method != http.MethodPost { + t.Errorf("expected POST, got %s", requests[0].Method) + } + var body map[string]string + if err := json.Unmarshal([]byte(requests[0].Body), &body); err != nil { + t.Fatalf("invalid request JSON: %v", err) + } + if body["name"] != "Copied Profile" { + t.Errorf("expected destination name, got %#v", body) + } +}