@@ -46,87 +46,42 @@ type DevcontainerConfig struct {
4646 PostCreateCommand string `json:"postCreateCommand,omitempty"`
4747}
4848
49- // ensureDevcontainerConfig creates or updates .devcontainer/gh-aw/devcontainer.json
49+ // ensureDevcontainerConfig creates or updates devcontainer.json
50+ // If .devcontainer/devcontainer.json exists, it updates it with gh-aw configuration.
51+ // If it doesn't exist, it creates it at the default location.
5052func ensureDevcontainerConfig (verbose bool , additionalRepos []string ) error {
51- devcontainerLog .Printf ("Creating or updating .devcontainer/gh-aw/ devcontainer.json with additional repos: %v" , additionalRepos )
53+ devcontainerLog .Printf ("Creating or updating devcontainer.json with additional repos: %v" , additionalRepos )
5254
53- // Create .devcontainer/gh-aw directory if it doesn't exist
54- // Using a subdirectory to avoid overriding existing devcontainer.json files
55- devcontainerDir := filepath .Join (".devcontainer" , "gh-aw" )
55+ // Check for existing devcontainer at default location first
56+ defaultDevcontainerPath := filepath .Join (".devcontainer" , "devcontainer.json" )
57+ devcontainerPath := defaultDevcontainerPath
58+
59+ // Create .devcontainer directory if it doesn't exist
60+ devcontainerDir := ".devcontainer"
5661 if err := os .MkdirAll (devcontainerDir , 0755 ); err != nil {
57- return fmt .Errorf ("failed to create .devcontainer/gh-aw directory: %w" , err )
62+ return fmt .Errorf ("failed to create .devcontainer directory: %w" , err )
5863 }
5964 devcontainerLog .Printf ("Ensured directory exists: %s" , devcontainerDir )
6065
61- devcontainerPath := filepath .Join (devcontainerDir , "devcontainer.json" )
62-
63- // Check if file already exists
66+ // Check if file already exists at default location
67+ var existingConfig * DevcontainerConfig
6468 if _ , err := os .Stat (devcontainerPath ); err == nil {
6569 devcontainerLog .Printf ("File already exists: %s" , devcontainerPath )
6670
67- // Read existing config to check if we need to update copilot-cli version
71+ // Read existing config to update it
6872 existingData , err := os .ReadFile (devcontainerPath )
6973 if err != nil {
7074 devcontainerLog .Printf ("Failed to read existing config: %v" , err )
71- if verbose {
72- fmt .Fprintf (os .Stderr , "Devcontainer already exists at %s (skipping)\n " , devcontainerPath )
73- }
74- return nil
75+ return fmt .Errorf ("failed to read existing devcontainer.json: %w" , err )
7576 }
7677
77- var existingConfig DevcontainerConfig
78- if err := json .Unmarshal (existingData , & existingConfig ); err != nil {
78+ var config DevcontainerConfig
79+ if err := json .Unmarshal (existingData , & config ); err != nil {
7980 devcontainerLog .Printf ("Failed to parse existing config: %v" , err )
80- if verbose {
81- fmt .Fprintf (os .Stderr , "Devcontainer already exists at %s (skipping)\n " , devcontainerPath )
82- }
83- return nil
84- }
85-
86- // Check if copilot-cli feature exists with a different version
87- needsUpdate := false
88- if existingConfig .Features != nil {
89- for key := range existingConfig .Features {
90- // Check if this is a copilot-cli feature with a different version
91- if strings .HasPrefix (key , "ghcr.io/devcontainers/features/copilot-cli:" ) && key != "ghcr.io/devcontainers/features/copilot-cli:latest" {
92- needsUpdate = true
93- // Remove the old version
94- delete (existingConfig .Features , key )
95- devcontainerLog .Printf ("Removing old copilot-cli version: %s" , key )
96- break
97- }
98- }
99- }
100-
101- if needsUpdate {
102- // Add the latest version
103- if existingConfig .Features == nil {
104- existingConfig .Features = make (DevcontainerFeatures )
105- }
106- existingConfig .Features ["ghcr.io/devcontainers/features/copilot-cli:latest" ] = map [string ]any {}
107- devcontainerLog .Printf ("Updated copilot-cli to :latest version" )
108-
109- // Write updated config
110- updatedData , err := json .MarshalIndent (existingConfig , "" , " " )
111- if err != nil {
112- return fmt .Errorf ("failed to marshal updated devcontainer.json: %w" , err )
113- }
114- updatedData = append (updatedData , '\n' )
115-
116- if err := os .WriteFile (devcontainerPath , updatedData , 0644 ); err != nil {
117- return fmt .Errorf ("failed to write updated devcontainer.json: %w" , err )
118- }
119- devcontainerLog .Printf ("Updated file: %s" , devcontainerPath )
120-
121- if verbose {
122- fmt .Fprintf (os .Stderr , "Updated copilot-cli to :latest in %s\n " , devcontainerPath )
123- }
124- } else {
125- if verbose {
126- fmt .Fprintf (os .Stderr , "Devcontainer already exists at %s (skipping)\n " , devcontainerPath )
127- }
81+ return fmt .Errorf ("failed to parse existing devcontainer.json: %w" , err )
12882 }
129- return nil
83+ existingConfig = & config
84+ devcontainerLog .Printf ("Successfully parsed existing devcontainer.json" )
13085 }
13186
13287 // Get current repository name from git remote
@@ -138,6 +93,115 @@ func ensureDevcontainerConfig(verbose bool, additionalRepos []string) error {
13893 // Get the owner from the current repository
13994 owner := getRepoOwner ()
14095
96+ // Prepare gh-aw specific configuration
97+ ghAwRepositories := buildRepositoryPermissions (repoName , owner , additionalRepos )
98+
99+ var config DevcontainerConfig
100+
101+ if existingConfig != nil {
102+ // Update existing configuration
103+ devcontainerLog .Printf ("Updating existing devcontainer.json" )
104+ config = * existingConfig
105+
106+ // Ensure customizations exists
107+ if config .Customizations == nil {
108+ config .Customizations = & DevcontainerCustomizations {}
109+ }
110+
111+ // Merge VSCode extensions
112+ if config .Customizations .VSCode == nil {
113+ config .Customizations .VSCode = & DevcontainerVSCode {}
114+ }
115+ config .Customizations .VSCode .Extensions = mergeExtensions (
116+ config .Customizations .VSCode .Extensions ,
117+ []string {"GitHub.copilot" , "GitHub.copilot-chat" },
118+ )
119+
120+ // Merge Codespaces repositories
121+ if config .Customizations .Codespaces == nil {
122+ config .Customizations .Codespaces = & DevcontainerCodespaces {
123+ Repositories : make (map [string ]DevcontainerRepoPermissions ),
124+ }
125+ }
126+ if config .Customizations .Codespaces .Repositories == nil {
127+ config .Customizations .Codespaces .Repositories = make (map [string ]DevcontainerRepoPermissions )
128+ }
129+ for repo , perms := range ghAwRepositories {
130+ config .Customizations .Codespaces .Repositories [repo ] = perms
131+ devcontainerLog .Printf ("Updated permissions for repo: %s" , repo )
132+ }
133+
134+ // Merge features
135+ if config .Features == nil {
136+ config .Features = make (DevcontainerFeatures )
137+ }
138+ mergeFeatures (config .Features , map [string ]any {
139+ "ghcr.io/devcontainers/features/github-cli:1" : map [string ]any {},
140+ "ghcr.io/devcontainers/features/copilot-cli:latest" : map [string ]any {},
141+ })
142+
143+ // Update postCreateCommand if not set or if it doesn't include gh-aw install
144+ if config .PostCreateCommand == "" || ! strings .Contains (config .PostCreateCommand , "install-gh-aw.sh" ) {
145+ ghAwInstall := "curl -fsSL https://raw.githubusercontent.com/githubnext/gh-aw/refs/heads/main/install-gh-aw.sh | bash"
146+ if config .PostCreateCommand == "" {
147+ config .PostCreateCommand = ghAwInstall
148+ } else {
149+ config .PostCreateCommand = config .PostCreateCommand + " && " + ghAwInstall
150+ }
151+ devcontainerLog .Printf ("Updated postCreateCommand to include gh-aw installation" )
152+ }
153+
154+ if verbose {
155+ fmt .Fprintf (os .Stderr , "Updated existing devcontainer at %s\n " , devcontainerPath )
156+ }
157+ } else {
158+ // Create new configuration
159+ devcontainerLog .Printf ("Creating new devcontainer.json at default location" )
160+ config = DevcontainerConfig {
161+ Name : "Agentic Workflows Development" ,
162+ Image : "mcr.microsoft.com/devcontainers/universal:latest" ,
163+ Customizations : & DevcontainerCustomizations {
164+ VSCode : & DevcontainerVSCode {
165+ Extensions : []string {
166+ "GitHub.copilot" ,
167+ "GitHub.copilot-chat" ,
168+ },
169+ },
170+ Codespaces : & DevcontainerCodespaces {
171+ Repositories : ghAwRepositories ,
172+ },
173+ },
174+ Features : DevcontainerFeatures {
175+ "ghcr.io/devcontainers/features/github-cli:1" : map [string ]any {},
176+ "ghcr.io/devcontainers/features/copilot-cli:latest" : map [string ]any {},
177+ },
178+ PostCreateCommand : "curl -fsSL https://raw.githubusercontent.com/githubnext/gh-aw/refs/heads/main/install-gh-aw.sh | bash" ,
179+ }
180+
181+ if verbose {
182+ fmt .Fprintf (os .Stderr , "Created new devcontainer at %s\n " , devcontainerPath )
183+ }
184+ }
185+
186+ // Write config file with proper indentation
187+ data , err := json .MarshalIndent (config , "" , " " )
188+ if err != nil {
189+ return fmt .Errorf ("failed to marshal devcontainer.json: %w" , err )
190+ }
191+
192+ // Add newline at end of file
193+ data = append (data , '\n' )
194+
195+ if err := os .WriteFile (devcontainerPath , data , 0644 ); err != nil {
196+ return fmt .Errorf ("failed to write devcontainer.json: %w" , err )
197+ }
198+ devcontainerLog .Printf ("Wrote file: %s" , devcontainerPath )
199+
200+ return nil
201+ }
202+
203+ // buildRepositoryPermissions creates the repository permissions map for gh-aw
204+ func buildRepositoryPermissions (repoName , owner string , additionalRepos []string ) map [string ]DevcontainerRepoPermissions {
141205 // Create repository permissions map
142206 // Reference: https://docs.github.com/en/codespaces/managing-your-codespaces/managing-repository-access-for-your-codespaces
143207 // Default codespace permissions are read/write to the repository from which it was created.
@@ -176,7 +240,9 @@ func ensureDevcontainerConfig(verbose bool, additionalRepos []string) error {
176240 if len (parts ) >= 2 {
177241 repoOwner := parts [0 ]
178242 if owner != "" && repoOwner != owner {
179- return fmt .Errorf ("repository '%s' is not in the same organization as the current repository (expected owner: '%s')" , repo , owner )
243+ // Skip repos with different owners rather than error
244+ devcontainerLog .Printf ("Skipping repository '%s' - different owner than current repo (expected: '%s')" , repo , owner )
245+ continue
180246 }
181247 }
182248 } else if owner != "" {
@@ -198,43 +264,48 @@ func ensureDevcontainerConfig(verbose bool, additionalRepos []string) error {
198264 }
199265 }
200266
201- // Create devcontainer configuration
202- config := DevcontainerConfig {
203- Name : "Agentic Workflows Development" ,
204- Image : "mcr.microsoft.com/devcontainers/universal:latest" ,
205- Customizations : & DevcontainerCustomizations {
206- VSCode : & DevcontainerVSCode {
207- Extensions : []string {
208- "GitHub.copilot" ,
209- "GitHub.copilot-chat" ,
210- },
211- },
212- Codespaces : & DevcontainerCodespaces {
213- Repositories : repositories ,
214- },
215- },
216- Features : DevcontainerFeatures {
217- "ghcr.io/devcontainers/features/github-cli:1" : map [string ]any {},
218- "ghcr.io/devcontainers/features/copilot-cli:latest" : map [string ]any {},
219- },
220- PostCreateCommand : "curl -fsSL https://raw.githubusercontent.com/githubnext/gh-aw/refs/heads/main/install-gh-aw.sh | bash" ,
221- }
267+ return repositories
268+ }
222269
223- // Write config file with proper indentation
224- data , err := json .MarshalIndent (config , "" , " " )
225- if err != nil {
226- return fmt .Errorf ("failed to marshal devcontainer.json: %w" , err )
270+ // mergeExtensions adds new extensions to existing list, avoiding duplicates
271+ func mergeExtensions (existing , toAdd []string ) []string {
272+ extensionSet := make (map [string ]bool )
273+ result := make ([]string , 0 , len (existing )+ len (toAdd ))
274+
275+ // Add existing extensions
276+ for _ , ext := range existing {
277+ if ! extensionSet [ext ] {
278+ extensionSet [ext ] = true
279+ result = append (result , ext )
280+ }
227281 }
228-
229- // Add newline at end of file
230- data = append (data , '\n' )
231-
232- if err := os .WriteFile (devcontainerPath , data , 0644 ); err != nil {
233- return fmt .Errorf ("failed to write devcontainer.json: %w" , err )
282+
283+ // Add new extensions if not already present
284+ for _ , ext := range toAdd {
285+ if ! extensionSet [ext ] {
286+ extensionSet [ext ] = true
287+ result = append (result , ext )
288+ }
234289 }
235- devcontainerLog .Printf ("Created file: %s" , devcontainerPath )
290+
291+ return result
292+ }
236293
237- return nil
294+ // mergeFeatures adds new features to existing features map, updating old copilot-cli versions
295+ func mergeFeatures (existing DevcontainerFeatures , toAdd map [string ]any ) {
296+ // First, remove old copilot-cli versions
297+ for key := range existing {
298+ if strings .HasPrefix (key , "ghcr.io/devcontainers/features/copilot-cli:" ) &&
299+ key != "ghcr.io/devcontainers/features/copilot-cli:latest" {
300+ delete (existing , key )
301+ devcontainerLog .Printf ("Removed old copilot-cli version: %s" , key )
302+ }
303+ }
304+
305+ // Add new features
306+ for key , value := range toAdd {
307+ existing [key ] = value
308+ }
238309}
239310
240311// getCurrentRepoName gets the current repository name from git remote in owner/repo format
0 commit comments