From 611ec1802766c7ba0e96eebaf30f25c5751795f7 Mon Sep 17 00:00:00 2001 From: looplj Date: Tue, 24 Mar 2026 22:17:55 +0800 Subject: [PATCH] Find by glob pattern --- .github/workflows/ci.yml | 26 +++++++++++ .github/workflows/release.yml | 5 +- README.md | 22 ++++++++- SKILL.md | 60 ++++++++++++++++-------- cmd/find.go | 22 ++++++--- cmd/update.go | 88 +++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + internal/config/config.go | 26 +++++++++++ internal/printer/printer.go | 27 +++++++---- internal/schema/finder.go | 27 +++++++++-- 11 files changed, 265 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 cmd/update.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2e01b68 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: ci + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Test + run: go test ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fca3d7e..bfc2660 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,9 +20,12 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: stable + go-version-file: go.mod cache: true + - name: Test + run: go test ./... + - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: diff --git a/README.md b/README.md index 255893c..a3bba4d 100644 --- a/README.md +++ b/README.md @@ -81,19 +81,39 @@ graphql-cli mutate -e production 'mutation { createUser(name: "test") { id } }' graphql-cli mutate -e production -f mutation.graphql -v '{"name": "test"}' ``` +### `update` — Update an existing endpoint + +```bash +graphql-cli update [--url ] [-d ] [--header key=value] +``` + +**Examples:** + +```bash +graphql-cli update production --url https://api.example.com/v2/graphql +graphql-cli update production --header "Authorization=Bearer new-token" +graphql-cli update production --url https://new-url.com/graphql --header "X-Custom=value" -d "Updated endpoint" +``` + ### `find` — Search schema definitions ```bash -graphql-cli find -e [keyword] [--query] [--mutation] [--type] [--input] [--enum] +graphql-cli find -e [keyword] [--query] [--mutation] [--type] [--input] [--enum] [--detail] ``` +By default, only names are shown. Use `--detail` to display full definitions with fields and arguments. + +The keyword supports glob syntax (`*`, `?`, `[...]`). Without glob characters, it matches as a substring (e.g., `user` matches `getUser`, `UserInput`). + **Examples:** ```bash graphql-cli find -e production user +graphql-cli find -e production "get*" graphql-cli find -e production user --query graphql-cli find -e production --mutation graphql-cli find -e production status --enum +graphql-cli find -e production user --detail ``` ### `login` — Authenticate with an endpoint diff --git a/SKILL.md b/SKILL.md index 995b0ef..1d1a600 100644 --- a/SKILL.md +++ b/SKILL.md @@ -15,6 +15,7 @@ A skill for managing GraphQL endpoints and executing operations using `graphql-c ## Capabilities - Add and manage multiple GraphQL endpoints (remote URL or local schema file) +- Update existing endpoint URL, headers, or description - Authenticate with endpoints (Bearer token, Basic auth, custom header) - Execute GraphQL queries and mutations - Explore and search GraphQL schemas by keyword @@ -54,23 +55,34 @@ graphql-cli add production --url https://api.example.com/graphql --description " graphql-cli add local --schema-file ./testdata/schema.graphql --description "Local schema" ``` -### 2. List endpoints +### 2. Update an endpoint + +```bash +graphql-cli update --url [--description "desc"] [--header "Key=Value"] +``` + +Example: +```bash +graphql-cli update production --url https://api.example.com/v2/graphql +graphql-cli update production --header "Authorization=Bearer new-token" -d "Updated prod API" +``` + +Headers are merged — existing headers not specified in the update are preserved. + +### 3. List endpoints ```bash graphql-cli list # names and URLs graphql-cli list --detail # includes headers (masked) and auth status ``` -### 3. Authenticate +### 4. Authenticate ```bash -# Interactive (prompts for auth type and credentials) -graphql-cli login - -# Non-interactive graphql-cli login --type token --token "my-api-key" graphql-cli login --type basic --user admin --pass secret graphql-cli login --type header --key X-API-Key --value "key123" +graphql-cli login -e production --type token --token "my-token" # Remove credentials graphql-cli logout @@ -78,12 +90,7 @@ graphql-cli logout Credentials are stored in the OS keyring (macOS Keychain, Windows Credential Manager, GNOME Keyring) with a plaintext file fallback. -You can also specify the endpoint via `-e`: -```bash -graphql-cli login -e production --type token --token "my-token" -``` - -### 4. Execute a query +### 5. Execute a query ```bash graphql-cli query '' -e @@ -92,7 +99,7 @@ graphql-cli query '{ user(id: "1") { name } }' -e -v '{"id": "1"}' graphql-cli query '{ me { name } }' -e -H "Authorization=Bearer token" ``` -### 5. Execute a mutation +### 6. Execute a mutation ```bash graphql-cli mutate '' -e @@ -100,12 +107,20 @@ graphql-cli mutate -f mutation.graphql -e -v '{"name": "test"}' graphql-cli mutate 'mutation { createUser(name: "test") { id } }' -e ``` -### 6. Explore the schema +### 7. Explore the schema ```bash -# Search all definitions +# Search all definitions (names only by default) graphql-cli find -e +# Keyword supports glob syntax (*, ?, [...]) +# Without glob characters, matches as substring +graphql-cli find "get*" -e +graphql-cli find "User?" -e + +# Show full definitions with fields and arguments +graphql-cli find -e --detail + # Narrow by kind graphql-cli find user -e --query # Query fields only graphql-cli find user -e --mutation # Mutation fields only @@ -144,12 +159,19 @@ graphql-cli query '{ users { id name } }' -e prod 2>/dev/null | jq '.users[0]' ### Explore before querying ```bash -# First, find what queries are available +# First, find what queries are available (names only) graphql-cli find -e prod --query -# Then find the input types needed -graphql-cli find CreateUser -e prod --input +# Then use --detail to see full definitions with fields and arguments +graphql-cli find user -e prod --query --detail + +# Find the input types needed +graphql-cli find CreateUser -e prod --input --detail # Then execute graphql-cli mutate 'mutation { createUser(input: {name: "Alice", email: "alice@example.com"}) { id } }' -e prod -``` \ No newline at end of file +``` + +## Guidelines + +- **Always use `find` without `--detail` first** to get an overview of matching names, then use `find --detail` on specific results to see full definitions with fields and arguments. This avoids overwhelming output when schemas are large. \ No newline at end of file diff --git a/cmd/find.go b/cmd/find.go index 60583c3..167f3e1 100644 --- a/cmd/find.go +++ b/cmd/find.go @@ -16,12 +16,20 @@ var findCmd = &cobra.Command{ Long: `Search for types, queries, mutations, inputs, and enums in the GraphQL schema. Use flags to narrow the search scope. +The keyword supports glob syntax (*, ?, [...]). Without glob characters, +it matches as a substring (e.g., "user" matches "getUser", "UserInput"). + Examples: - graphql-cli find user - graphql-cli find user --query - graphql-cli find --mutation - graphql-cli find user --type --input - graphql-cli find status --enum`, + graphql-cli find user # substring match + graphql-cli find "get*" # glob: starts with "get" + graphql-cli find "User?" # glob: "User" + one char + graphql-cli find "{createUser,CreateUserInput}" # glob: exact alternatives + graphql-cli find "[A-Z]*Input" # glob: capitalized, ends with "Input" + graphql-cli find user --query # only Query fields + graphql-cli find --mutation # list all mutations + graphql-cli find user --type --input # types and inputs + graphql-cli find status --enum # enums only + graphql-cli find user --detail # show full definitions`, Args: cobra.MaximumNArgs(1), PreRunE: requireEndpoint, RunE: runFind, @@ -33,6 +41,7 @@ var ( findType bool findInput bool findEnum bool + findDetail bool ) func init() { @@ -41,6 +50,7 @@ func init() { findCmd.Flags().BoolVar(&findType, "type", false, "search only Object/Interface/Union/Scalar types") findCmd.Flags().BoolVar(&findInput, "input", false, "search only Input types") findCmd.Flags().BoolVar(&findEnum, "enum", false, "search only Enum types") + findCmd.Flags().BoolVar(&findDetail, "detail", false, "show fields and arguments") rootCmd.AddCommand(findCmd) } @@ -78,7 +88,7 @@ func runFind(cmd *cobra.Command, args []string) error { return err } - printer.PrintFindResults(results) + printer.PrintFindResults(results, findDetail) return nil } diff --git a/cmd/update.go b/cmd/update.go new file mode 100644 index 0000000..2dfa09a --- /dev/null +++ b/cmd/update.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "fmt" + "net/url" + + "github.com/spf13/cobra" + + "github.com/looplj/graphql-cli/internal/config" +) + +var updateCmd = &cobra.Command{ + Use: "update ", + Short: "Update an existing endpoint's URL or headers", + Long: `Update an existing GraphQL endpoint configuration. + +Examples: + graphql-cli update production --url https://api.example.com/v2/graphql + graphql-cli update production --header "Authorization=Bearer new-token" + graphql-cli update production --url https://new-url.com/graphql --header "X-Custom=value"`, + Args: cobra.ExactArgs(1), + RunE: runUpdate, +} + +var ( + updateURL string + updateDescription string + updateHeaders []string +) + +func init() { + updateCmd.Flags().StringVar(&updateURL, "url", "", "new GraphQL endpoint URL") + updateCmd.Flags().StringVarP(&updateDescription, "description", "d", "", "new endpoint description") + updateCmd.Flags().StringSliceVar(&updateHeaders, "header", nil, "HTTP headers to add/update (key=value), can be specified multiple times") + rootCmd.AddCommand(updateCmd) +} + +func runUpdate(cmd *cobra.Command, args []string) error { + name := args[0] + + var urlPtr *string + + if cmd.Flags().Changed("url") { + u, err := url.Parse(updateURL) + if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { + return fmt.Errorf("invalid endpoint URL %q: must be a valid http or https URL", updateURL) + } + + urlPtr = &updateURL + } + + var descPtr *string + if cmd.Flags().Changed("description") { + descPtr = &updateDescription + } + + headers := make(map[string]string) + + for _, h := range updateHeaders { + k, v, ok := parseHeader(h) + if !ok { + return fmt.Errorf("invalid header format %q, expected key=value", h) + } + + headers[k] = v + } + + if urlPtr == nil && descPtr == nil && len(headers) == 0 { + return fmt.Errorf("must specify at least one of --url, --description, or --header") + } + + cfg, err := config.Load(cfgFile) + if err != nil { + return err + } + + if err := cfg.UpdateEndpoint(name, urlPtr, descPtr, headers); err != nil { + return err + } + + if err := cfg.Save(cfgFile); err != nil { + return err + } + + fmt.Printf("Updated endpoint %q\n", name) + + return nil +} diff --git a/go.mod b/go.mod index bb6f18a..66cc6be 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.0 require ( github.com/fatih/color v1.19.0 + github.com/gobwas/glob v0.2.3 github.com/spf13/cobra v1.10.2 github.com/vektah/gqlparser/v2 v2.5.32 github.com/zalando/go-keyring v0.2.7 diff --git a/go.sum b/go.sum index 6e10f99..121558c 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7c github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= diff --git a/internal/config/config.go b/internal/config/config.go index a45d7c8..79ffd84 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -85,6 +85,32 @@ func (c *Config) GetEndpoint(name string) (*Endpoint, error) { return nil, fmt.Errorf("endpoint %q not found", name) } +func (c *Config) UpdateEndpoint(name string, url *string, description *string, headers map[string]string) error { + for i := range c.Endpoints { + if c.Endpoints[i].Name == name { + if url != nil { + c.Endpoints[i].URL = *url + } + + if description != nil { + c.Endpoints[i].Description = *description + } + + for k, v := range headers { + if c.Endpoints[i].Headers == nil { + c.Endpoints[i].Headers = make(map[string]string) + } + + c.Endpoints[i].Headers[k] = v + } + + return nil + } + } + + return fmt.Errorf("endpoint %q not found", name) +} + func (c *Config) AddEndpoint(ep Endpoint) error { for _, e := range c.Endpoints { if e.Name == ep.Name { diff --git a/internal/printer/printer.go b/internal/printer/printer.go index a882cd6..958f335 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -17,23 +17,30 @@ var ( dimColor = color.New(color.FgHiBlack) ) -func PrintFindResults(results []schema.FindResult) { +func PrintFindResults(results []schema.FindResult, detail bool) { if len(results) == 0 { dimColor.Println("No results found.") return } - for i, r := range results { - if i > 0 { - fmt.Println() - dimColor.Println(strings.Repeat("─", 60)) + if detail { + for i, r := range results { + if i > 0 { + fmt.Println() + dimColor.Println(strings.Repeat("─", 60)) + fmt.Println() + } + + kindColor.Printf("[%s] ", strings.ToUpper(r.Kind)) + nameColor.Println(r.Name) fmt.Println() + fmt.Println(r.Definition) + } + } else { + for _, r := range results { + kindColor.Printf("[%s] ", strings.ToUpper(r.Kind)) + nameColor.Println(r.Name) } - - kindColor.Printf("[%s] ", strings.ToUpper(r.Kind)) - nameColor.Println(r.Name) - fmt.Println() - fmt.Println(r.Definition) } } diff --git a/internal/schema/finder.go b/internal/schema/finder.go index bdfea34..45497e8 100644 --- a/internal/schema/finder.go +++ b/internal/schema/finder.go @@ -1,8 +1,10 @@ package schema import ( + "fmt" "strings" + "github.com/gobwas/glob" "github.com/vektah/gqlparser/v2" "github.com/vektah/gqlparser/v2/ast" ) @@ -35,7 +37,24 @@ func ParseAndFind(sdl string, keyword string, scope FindScope) ([]FindResult, er } searchAll := scope.IsEmpty() - keyword = strings.ToLower(keyword) + + var matcher glob.Glob + + if keyword != "" { + pattern := strings.ToLower(keyword) + if !strings.ContainsAny(pattern, "*?[") { + pattern = "*" + pattern + "*" + } + + matcher, err = glob.Compile(pattern) + if err != nil { + return nil, fmt.Errorf("invalid glob pattern %q: %w", keyword, err) + } + } + + matchName := func(name string) bool { + return matcher == nil || matcher.Match(strings.ToLower(name)) + } var results []FindResult @@ -46,7 +65,7 @@ func ParseAndFind(sdl string, keyword string, scope FindScope) ([]FindResult, er continue } - if keyword == "" || strings.Contains(strings.ToLower(f.Name), keyword) { + if matchName(f.Name) { results = append(results, FindResult{ Kind: "query", Name: f.Name, @@ -64,7 +83,7 @@ func ParseAndFind(sdl string, keyword string, scope FindScope) ([]FindResult, er continue } - if keyword == "" || strings.Contains(strings.ToLower(f.Name), keyword) { + if matchName(f.Name) { results = append(results, FindResult{ Kind: "mutation", Name: f.Name, @@ -81,7 +100,7 @@ func ParseAndFind(sdl string, keyword string, scope FindScope) ([]FindResult, er continue } - if keyword != "" && !strings.Contains(strings.ToLower(t.Name), keyword) { + if !matchName(t.Name) { continue }