@@ -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