Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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 ./...
5 changes: 4 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name> [--url <url>] [-d <description>] [--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 <endpoint> [keyword] [--query] [--mutation] [--type] [--input] [--enum]
graphql-cli find -e <endpoint> [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
Expand Down
60 changes: 41 additions & 19 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -54,36 +55,42 @@ 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 <name> --url <new-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 <endpoint>

# Non-interactive
graphql-cli login <endpoint> --type token --token "my-api-key"
graphql-cli login <endpoint> --type basic --user admin --pass secret
graphql-cli login <endpoint> --type header --key X-API-Key --value "key123"
graphql-cli login -e production --type token --token "my-token"

# Remove credentials
graphql-cli logout <endpoint>
```

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 '<graphql-query>' -e <endpoint>
Expand All @@ -92,20 +99,28 @@ graphql-cli query '{ user(id: "1") { name } }' -e <endpoint> -v '{"id": "1"}'
graphql-cli query '{ me { name } }' -e <endpoint> -H "Authorization=Bearer token"
```

### 5. Execute a mutation
### 6. Execute a mutation

```bash
graphql-cli mutate '<graphql-mutation>' -e <endpoint>
graphql-cli mutate -f mutation.graphql -e <endpoint> -v '{"name": "test"}'
graphql-cli mutate 'mutation { createUser(name: "test") { id } }' -e <endpoint>
```

### 6. Explore the schema
### 7. Explore the schema

```bash
# Search all definitions
# Search all definitions (names only by default)
graphql-cli find <keyword> -e <endpoint>

# Keyword supports glob syntax (*, ?, [...])
# Without glob characters, matches as substring
graphql-cli find "get*" -e <endpoint>
graphql-cli find "User?" -e <endpoint>

# Show full definitions with fields and arguments
graphql-cli find <keyword> -e <endpoint> --detail

# Narrow by kind
graphql-cli find user -e <endpoint> --query # Query fields only
graphql-cli find user -e <endpoint> --mutation # Mutation fields only
Expand Down Expand Up @@ -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
```
```

## 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.
22 changes: 16 additions & 6 deletions cmd/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -33,6 +41,7 @@ var (
findType bool
findInput bool
findEnum bool
findDetail bool
)

func init() {
Expand All @@ -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)
}

Expand Down Expand Up @@ -78,7 +88,7 @@ func runFind(cmd *cobra.Command, args []string) error {
return err
}

printer.PrintFindResults(results)
printer.PrintFindResults(results, findDetail)

return nil
}
88 changes: 88 additions & 0 deletions cmd/update.go
Original file line number Diff line number Diff line change
@@ -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 <name>",
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The parseHeader function is used here but is not defined within this file or imported from another package in the provided context. This will lead to a compilation error. Please define this helper function or import it if it exists elsewhere.

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
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
26 changes: 26 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading