@@ -54,13 +54,20 @@ type ProfilesImportLocalInput struct {
5454}
5555
5656type ProfilesImportLocalCmd struct {
57- prompter interactive.Prompter
58- homeDir func () (string , error )
59- now func () time.Time
60- providers func () []passwordmanager.Provider
61- provisioner managedAuthProvisioner
62- managedAuthCapacity func (context.Context ) (managedAuthCapacity , error )
63- profileExists func (context.Context , string ) (bool , error )
57+ prompter interactive.Prompter
58+ homeDir func () (string , error )
59+ now func () time.Time
60+ providers func () []passwordmanager.Provider
61+ provisioner managedAuthProvisioner
62+ managedAuthCapacity func (context.Context ) (managedAuthCapacity , error )
63+ profileLookup func (context.Context , string ) (kernelProfileReference , bool , error )
64+ selectProfileTarget func (string , []string , string ) (string , error )
65+ selectManagedAuthAccount func (string , []string , string ) (string , error )
66+ }
67+
68+ type kernelProfileReference struct {
69+ ID string
70+ Name string
6471}
6572
6673type pendingManagedAuth struct {
@@ -195,22 +202,18 @@ func (c ProfilesImportLocalCmd) Run(ctx context.Context, in ProfilesImportLocalI
195202 pterm .Success .Printf ("Found %s\n " , profile .DisplayName ())
196203 }
197204 targetName := in .ProfileName
198- explicitProfileName := targetName ! = ""
205+ targetProfileID : = ""
199206 if targetName == "" {
200207 targetName = defaultImportedProfileName (profile )
201208 }
202209 if targetName == "" || len (targetName ) > 255 || profileIDNameCharacters .MatchString (targetName ) || cuidLikeProfileName .MatchString (targetName ) {
203210 return fmt .Errorf ("profile name must be 1-255 letters, numbers, dots, underscores, or hyphens and cannot be a cuid-like string" )
204211 }
205- if c .profileExists != nil {
206- resolvedName , renamed , err := resolveImportedProfileName (ctx , targetName , explicitProfileName , c . profileExists )
212+ if c .profileLookup != nil {
213+ targetName , targetProfileID , err = c . chooseImportedProfileTarget (ctx , profile , targetName , nonInteractive )
207214 if err != nil {
208215 return err
209216 }
210- if renamed && humanOutput {
211- pterm .Info .Printf ("Kernel profile %q already exists; using %q for this import\n " , targetName , resolvedName )
212- }
213- targetName = resolvedName
214217 }
215218 explicitSites := in .Sites
216219 if len (explicitSites ) > 0 {
@@ -399,7 +402,7 @@ func (c ProfilesImportLocalCmd) Run(ctx context.Context, in ProfilesImportLocalI
399402 ID : profile .ID , Kind : "browser" , Name : profile .DisplayName (), Browser : profile .Browser .ID ,
400403 DataTypes : categories , ItemCounts : itemCounts ,
401404 }}}
402- selection := localbrowser.Selection {Profiles : []localbrowser.ProfileSelection {{SourceID : profile .ID , TargetName : targetName , Categories : categories }}, CredentialSources : make ([]string , 0 )}
405+ selection := localbrowser.Selection {Profiles : []localbrowser.ProfileSelection {{SourceID : profile .ID , TargetName : targetName , TargetProfileID : targetProfileID , Categories : categories }}, CredentialSources : make ([]string , 0 )}
403406 profileJob := startProfileImport (ctx , client , profileImportRequest {
404407 importID : importID , helperToken : helperToken , dashboardHandoff : dashboardHandoff ,
405408 inventory : inventory , selection : selection , bundle : bundle , waitTimeout : in .WaitTimeout ,
@@ -776,12 +779,21 @@ func (c ProfilesImportLocalCmd) chooseManagedAuthLogins(ctx context.Context, pro
776779 }
777780 return pendingManagedAuth {}, nil
778781 }
782+ otherProfiles := make (map [string ][]string )
783+ if finder , ok := c .provisioner .(crossProfileManagedAuthFinder ); ok {
784+ otherProfiles , err = finder .ExistingProfiles (ctx , profileName , candidates )
785+ if err != nil {
786+ return pendingManagedAuth {}, fmt .Errorf ("check Managed Auth connections on other profiles: %w" , err )
787+ }
788+ }
779789 hasExisting := false
780790 hasNew := false
781791 for _ , candidate := range candidates {
782- if existing [candidateKey (candidate )] {
792+ key := candidateKey (candidate )
793+ if existing [key ] || len (otherProfiles [key ]) > 0 {
783794 hasExisting = true
784- } else {
795+ }
796+ if ! existing [key ] {
785797 hasNew = true
786798 }
787799 }
@@ -915,7 +927,7 @@ func (c ProfilesImportLocalCmd) chooseManagedAuthLogins(ctx context.Context, pro
915927 approvedCandidates = append (approvedCandidates , sourced )
916928 }
917929 } else {
918- approvedCandidates , err = c .chooseManagedAuthAccountsByWebsite (sites , allCandidates , existing , availableConnections , 0 , displayCapacity , displayCapacityKnown )
930+ approvedCandidates , err = c .chooseManagedAuthAccountsByWebsite (profileName , sites , allCandidates , existing , otherProfiles , availableConnections , 0 , displayCapacity , displayCapacityKnown )
919931 if err != nil {
920932 return pendingManagedAuth {}, err
921933 }
@@ -939,7 +951,7 @@ func (c ProfilesImportLocalCmd) chooseManagedAuthLogins(ctx context.Context, pro
939951 return pending , nil
940952}
941953
942- func (c ProfilesImportLocalCmd ) chooseManagedAuthAccountsByWebsite (sites []string , candidates []sourcedPasswordManagerCandidate , existing map [string ]bool , availableConnections , alreadySelectedNew int , capacity managedAuthCapacity , capacityKnown bool ) ([]sourcedPasswordManagerCandidate , error ) {
954+ func (c ProfilesImportLocalCmd ) chooseManagedAuthAccountsByWebsite (profileName string , sites []string , candidates []sourcedPasswordManagerCandidate , existing map [string ]bool , otherProfiles map [ string ][] string , availableConnections , alreadySelectedNew int , capacity managedAuthCapacity , capacityKnown bool ) ([]sourcedPasswordManagerCandidate , error ) {
943955 byDomain := make (map [string ][]sourcedPasswordManagerCandidate , len (sites ))
944956 for _ , candidate := range candidates {
945957 byDomain [candidate .candidate .Domain ] = append (byDomain [candidate .candidate .Domain ], candidate )
@@ -958,7 +970,7 @@ func (c ProfilesImportLocalCmd) chooseManagedAuthAccountsByWebsite(sites []strin
958970 for domainIndex := 0 ; domainIndex < len (domains ); {
959971 domain := domains [domainIndex ]
960972 domainCandidates := byDomain [domain ]
961- if len (domainCandidates ) == 1 {
973+ if len (domainCandidates ) == 1 && len ( otherProfiles [ candidateKey ( domainCandidates [ 0 ]. candidate )]) == 0 {
962974 chosen := domainCandidates [0 ]
963975 delete (choices , domain )
964976 if ! existing [candidateKey (chosen .candidate )] && managedAuthCapacityReached (alreadySelectedNew + managedAuthNewChoiceCount (choices , existing ), availableConnections , capacity ) {
@@ -971,16 +983,38 @@ func (c ProfilesImportLocalCmd) chooseManagedAuthAccountsByWebsite(sites []strin
971983 domainIndex ++
972984 continue
973985 }
974- labels := make ([]string , 0 , len (domainCandidates ))
986+ labels := make ([]string , 0 , len (domainCandidates )* 2 )
975987 byLabel := make (map [string ]sourcedPasswordManagerCandidate , len (domainCandidates ))
988+ keepExisting := make (map [string ]bool )
976989 existingLabels := make ([]string , 0 , len (domainCandidates ))
977- labels = groupedLoginLabels (domainCandidates )
990+ candidateLabels := groupedLoginLabels (domainCandidates )
991+ accountPrompt := domain + " — ↑/↓ move, Enter chooses"
992+ if len (domainCandidates ) == 1 {
993+ profiles := otherProfiles [candidateKey (domainCandidates [0 ].candidate )]
994+ if len (profiles ) > 0 && ! existing [candidateKey (domainCandidates [0 ].candidate )] {
995+ pterm .Printf ("%s\n Already managed on %q with this account\n \n " , domain , profiles [0 ])
996+ accountPrompt = "Choose how to use this account — ↑/↓ move, Enter chooses"
997+ }
998+ }
978999 for index , sourced := range domainCandidates {
979- label := labels [index ]
1000+ label := candidateLabels [index ]
1001+ profiles := otherProfiles [candidateKey (sourced .candidate )]
1002+ if len (profiles ) > 0 && ! existing [candidateKey (sourced .candidate )] {
1003+ keepLabel := fmt .Sprintf ("Keep this account managed on %q · 0 new slots" , profiles [0 ])
1004+ alsoLabel := fmt .Sprintf ("Also manage this account on %q · uses 1 slot" , profileName )
1005+ if len (domainCandidates ) > 1 {
1006+ keepLabel = fmt .Sprintf ("Keep %s on %q · 0 new slots" , label , profiles [0 ])
1007+ alsoLabel = fmt .Sprintf ("Also manage %s on %q · uses 1 slot" , label , profileName )
1008+ }
1009+ labels = append (labels , keepLabel , alsoLabel )
1010+ keepExisting [keepLabel ] = true
1011+ byLabel [alsoLabel ] = sourced
1012+ continue
1013+ }
9801014 if existing [candidateKey (sourced .candidate )] {
9811015 label += " ✓ existing · no new slot"
982- labels [index ] = label
9831016 }
1017+ labels = append (labels , label )
9841018 byLabel [label ] = sourced
9851019 if existing [candidateKey (sourced .candidate )] {
9861020 existingLabels = append (existingLabels , label )
@@ -1004,7 +1038,13 @@ func (c ProfilesImportLocalCmd) chooseManagedAuthAccountsByWebsite(sites []strin
10041038 } else if managedAuthCapacityReached (alreadySelectedNew + managedAuthNewChoiceCount (choices , existing ), availableConnections , capacity ) {
10051039 defaultOption = "Skip this website"
10061040 }
1007- selected , err := c .prompter .SelectDefault ("login for " + domain , "use arrow keys and press Enter" , domain + " — ↑/↓ move, Enter chooses" , options , defaultOption )
1041+ var selected string
1042+ var err error
1043+ if c .selectManagedAuthAccount != nil {
1044+ selected , err = c .selectManagedAuthAccount (domain , options , defaultOption )
1045+ } else {
1046+ selected , err = c .prompter .SelectDefault ("login for " + domain , "use arrow keys and press Enter" , accountPrompt , options , defaultOption )
1047+ }
10081048 if err != nil {
10091049 return nil , err
10101050 }
@@ -1024,6 +1064,11 @@ func (c ProfilesImportLocalCmd) chooseManagedAuthAccountsByWebsite(sites []strin
10241064 domainIndex ++
10251065 continue
10261066 }
1067+ if keepExisting [selected ] {
1068+ delete (choices , domain )
1069+ domainIndex ++
1070+ continue
1071+ }
10271072 chosen := byLabel [selected ]
10281073 previous , hadPrevious := choices [domain ]
10291074 delete (choices , domain )
@@ -1447,6 +1492,44 @@ func resolveImportedProfileName(ctx context.Context, requested string, explicit
14471492 return "" , false , fmt .Errorf ("could not find an available Kernel profile name based on %q; use --profile-name" , requested )
14481493}
14491494
1495+ func (c ProfilesImportLocalCmd ) chooseImportedProfileTarget (ctx context.Context , source localbrowser.Profile , requested string , nonInteractive bool ) (string , string , error ) {
1496+ existing , found , err := c .profileLookup (ctx , requested )
1497+ if err != nil {
1498+ return "" , "" , fmt .Errorf ("check Kernel profile name %q: %w" , requested , err )
1499+ }
1500+ if ! found {
1501+ return requested , "" , nil
1502+ }
1503+ if nonInteractive {
1504+ return "" , "" , fmt .Errorf ("Kernel profile %q already exists; run interactively to update it or choose a different --profile-name" , requested )
1505+ }
1506+
1507+ separateName , _ , err := resolveImportedProfileName (ctx , requested , false , func (ctx context.Context , name string ) (bool , error ) {
1508+ _ , found , err := c .profileLookup (ctx , name )
1509+ return found , err
1510+ })
1511+ if err != nil {
1512+ return "" , "" , err
1513+ }
1514+ updateOption := fmt .Sprintf ("Update %q (recommended; keeps existing Managed Auth connections)" , existing .Name )
1515+ separateOption := fmt .Sprintf ("Create a separate profile %q" , separateName )
1516+ prompt := fmt .Sprintf ("An earlier %s import already exists" , source .Browser .Name )
1517+ options := []string {updateOption , separateOption }
1518+ var choice string
1519+ if c .selectProfileTarget != nil {
1520+ choice , err = c .selectProfileTarget (prompt , options , updateOption )
1521+ } else {
1522+ choice , err = c .prompter .SelectDefault ("profile import destination" , "choose whether to update or create a separate profile" , prompt , options , updateOption )
1523+ }
1524+ if err != nil {
1525+ return "" , "" , err
1526+ }
1527+ if choice == updateOption {
1528+ return existing .Name , existing .ID , nil
1529+ }
1530+ return separateName , "" , nil
1531+ }
1532+
14501533func durationMilliseconds (values map [string ]time.Duration ) map [string ]int64 {
14511534 result := make (map [string ]int64 , len (values ))
14521535 for name , duration := range values {
@@ -1943,16 +2026,16 @@ func runProfilesImportLocalWithInput(cmd *cobra.Command, input ProfilesImportLoc
19432026 providers : passwordmanager .Detect ,
19442027 provisioner : kernelManagedAuthProvisioner {credentials : & credentials , connections : & connections },
19452028 managedAuthCapacity : func (ctx context.Context ) (managedAuthCapacity , error ) { return loadManagedAuthCapacity (ctx , & limits ) },
1946- profileExists : func (ctx context.Context , name string ) (bool , error ) {
1947- _ , err := profiles .Get (ctx , name )
2029+ profileLookup : func (ctx context.Context , name string ) (kernelProfileReference , bool , error ) {
2030+ profile , err := profiles .Get (ctx , name )
19482031 if err == nil {
1949- return true , nil
2032+ return kernelProfileReference { ID : profile . ID , Name : profile . Name }, true , nil
19502033 }
19512034 var apiErr * kernel.Error
19522035 if errors .As (err , & apiErr ) && apiErr .StatusCode == http .StatusNotFound {
1953- return false , nil
2036+ return kernelProfileReference {}, false , nil
19542037 }
1955- return false , util.CleanedUpSdkError {Err : err }
2038+ return kernelProfileReference {}, false , util.CleanedUpSdkError {Err : err }
19562039 },
19572040 }
19582041 input .ProjectID = project .ID
0 commit comments