Skip to content

Commit f15aa40

Browse files
baeyc0510baeyc0510
authored andcommitted
feat(init): preserve existing config files when .sym directory exists
- Skip overwriting roles.json, user-policy.json, config.json if they exist - Auto-select non-privileged role for users joining existing projects - Only create missing files instead of failing when .sym/ exists - Add --force flag to explicitly overwrite existing configuration This enables team collaboration where the project lead defines roles and policies, commits .sym/ to version control, and team members can run 'sym init' to set up their local environment without losing shared config.
1 parent d7cc7c4 commit f15aa40

1 file changed

Lines changed: 105 additions & 35 deletions

File tree

internal/cmd/init.go

Lines changed: 105 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,15 @@ This command:
2424
1. Creates .sym/roles.json with default roles (admin, developer, viewer)
2525
2. Creates .sym/user-policy.json with default RBAC configuration
2626
3. Creates .sym/config.json with default settings
27-
4. Sets your role to admin (can be changed later via dashboard)
27+
4. Sets your default role (admin for new projects, non-privileged for existing)
2828
5. Optionally registers MCP server for AI tools
2929
6. Optionally configures LLM backend
3030
31-
Use --force to reinitialize an existing Symphony project.`,
31+
If .sym/ already exists (e.g., synced via git), existing configuration files
32+
are preserved and only missing files are created. This allows team members
33+
to share roles and policies through version control.
34+
35+
Use --force to reinitialize and overwrite existing configuration.`,
3236
Run: runInit,
3337
}
3438

@@ -61,48 +65,67 @@ func runInit(cmd *cobra.Command, args []string) {
6165
os.Exit(1)
6266
}
6367

68+
// When .sym exists without --force, preserve existing files and only create missing ones
6469
if symDirExists && !initForce {
65-
printWarn(".sym directory already exists")
66-
fmt.Println("Use --force flag to reinitialize")
67-
os.Exit(1)
68-
}
69-
70-
// Create default roles (empty user lists - users select their own role)
71-
newRoles := roles.Roles{
72-
"admin": []string{},
73-
"developer": []string{},
74-
"viewer": []string{},
70+
fmt.Println("Found existing .sym directory, preserving existing configuration files...")
7571
}
7672

