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
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ brew tap disk0Dancer/tap && brew install climate

Or `go install github.com/disk0Dancer/climate/cmd/climate@latest`.

Enable local shell completion:

```bash
climate completion install --shell zsh
```

## How it works

One command turns an OpenAPI 3.x spec into a compiled Go binary with auth, JSON output, and structured errors.
Expand All @@ -33,6 +39,18 @@ climate generate --name myapi https://api.example.com/openapi.json
myapi <group> <operation> [flags] --output=json|table|raw
```

Generated CLIs also ship with spec-aware event commands:

```bash
myapi events list
myapi config profiles create work
myapi config profiles use work
myapi auth login
myapi config set --secret events.signing_secret supersecret
myapi events listen payment-succeeded --port 8081 --tunnel auto --signature-mode hmac
myapi events emit payment-succeeded --target-url http://localhost:8081/webhooks/payment-succeeded --signature-mode hmac
```

## Agent skill

An agent with climate can build its own tools. Point it at any OpenAPI spec —
Expand Down Expand Up @@ -61,19 +79,46 @@ Demo: [disk0Dancer/github](https://github.com/disk0Dancer/github) — 1 100+ end
| `generate` | Create CLI from OpenAPI spec |
| `compose` | Merge multiple specs (with prefixes) into one facade CLI |
| `mock` | Run local mock HTTP server from OpenAPI spec |
| `completion` | Print shell completions or install/uninstall them locally |
| `list` | Show registered CLIs |
| `remove` | Delete a generated CLI |
| `remove` | Interactively delete a generated CLI |
| `uninstall` | Remove the climate CLI itself, optionally with full cleanup |
| `upgrade` | Regenerate from updated spec |
| `publish` | Push CLI to GitHub with CI/auto-fix/release |
| `skill generate` | Emit agent skill prompt |

## Shell completion

```bash
# print a completion script
climate completion zsh

# install it into your local shell setup
climate completion install --shell zsh

# remove climate-managed completion wiring later
climate completion uninstall --shell zsh

# remove one generated CLI with confirmation
climate remove petstore

# uninstall only the climate executable
climate uninstall

# uninstall climate plus generated CLIs, manifest, and completions
climate uninstall --full
```

## Docs

- [Site](https://disk0dancer.github.io/climate/)
- [LLM index](https://disk0dancer.github.io/climate/llms.txt)
- [Compose design](docs/design-compose.md)
- [CI auto-fix design](docs/design-ci-autofix.md)
- [Mock design](docs/design-mock.md)
- [Generated event listener design](docs/design-generated-events.md)
- [Shell completion design](docs/design-shell-completions.md)
- [Uninstall design](docs/design-uninstall.md)
- [OpenAPI 3.0 support matrix](docs/openapi-3-support-matrix.md)

## Development
Expand Down
124 changes: 124 additions & 0 deletions cmd/climate/commands/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package commands

import (
"fmt"
"os"
"runtime"

cliCompletion "github.com/disk0Dancer/climate/internal/completion"
"github.com/spf13/cobra"
)

var (
completionInstallShell string
completionUninstallShell string
)

var completionCmd = &cobra.Command{
Use: "completion",
Short: "Generate and manage shell completions",
Long: `Generate shell completion scripts for climate or install them into your local shell setup.

