11package cli
22
33import (
4+ "encoding/json"
45 "fmt"
56 "os"
7+ "os/exec"
8+ "strings"
69
710 "github.com/githubnext/gh-aw/pkg/console"
11+ "github.com/githubnext/gh-aw/pkg/workflow"
812 "github.com/spf13/cobra"
913)
1014
@@ -116,6 +120,8 @@ var recommendedTokenSpecs = []tokenSpec{
116120// NewTokensBootstrapSubcommand creates the `tokens bootstrap` subcommand
117121func NewTokensBootstrapSubcommand () * cobra.Command {
118122 var engineFlag string
123+ var ownerFlag string
124+ var repoFlag string
119125
120126 cmd := & cobra.Command {
121127 Use : "bootstrap" ,
@@ -131,19 +137,31 @@ available) and prints the exact secrets to add and suggested scopes.
131137For full details, including precedence rules, see the GitHub Tokens
132138reference in the documentation.` ,
133139 RunE : func (cmd * cobra.Command , args []string ) error {
134- return runTokensBootstrap (engineFlag )
140+ return runTokensBootstrap (engineFlag , ownerFlag , repoFlag )
135141 },
136142 }
137143
138144 cmd .Flags ().StringVarP (& engineFlag , "engine" , "e" , "" , "Check tokens for specific engine (copilot, claude, codex)" )
145+ cmd .Flags ().StringVar (& ownerFlag , "owner" , "" , "Repository owner (defaults to current repository)" )
146+ cmd .Flags ().StringVar (& repoFlag , "repo" , "" , "Repository name (defaults to current repository)" )
139147
140148 return cmd
141149}
142150
143- func runTokensBootstrap (engine string ) error {
144- repoSlug , err := GetCurrentRepoSlug ()
145- if err != nil {
146- return fmt .Errorf ("failed to detect current repository: %w" , err )
151+ func runTokensBootstrap (engine , owner , repo string ) error {
152+ var repoSlug string
153+ var err error
154+
155+ // Determine target repository
156+ if owner != "" && repo != "" {
157+ repoSlug = fmt .Sprintf ("%s/%s" , owner , repo )
158+ } else if owner != "" || repo != "" {
159+ return fmt .Errorf ("both --owner and --repo must be specified together" )
160+ } else {
161+ repoSlug , err = GetCurrentRepoSlug ()
162+ if err != nil {
163+ return fmt .Errorf ("failed to detect current repository: %w" , err )
164+ }
147165 }
148166
149167 fmt .Fprintln (os .Stderr , console .FormatInfoMessage (fmt .Sprintf ("Checking recommended gh-aw token secrets in %s..." , repoSlug )))
@@ -160,7 +178,7 @@ func runTokensBootstrap(engine string) error {
160178 missing := make ([]tokenSpec , 0 , len (tokensToCheck ))
161179
162180 for _ , spec := range tokensToCheck {
163- exists , err := checkSecretExists (spec .Name )
181+ exists , err := checkSecretExistsInRepo (spec .Name , repoSlug )
164182 if err != nil {
165183 // If we hit a 403 or other error, surface a friendly message and abort
166184 return fmt .Errorf ("unable to inspect repository secrets (gh secret list failed for %s): %w" , spec .Name , err )
@@ -185,14 +203,19 @@ func runTokensBootstrap(engine string) error {
185203 }
186204 }
187205
206+ // Extract owner and repo from slug for command examples
207+ parts := splitRepoSlug (repoSlug )
208+ cmdOwner := parts [0 ]
209+ cmdRepo := parts [1 ]
210+
188211 if len (requiredMissing ) > 0 {
189212 fmt .Fprintln (os .Stderr , console .FormatErrorMessage ("Required gh-aw token secrets are missing:" ))
190213 for _ , spec := range requiredMissing {
191214 fmt .Fprintln (os .Stderr , "" )
192215 fmt .Fprintln (os .Stderr , console .FormatInfoMessage (fmt .Sprintf ("Secret: %s" , spec .Name )))
193216 fmt .Fprintln (os .Stderr , console .FormatInfoMessage (fmt .Sprintf ("When needed: %s" , spec .When )))
194217 fmt .Fprintln (os .Stderr , console .FormatInfoMessage (fmt .Sprintf ("Recommended scopes: %s" , spec .Description )))
195- fmt .Fprintln (os .Stderr , console .FormatCommandMessage (fmt .Sprintf ("gh aw secret set %s --owner <owner> --repo <repo> " , spec .Name )))
218+ fmt .Fprintln (os .Stderr , console .FormatCommandMessage (fmt .Sprintf ("gh aw secret set %s --owner %s --repo %s " , spec .Name , cmdOwner , cmdRepo )))
196219 }
197220 }
198221
@@ -204,7 +227,7 @@ func runTokensBootstrap(engine string) error {
204227 fmt .Fprintln (os .Stderr , console .FormatInfoMessage (fmt .Sprintf ("Secret: %s (optional)" , spec .Name )))
205228 fmt .Fprintln (os .Stderr , console .FormatInfoMessage (fmt .Sprintf ("When needed: %s" , spec .When )))
206229 fmt .Fprintln (os .Stderr , console .FormatInfoMessage (fmt .Sprintf ("Recommended scopes: %s" , spec .Description )))
207- fmt .Fprintln (os .Stderr , console .FormatCommandMessage (fmt .Sprintf ("gh aw secret set %s --owner <owner> --repo <repo> " , spec .Name )))
230+ fmt .Fprintln (os .Stderr , console .FormatCommandMessage (fmt .Sprintf ("gh aw secret set %s --owner %s --repo %s " , spec .Name , cmdOwner , cmdRepo )))
208231 }
209232 }
210233
@@ -213,3 +236,48 @@ func runTokensBootstrap(engine string) error {
213236
214237 return nil
215238}
239+
240+ // checkSecretExistsInRepo checks if a secret exists in a specific repository
241+ func checkSecretExistsInRepo (secretName , repoSlug string ) (bool , error ) {
242+ secretsLog .Printf ("Checking if secret exists in %s: %s" , repoSlug , secretName )
243+
244+ // Use gh CLI to list repository secrets
245+ cmd := workflow .ExecGH ("secret" , "list" , "--repo" , repoSlug , "--json" , "name" )
246+ output , err := cmd .Output ()
247+ if err != nil {
248+ // Check if it's a 403 error by examining the error
249+ if exitError , ok := err .(* exec.ExitError ); ok {
250+ if strings .Contains (string (exitError .Stderr ), "403" ) {
251+ return false , fmt .Errorf ("403 access denied" )
252+ }
253+ }
254+ return false , fmt .Errorf ("failed to list secrets: %w" , err )
255+ }
256+
257+ // Parse the JSON output
258+ var secrets []struct {
259+ Name string `json:"name"`
260+ }
261+
262+ if err := json .Unmarshal (output , & secrets ); err != nil {
263+ return false , fmt .Errorf ("failed to parse secrets list: %w" , err )
264+ }
265+
266+ // Check if our secret exists
267+ for _ , secret := range secrets {
268+ if secret .Name == secretName {
269+ return true , nil
270+ }
271+ }
272+
273+ return false , nil
274+ }
275+
276+ // splitRepoSlug splits "owner/repo" into [owner, repo]
277+ func splitRepoSlug (slug string ) [2 ]string {
278+ parts := strings .SplitN (slug , "/" , 2 )
279+ if len (parts ) == 2 {
280+ return [2 ]string {parts [0 ], parts [1 ]}
281+ }
282+ return [2 ]string {slug , "" }
283+ }
0 commit comments