77-
if err := roles.SaveRoles(newRoles); err != nil {
78-
printError(fmt.Sprintf("Failed to create roles.json: %v", err))
73+
// Create roles.json only if it doesn't exist or --force is set
74+
rolesExist, err := roles.RolesExists()
75+
if err != nil {
76+
printError(fmt.Sprintf("Failed to check roles.json: %v", err))
7977
os.Exit(1)
8078
}
8179

8280
rolesPath, _ := roles.GetRolesPath()
83-
printOK("roles.json created")
84-
fmt.Println(indent(fmt.Sprintf("Location: %s", rolesPath)))
81+
if rolesExist && !initForce {
82+
printOK("roles.json already exists (preserved)")
83+
fmt.Println(indent(fmt.Sprintf("Location: %s", rolesPath)))
84+
} else {
85+
// Create default roles (empty user lists - users select their own role)
86+
newRoles := roles.Roles{
87+
"admin": []string{},
88+
"developer": []string{},
89+
"viewer": []string{},
90+
}
91+
92+
if err := roles.SaveRoles(newRoles); err != nil {
93+
printError(fmt.Sprintf("Failed to create roles.json: %v", err))
94+
os.Exit(1)
95+
}
96+
97+
printOK("roles.json created")
98+
fmt.Println(indent(fmt.Sprintf("Location: %s", rolesPath)))
99+
}
85100

86-
// Create default policy file with RBAC roles
87-
if err := createDefaultPolicy(); err != nil {
101+
// Create default policy file with RBAC roles (only if not exists or --force)
102+
policyCreated, err := createDefaultPolicy()
103+
if err != nil {
88104
printWarn(fmt.Sprintf("Failed to create policy file: %v", err))
89105
fmt.Println(indent("You can manually create it later using the dashboard"))
90-
} else {
106+
} else if policyCreated {
91107
printOK("user-policy.json created with default RBAC roles")
108+
} else {
109+
printOK("user-policy.json already exists (preserved)")
92110
}
93111

94-
// Create .sym/config.json with default settings
95-
if err := initializeConfigFile(); err != nil {
112+
// Create .sym/config.json with default settings (only if not exists or --force)
113+
configCreated, err := initializeConfigFile()
114+
if err != nil {
96115
printWarn(fmt.Sprintf("Failed to create config.json: %v", err))
97-
} else {
116+
} else if configCreated {
98117
printOK("config.json created")
118+
} else {
119+
printOK("config.json already exists (preserved)")
99120
}
100121

101-
// Set default role to admin during initialization
102-
if err := roles.SetCurrentRole("admin"); err != nil {
122+
// Select appropriate default role based on existing configuration
123+
// Note: role selection is stored in .sym/.env which is gitignored, so each user sets their own role
124+
defaultRole := selectDefaultRole(policyCreated)
125+
if err := roles.SetCurrentRole(defaultRole); err != nil {
103126
printWarn(fmt.Sprintf("Failed to save role selection: %v", err))
104127
} else {
105-
printOK("Your role has been set to: admin")
128+
printOK(fmt.Sprintf("Your role has been set to: %s", defaultRole))
106129
}
107130

108131
// MCP registration prompt
@@ -131,19 +154,20 @@ func runInit(cmd *cobra.Command, args []string) {
131154
fmt.Println(indent("Commit .sym/ folder to share with your team"))
132155
}
133156

134-
// createDefaultPolicy creates a default policy file with RBAC roles
135-
func createDefaultPolicy() error {
157+
// createDefaultPolicy creates a default policy file with RBAC roles.
158+
// Returns (true, nil) if a new file was created, (false, nil) if existing file was preserved.
159+
func createDefaultPolicy() (bool, error) {
136160
defaultPolicyPath := ".sym/user-policy.json"
137161

138162
// Check if policy file already exists
139163
exists, err := policy.PolicyExists(defaultPolicyPath)
140164
if err != nil {
141-
return err
165+
return false, err
142166
}
143167

144168
if exists && !initForce {
145-
// Policy already exists, skip creation
146-
return nil
169+
// Policy already exists, preserve it
170+
return false, nil
147171
}
148172

149173
// Create default policy with categories and RBAC roles
@@ -188,22 +212,68 @@ func createDefaultPolicy() error {
188212
Rules: []schema.UserRule{},
189213
}
190214

191-
return policy.SavePolicy(defaultPolicy, defaultPolicyPath)
215+
if err := policy.SavePolicy(defaultPolicy, defaultPolicyPath); err != nil {
216+
return false, err
217+
}
218+
return true, nil
192219
}
193220

194-
// initializeConfigFile creates .sym/config.json with default settings
195-
func initializeConfigFile() error {
221+
// initializeConfigFile creates .sym/config.json with default settings.
222+
// Returns (true, nil) if a new file was created, (false, nil) if existing file was preserved.
223+
func initializeConfigFile() (bool, error) {
196224
// Check if config.json already exists (skip unless force is set)
197225
if config.ProjectConfigExists() && !initForce {
198-
return nil
226+
return false, nil
199227
}
200228

201229
// Create default project config
202230
defaultConfig := &config.ProjectConfig{
203231
PolicyPath: ".sym/user-policy.json",
204232
}
205233

206-
return config.SaveProjectConfig(defaultConfig)
234+
if err := config.SaveProjectConfig(defaultConfig); err != nil {
235+
return false, err
236+
}
237+
return true, nil
238+
}
239+
240+
// selectDefaultRole determines the appropriate default role for a new user.
241+
// For existing projects, selects a role without policy/role editing permissions.
242+
// For new projects, returns the first available role (typically "admin").
243+
func selectDefaultRole(isNewProject bool) string {
244+
// For new projects, use admin role
245+
if isNewProject {
246+
return "admin"
247+
}
248+
249+
// Try to load existing policy to find a non-privileged role
250+
loader := policy.NewLoader(false)
251+
userPolicy, err := loader.LoadUserPolicy(".sym/user-policy.json")
252+
if err != nil || userPolicy == nil || userPolicy.RBAC == nil || len(userPolicy.RBAC.Roles) == 0 {
253+
// Fallback: try to get first available role from roles.json
254+
availableRoles, err := roles.GetAvailableRoles()
255+
if err != nil || len(availableRoles) == 0 {
256+
return "admin" // Ultimate fallback
257+
}
258+
return availableRoles[0]
259+
}
260+
261+
// Find a role without editing permissions (non-privileged)
262+
var firstRole string
263+
for roleName, role := range userPolicy.RBAC.Roles {
264+
if firstRole == "" {
265+
firstRole = roleName
266+
}
267+
if !role.CanEditPolicy && !role.CanEditRoles {
268+
return roleName
269+
}
270+
}
271+
272+
// No non-privileged role found, return first available role
273+
if firstRole != "" {
274+
return firstRole
275+
}
276+
return "admin"
207277
}
208278

209279
// removeExistingCodePolicy removes generated linter config files when --force flag is used

0 commit comments

Comments
 (0)