feat: profile rename subcommand + expand all model display names

This commit is contained in:
2026-05-23 21:13:27 +01:00
parent fb02b165d7
commit 21a86c084f
4 changed files with 440 additions and 653 deletions
+69 -3
View File
@@ -34,8 +34,8 @@ func NewProfile(reg *adapter.Registry) *cobra.Command {
}
names := make([]string, 0, len(entries))
for _, e := range entries {
if strings.HasSuffix(e.Name(), ".yaml") {
names = append(names, strings.TrimSuffix(e.Name(), ".yaml"))
if n, ok := strings.CutSuffix(e.Name(), ".yaml"); ok {
names = append(names, n)
}
}
sort.Strings(names)
@@ -84,8 +84,54 @@ func NewProfile(reg *adapter.Registry) *cobra.Command {
rootFlag(use)
use.Flags().BoolVar(&applyAfter, "apply", false, "run apply automatically after switching profile")
rename := &cobra.Command{
Use: "rename <old> <new>",
Short: "Rename a profile (file + name field + active_profile if needed)",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
r, err := resolveRoot(root)
if err != nil {
return err
}
oldName, newName := args[0], args[1]
oldPath := filepath.Join(r, "profiles", oldName+".yaml")
newPath := filepath.Join(r, "profiles", newName+".yaml")
if _, statErr := os.Stat(oldPath); statErr != nil {
return fmt.Errorf("profile %q not found", oldName)
}
if _, statErr := os.Stat(newPath); statErr == nil {
return fmt.Errorf("profile %q already exists", newName)
}
body, err := os.ReadFile(oldPath)
if err != nil {
return err
}
if err := os.WriteFile(newPath, []byte(setProfileName(string(body), newName)), 0o600); err != nil {
return err
}
if err := os.Remove(oldPath); err != nil {
_ = os.Remove(newPath)
return err
}
// Update active_profile in config.yaml if it points at the old name.
configPath := filepath.Join(r, "config.yaml")
if existing, readErr := os.ReadFile(configPath); readErr == nil {
if isActiveProfile(string(existing), oldName) {
_ = os.WriteFile(configPath, []byte(setActiveProfile(string(existing), newName)), 0o600)
}
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Renamed profile %q → %q.\n", oldName, newName)
return nil
},
}
rootFlag(rename)
cmd := &cobra.Command{Use: "profile", Short: "Manage profiles"}
cmd.AddCommand(list, use)
cmd.AddCommand(list, use, rename)
return cmd
}
@@ -99,3 +145,23 @@ func setActiveProfile(existing, name string) string {
}
return strings.TrimRight(existing, "\n") + "\nactive_profile: " + name + "\n"
}
func isActiveProfile(configYAML, name string) bool {
for l := range strings.SplitSeq(configYAML, "\n") {
if rest, ok := strings.CutPrefix(l, "active_profile:"); ok {
return strings.TrimSpace(rest) == name
}
}
return false
}
func setProfileName(body, name string) string {
lines := strings.Split(body, "\n")
for i, l := range lines {
if strings.HasPrefix(l, "name:") {
lines[i] = "name: " + name
return strings.Join(lines, "\n")
}
}
return "name: " + name + "\n" + strings.TrimLeft(body, "\n")
}
+63
View File
@@ -62,6 +62,69 @@ func TestProfileUseHintMessage(t *testing.T) {
assert.Contains(t, buf.String(), "harness-sync apply")
}
func TestProfileRename(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, "profiles"), 0o750))
require.NoError(t, os.WriteFile(filepath.Join(root, "profiles", "old.yaml"), []byte("name: old\ngateway:\n url: https://gw\n"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(root, "config.yaml"), []byte("active_profile: other\n"), 0o600))
var buf bytes.Buffer
cmd := NewProfile(nil)
cmd.SetOut(&buf)
cmd.SetArgs([]string{"rename", "old", "new", "--root", root})
require.NoError(t, cmd.Execute())
// old file gone, new file present with updated name field.
_, err := os.Stat(filepath.Join(root, "profiles", "old.yaml"))
assert.True(t, os.IsNotExist(err), "old.yaml should be removed")
body, err := os.ReadFile(filepath.Join(root, "profiles", "new.yaml"))
require.NoError(t, err)
assert.Contains(t, string(body), "name: new")
assert.NotContains(t, string(body), "name: old")
// config.yaml not modified — old was not active.
cfg, err := os.ReadFile(filepath.Join(root, "config.yaml"))
require.NoError(t, err)
assert.Contains(t, string(cfg), "active_profile: other")
assert.Contains(t, buf.String(), `"old" → "new"`)
}
func TestProfileRenameUpdatesActiveProfile(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, "profiles"), 0o750))
require.NoError(t, os.WriteFile(filepath.Join(root, "profiles", "old.yaml"), []byte("name: old\n"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(root, "config.yaml"), []byte("active_profile: old\n"), 0o600))
cmd := NewProfile(nil)
cmd.SetArgs([]string{"rename", "old", "new", "--root", root})
require.NoError(t, cmd.Execute())
cfg, err := os.ReadFile(filepath.Join(root, "config.yaml"))
require.NoError(t, err)
assert.Contains(t, string(cfg), "active_profile: new")
}
func TestProfileRenameErrorNotFound(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, "profiles"), 0o750))
cmd := NewProfile(nil)
cmd.SetArgs([]string{"rename", "missing", "new", "--root", root})
assert.Error(t, cmd.Execute())
}
func TestProfileRenameErrorAlreadyExists(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, "profiles"), 0o750))
require.NoError(t, os.WriteFile(filepath.Join(root, "profiles", "old.yaml"), []byte("name: old\n"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(root, "profiles", "new.yaml"), []byte("name: new\n"), 0o600))
cmd := NewProfile(nil)
cmd.SetArgs([]string{"rename", "old", "new", "--root", root})
assert.Error(t, cmd.Execute())
}
// TestProfileUseWithApply verifies --apply triggers apply after switching.
func TestProfileUseWithApply(t *testing.T) {
root := t.TempDir()