Examples:
climate completion zsh
climate completion install --shell zsh
climate completion uninstall --shell zsh`,
}

var completionInstallCmd = &cobra.Command{
Use: "install",
Short: "Install shell completions into the local shell config",
RunE: func(cmd *cobra.Command, args []string) error {
home, err := os.UserHomeDir()
if err != nil {
exitError("Failed to find home directory", err)
}

shell, err := cliCompletion.ResolveShell(completionInstallShell, os.Getenv("SHELL"), runtime.GOOS)
if err != nil {
exitError("Failed to determine shell", err)
}

result, err := cliCompletion.Install(home, shell, runtime.GOOS, func(w cliCompletion.Writer) error {
return generateCompletionScript(cmd.Root(), shell, w)
})
if err != nil {
exitError("Failed to install shell completions", err)
}

writeJSON(result)
return nil
},
}

var completionUninstallCmd = &cobra.Command{
Use: "uninstall",
Short: "Remove climate-managed shell completions",
RunE: func(cmd *cobra.Command, args []string) error {
home, err := os.UserHomeDir()
if err != nil {
exitError("Failed to find home directory", err)
}

shell, err := cliCompletion.ResolveShell(completionUninstallShell, os.Getenv("SHELL"), runtime.GOOS)
if err != nil {
exitError("Failed to determine shell", err)
}

result, err := cliCompletion.Uninstall(home, shell, runtime.GOOS)
if err != nil {
exitError("Failed to uninstall shell completions", err)
}

writeJSON(result)
return nil
},
}

func newCompletionScriptCmd(shell cliCompletion.Shell) *cobra.Command {
return &cobra.Command{
Use: string(shell),
Short: fmt.Sprintf("Print the %s completion script", shell),
RunE: func(cmd *cobra.Command, args []string) error {
if err := generateCompletionScript(cmd.Root(), shell, cmd.OutOrStdout()); err != nil {
exitError("Failed to generate completion script", err)
}
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Tip: run `climate completion install --shell %s` to wire this into your local shell config.\n", shell)
return nil
},
}
}

func generateCompletionScript(root *cobra.Command, shell cliCompletion.Shell, out cliCompletion.Writer) error {
switch shell {
case cliCompletion.ShellBash:
return root.GenBashCompletionV2(out, true)
case cliCompletion.ShellZsh:
return root.GenZshCompletion(out)
case cliCompletion.ShellFish:
return root.GenFishCompletion(out, true)
case cliCompletion.ShellPowerShell:
return root.GenPowerShellCompletionWithDesc(out)
default:
return fmt.Errorf("unsupported shell %q", shell)
}
}

func completeSupportedShells(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return cliCompletion.SupportedShellNames(), cobra.ShellCompDirectiveNoFileComp
}

func init() {
completionInstallCmd.Flags().StringVar(&completionInstallShell, "shell", "", "Shell to install completions for")
completionUninstallCmd.Flags().StringVar(&completionUninstallShell, "shell", "", "Shell to uninstall completions for")
_ = completionInstallCmd.RegisterFlagCompletionFunc("shell", completeSupportedShells)
_ = completionUninstallCmd.RegisterFlagCompletionFunc("shell", completeSupportedShells)

completionCmd.AddCommand(newCompletionScriptCmd(cliCompletion.ShellBash))
completionCmd.AddCommand(newCompletionScriptCmd(cliCompletion.ShellZsh))
completionCmd.AddCommand(newCompletionScriptCmd(cliCompletion.ShellFish))
completionCmd.AddCommand(newCompletionScriptCmd(cliCompletion.ShellPowerShell))
completionCmd.AddCommand(completionInstallCmd)
completionCmd.AddCommand(completionUninstallCmd)
rootCmd.AddCommand(completionCmd)
}
105 changes: 105 additions & 0 deletions cmd/climate/commands/completion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package commands

import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)

func TestCompletionZshWritesScriptAndTip(t *testing.T) {
completionInstallShell = ""
completionUninstallShell = ""

var stdout bytes.Buffer
var stderr bytes.Buffer
rootCmd.SetOut(&stdout)
rootCmd.SetErr(&stderr)
rootCmd.SetArgs([]string{"completion", "zsh"})

if err := rootCmd.Execute(); err != nil {
t.Fatalf("Execute() error = %v", err)
}

if stdout.Len() == 0 {
t.Fatal("completion script should be written to stdout")
}
if !strings.Contains(stdout.String(), "climate") {
t.Fatal("completion output should mention the climate command")
}
if !strings.Contains(stderr.String(), "completion install --shell zsh") {
t.Fatal("stderr should include install tip")
}
}

func TestCompletionInstallAndUninstall(t *testing.T) {
completionInstallShell = ""
completionUninstallShell = ""

home := t.TempDir()
t.Setenv("HOME", home)

var stdout bytes.Buffer
var stderr bytes.Buffer
rootCmd.SetOut(&stdout)
rootCmd.SetErr(&stderr)
rootCmd.SetArgs([]string{"completion", "install", "--shell", "zsh"})

rawInstall := captureStdout(t, func() {
if err := rootCmd.Execute(); err != nil {
t.Fatalf("install Execute() error = %v", err)
}
})

var installResp struct {
Shell string `json:"shell"`
ScriptPath string `json:"script_path"`
ConfigPath string `json:"config_path"`
}
if err := json.Unmarshal([]byte(rawInstall), &installResp); err != nil {
t.Fatalf("unmarshal install response: %v", err)
}
if installResp.Shell != "zsh" {
t.Fatalf("Shell = %q, want zsh", installResp.Shell)
}
if _, err := os.Stat(installResp.ScriptPath); err != nil {
t.Fatalf("installed script missing: %v", err)
}
configBytes, err := os.ReadFile(filepath.Join(home, ".zshrc"))
if err != nil {
t.Fatalf("reading .zshrc: %v", err)
}
if !strings.Contains(string(configBytes), installResp.ScriptPath) {
t.Fatal(".zshrc should source installed completion script")
}

stdout.Reset()
stderr.Reset()
rootCmd.SetArgs([]string{"completion", "uninstall", "--shell", "zsh"})

rawUninstall := captureStdout(t, func() {
if err := rootCmd.Execute(); err != nil {
t.Fatalf("uninstall Execute() error = %v", err)
}
})

var uninstallResp struct {
ScriptRemoved bool `json:"script_removed"`
}
if err := json.Unmarshal([]byte(rawUninstall), &uninstallResp); err != nil {
t.Fatalf("unmarshal uninstall response: %v", err)
}
if !uninstallResp.ScriptRemoved {
t.Fatal("script_removed should be true after uninstall")
}

configBytes, err = os.ReadFile(filepath.Join(home, ".zshrc"))
if err != nil {
t.Fatalf("reading .zshrc after uninstall: %v", err)
}
if strings.Contains(string(configBytes), "climate completion") {
t.Fatal(".zshrc should not contain climate-managed completion block after uninstall")
}
}
Loading
Loading