This commit is contained in:
Theodor S. Midtlien
2026-06-02 14:41:41 +02:00
parent 1d04a34f45
commit e27f0ed05b
5 changed files with 23 additions and 47 deletions

View File

@@ -83,9 +83,7 @@ func NewProfileManager() *ProfileManager {
}
// GetActiveProfile returns the active profile as recorded in the local
// user state file. Only ID is populated; the display Name is not known
// at this layer — callers that need it should query the daemon's
// ListProfiles RPC.
// user state file. Only ID is populated.
func (pm *ProfileManager) GetActiveProfile() (*Profile, error) {
pm.mu.Lock()
defer pm.mu.Unlock()
@@ -95,9 +93,7 @@ func (pm *ProfileManager) GetActiveProfile() (*Profile, error) {
}
// SwitchProfile records the given profile ID as active in the local user
// state file. The CLI calls this after the daemon has resolved the user-
// supplied handle to a concrete ID, so the value written here is always a
// valid profile filename stem.
// state file.
func (pm *ProfileManager) SwitchProfile(id string) error {
if id != defaultProfileName && !IsValidProfileFilenameStem(id) {
return fmt.Errorf("invalid profile ID: %q", id)

View File

@@ -46,9 +46,8 @@ const (
AmbiguityKindName
)
// profileMeta is the minimal slice of a profile JSON we need for the list.
// Using a private struct here avoids running the full Config.apply()
// pipeline (which can rewrite the file) on every list operation.
// profileMeta is the minimal slice of a profile JSON we need, so we avoid
// reading all fields
type profileMeta struct {
Name string
}
@@ -87,11 +86,11 @@ func init() {
}
type ActiveProfileState struct {
// ID is the on-disk filename stem of the active profile. The JSON
// tag stays as "name" for backwards compatibility with active state
// files written before the ID-keyed layout. Legacy values were
// profile names, which were also the legacy filename stems, so they
// still resolve to the correct file on disk.
// ID is the on-disk filename stem of the active profile. The JSON tag stays
// as "name" for backwards compatibility with active state files written
// before the ID-based config files. Legacy values were profile names, which
// were also the legacy filename stems, so they still resolve to the correct
// file on disk.
ID string `json:"name"`
Username string `json:"username"`
}
@@ -373,8 +372,6 @@ func (s *ServiceManager) RemoveProfile(id, username string) error {
return fmt.Errorf("failed to remove profile config: %w", err)
}
// Best-effort state file cleanup. Missing is fine — state files are
// only created on demand.
stateFile := filepath.Join(filepath.Dir(target.Path), id+".state.json")
if err := os.Remove(stateFile); err != nil && !os.IsNotExist(err) {
log.Warnf("failed to remove profile state file %s: %v", stateFile, err)
@@ -436,8 +433,8 @@ func (s *ServiceManager) getConfigDir(username string) (string, error) {
// by ID for a stable display order.
//
// Each Profile is fully populated: ID is the filename stem, Name comes
// from the JSON's "name" field (falling back to the ID when absent), and
// Path is built from a basename read off disk (path-traversal safe).
// from the JSON's "name" field (falling back to the filename stem when absent)
// and Path is built from a basename read off disk.
func (s *ServiceManager) loadAllProfiles(username string) ([]Profile, error) {
activeID, activeIsDefault := s.activeProfileID()
@@ -545,15 +542,13 @@ func (s *ServiceManager) ResolveProfile(handle, username string) (*Profile, erro
return nil, err
}
// Exact ID match wins outright — covers the default profile and any
// legacy profile whose ID is its name.
for i := range profiles {
if profiles[i].ID == handle {
return &profiles[i], nil
}
}
// ID prefix match. Skip the default profile so a `select d` does not
// ID prefix match. Skip the default profile so `select d` does not
// accidentally pick it via prefix.
var prefixMatches []Profile
for i := range profiles {
@@ -575,7 +570,6 @@ func (s *ServiceManager) ResolveProfile(handle, username string) (*Profile, erro
}
}
// Exact name match.
var nameMatches []Profile
for i := range profiles {
if profiles[i].Name == handle {

View File

@@ -4427,7 +4427,7 @@ func (x *AddProfileRequest) GetProfileName() string {
type AddProfileResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// id is the generated on-disk ID of the new profile. CLI clients
// display a truncated form; UI clients can ignore it.
// display a truncated form, UI clients can ignore it.
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -4659,12 +4659,10 @@ func (x *ListProfilesResponse) GetProfiles() []*Profile {
}
type Profile struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
IsActive bool `protobuf:"varint,2,opt,name=is_active,json=isActive,proto3" json:"is_active,omitempty"`
// id is the on-disk filename stem of the profile. Always set by the
// server; older clients that ignore it continue to work via name.
Id string `protobuf:"bytes,3,opt,name=id,proto3" json:"id,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
IsActive bool `protobuf:"varint,2,opt,name=is_active,json=isActive,proto3" json:"is_active,omitempty"`
Id string `protobuf:"bytes,3,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -4757,13 +4755,10 @@ func (*GetActiveProfileRequest) Descriptor() ([]byte, []int) {
}
type GetActiveProfileResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// profileName carries the display name for backwards compatibility
// with UI clients. New callers should prefer id, which is unique.
ProfileName string `protobuf:"bytes,1,opt,name=profileName,proto3" json:"profileName,omitempty"`
Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"`
// id is the on-disk filename stem of the active profile.
Id string `protobuf:"bytes,3,opt,name=id,proto3" json:"id,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
ProfileName string `protobuf:"bytes,1,opt,name=profileName,proto3" json:"profileName,omitempty"`
Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"`
Id string `protobuf:"bytes,3,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}

View File

@@ -698,7 +698,7 @@ message AddProfileRequest {
message AddProfileResponse {
// id is the generated on-disk ID of the new profile. CLI clients
// display a truncated form; UI clients can ignore it.
// display a truncated form, UI clients can ignore it.
string id = 1;
}
@@ -726,19 +726,14 @@ message ListProfilesResponse {
message Profile {
string name = 1;
bool is_active = 2;
// id is the on-disk filename stem of the profile. Always set by the
// server; older clients that ignore it continue to work via name.
string id = 3;
}
message GetActiveProfileRequest {}
message GetActiveProfileResponse {
// profileName carries the display name for backwards compatibility
// with UI clients. New callers should prefer id, which is unique.
string profileName = 1;
string username = 2;
// id is the on-disk filename stem of the active profile.
string id = 3;
}

View File

@@ -1011,8 +1011,6 @@ func (s *Server) canRemoveProfile(id string) error {
return nil
}
// validateProfileOperation runs the common preconditions for add/remove/
// logout flows. `id` is the resolved profile ID (never user input).
func (s *Server) validateProfileOperation(id string, allowActiveProfile bool) error {
if s.checkProfilesDisabled() {
return gstatus.Errorf(codes.Unavailable, errProfilesDisabled)
@@ -1031,8 +1029,6 @@ func (s *Server) validateProfileOperation(id string, allowActiveProfile bool) er
return nil
}
// logoutFromProfile logs out from a specific (already-resolved) profile
// by loading its config and sending a logout request.
func (s *Server) logoutFromProfile(ctx context.Context, profile *profilemanager.Profile) error {
activeProf, err := s.profileManager.GetActiveProfileState()
if err == nil && activeProf.ID == profile.ID && s.connectClient != nil {
@@ -1639,7 +1635,7 @@ func (s *Server) ListProfiles(ctx context.Context, msg *proto.ListProfilesReques
// GetActiveProfile returns the active profile in the daemon. The
// ProfileName field carries the display name for backwards compatibility
// with UI clients; new callers should prefer Id.
// with UI clients, new callers should prefer Id.
func (s *Server) GetActiveProfile(ctx context.Context, msg *proto.GetActiveProfileRequest) (*proto.GetActiveProfileResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()