Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion internal/cmd/profiles.go
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
45 changes: 45 additions & 0 deletions internal/cmd/profiles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package cmd

import (
"context"
"encoding/json"
"net/http"
"strings"
"testing"

Expand Down Expand Up @@ -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)
}
}
Loading