Skip to content
Open
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
14 changes: 14 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "built-fast",
"owner": {
"name": "BuiltFast",
"email": "support@builtfast.com"
},
"plugins": [
{
"name": "vector",
"source": "./",
"description": "Vector Pro integration for Claude Code: manage sites, environments, deployments, backups, WAF, and SSL via the vector CLI."
}
]
}
12 changes: 12 additions & 0 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "vector",
"version": "0.8.0",
"description": "Vector Pro integration for Claude Code. Manage sites, environments, deployments, backups, WAF, and SSL via the vector CLI, plus a /vector:doctor health check.",
"author": {
"name": "BuiltFast",
"email": "support@builtfast.com"
},
"homepage": "https://github.com/built-fast/vector-cli",
"repository": "https://github.com/built-fast/vector-cli",
"license": "MIT"
}
1 change: 1 addition & 0 deletions .surface
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ CMD vector deploy list
CMD vector deploy rollback
CMD vector deploy show
CMD vector deploy trigger
CMD vector doctor
CMD vector env
CMD vector env create
CMD vector env db
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,18 @@ vector webhook delete <webhook_id>
vector php-versions
```

### Doctor

Diagnose CLI setup, authentication, and live API connectivity:

```bash
vector doctor # table of checks (cli / auth / api)
vector doctor --json # machine-readable; backs /vector:doctor
```

`doctor` always exits 0 — read each check's status (`pass`/`warn`/`skip`/`fail`)
rather than the exit code.

### MCP Integration

Configure [Claude Desktop](https://claude.ai/download) to use Vector CLI as an MCP server:
Expand All @@ -314,6 +326,25 @@ Configure [Claude Desktop](https://claude.ai/download) to use Vector CLI as an M
vector mcp setup
```

### Claude Code Plugin

Install the Vector plugin to give Claude Code the bundled skill and a
`/vector:doctor` health-check command. Inside Claude Code:

```text
/plugin marketplace add built-fast/vector-cli
/plugin install vector@built-fast
```

The plugin reuses the same `SKILL.md` reference that `vector skill install`
provides — install it via the plugin or the standalone command, not both. For
other agents, point them directly at the embedded skill:

```bash
vector skill # print SKILL.md to stdout
vector skill install # install to ~/.agents/skills + ~/.claude/skills
```

## Output Format

- **Interactive (TTY)**: Human-readable table format
Expand Down
31 changes: 31 additions & 0 deletions commands/doctor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
description: Check Vector CLI health — binary, authentication, and live API connectivity.
allowed-tools: Bash(vector doctor:*)
---

# /vector:doctor

Run the Vector CLI health check and report the results.

```bash
vector doctor --json
```

The JSON output has an `ok` boolean and a `checks` array. Each check has a
`name`, a `status`, a `detail`, and an optional `hint`. Interpret the status:

- **pass** — working correctly
- **warn** — non-critical issue
- **skip** — check not run (e.g. unauthenticated)
- **fail** — broken, needs attention

The command always exits 0; rely on the `status` fields, not the exit code.

Common fixes (follow the `hint` field first):

- `auth` fail → run `vector auth login`, or set `VECTOR_API_KEY`
- `api` fail with "rejected" → token is invalid or expired; run `vector auth login`
- `api` fail with "network error" → check network/VPN connectivity

Report results concisely: list any failures and warnings with their hints. If
everything passes, say so in one line.
1 change: 1 addition & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ func NewRootCmd() *cobra.Command {
cmd.PersistentFlags().String("jq", "", `Filter JSON output with a jq expression (built-in, no external jq required)`)

cmd.AddCommand(commands.NewAuthCmd())
cmd.AddCommand(commands.NewDoctorCmd())
cmd.AddCommand(commands.NewSiteCmd())
cmd.AddCommand(commands.NewEnvCmd())
cmd.AddCommand(commands.NewDeployCmd())
Expand Down
207 changes: 207 additions & 0 deletions internal/commands/doctor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package commands

import (
"encoding/json"
"errors"
"fmt"
"io"

"github.com/spf13/cobra"

"github.com/built-fast/vector-cli/internal/api"
"github.com/built-fast/vector-cli/internal/appctx"
"github.com/built-fast/vector-cli/internal/output"
"github.com/built-fast/vector-cli/internal/version"
)

// Doctor check statuses, mirrored in the /vector:doctor plugin command.
const (
doctorPass = "pass" // working correctly
doctorWarn = "warn" // non-critical issue
doctorSkip = "skip" // check not run (e.g. unauthenticated)
doctorFail = "fail" // broken, needs attention
)

// doctorCheck is a single diagnostic result.
type doctorCheck struct {
Name string `json:"name"`
Status string `json:"status"`
Detail string `json:"detail"`
Hint string `json:"hint,omitempty"`
}

// NewDoctorCmd creates the doctor command.
func NewDoctorCmd() *cobra.Command {
return &cobra.Command{
Use: "doctor",
Short: "Diagnose CLI setup, authentication, and API connectivity",
Long: "Run health checks on the Vector CLI: binary version, configured " +
"authentication, and live API connectivity. Backs the Claude Code " +
"/vector:doctor command and is useful for troubleshooting. Always exits 0 " +
"on a successful run; health is reported in the status of each check.",
Example: ` # Run all health checks
vector doctor

# Machine-readable output (used by /vector:doctor)
vector doctor --json`,
RunE: func(cmd *cobra.Command, args []string) error {
return runDoctor(cmd)
},
}
}

// runDoctor gathers the health checks and renders them. It does not use
// requireApp: diagnosing the unauthenticated state is part of its job.
func runDoctor(cmd *cobra.Command) error {
app := appctx.FromContext(cmd.Context())
if app == nil {
return fmt.Errorf("app not initialized")
}

auth := doctorAuthCheck(app)
checks := []doctorCheck{
doctorCLICheck(),
auth,
doctorAPICheck(cmd, app, auth.Status == doctorPass),
}

ok := true
for _, c := range checks {
if c.Status == doctorFail {
ok = false
}
}

if app.Output.Format() == output.JSON {
return app.Output.JSON(map[string]any{
"ok": ok,
"api_url": app.Config.APIURL,
"checks": checks,
})
}

rows := make([][]string, 0, len(checks))
for _, c := range checks {
rows = append(rows, []string{c.Name, doctorStatusLabel(c.Status), c.Detail})
}
app.Output.Table([]string{"CHECK", "STATUS", "DETAIL"}, rows)

for _, c := range checks {
if c.Hint != "" {
app.Output.Message(fmt.Sprintf("→ %s: %s", c.Name, c.Hint))
}
}

return nil
}

// doctorCLICheck reports the running binary version; it never fails.
func doctorCLICheck() doctorCheck {
return doctorCheck{
Name: "cli",
Status: doctorPass,
Detail: version.FullVersion(),
}
}

// doctorAuthCheck verifies a token is configured, without contacting the API.
func doctorAuthCheck(app *appctx.App) doctorCheck {
if app.Client.Token == "" {
return doctorCheck{
Name: "auth",
Status: doctorFail,
Detail: "no API token configured",
Hint: "run 'vector auth login', pass --token, or set VECTOR_API_KEY",
}
}

source := app.TokenSource
if source == "" {
source = "unknown source"
}
return doctorCheck{
Name: "auth",
Status: doctorPass,
Detail: "token from " + source,
}
}

// doctorAPICheck validates the token against the live API. It is skipped when
// no token is configured (the auth check already reported that).
func doctorAPICheck(cmd *cobra.Command, app *appctx.App, haveToken bool) doctorCheck {
if !haveToken {
return doctorCheck{
Name: "api",
Status: doctorSkip,
Detail: "skipped (not authenticated)",
}
}

resp, err := app.Client.Get(cmd.Context(), "/api/v1/auth/whoami", nil)
if err != nil {
var apiErr *api.APIError
if errors.As(err, &apiErr) {
if apiErr.HTTPStatus == 401 || apiErr.HTTPStatus == 403 {
return doctorCheck{
Name: "api",
Status: doctorFail,
Detail: "token rejected (invalid or expired)",
Hint: "run 'vector auth login' to re-authenticate",
}
}
return doctorCheck{
Name: "api",
Status: doctorFail,
Detail: fmt.Sprintf("API error: %s", apiErr.Message),
Hint: "check the Vector status page and try again",
}
}
return doctorCheck{
Name: "api",
Status: doctorFail,
Detail: fmt.Sprintf("network error: %s", err),
Hint: "check your network connection or VPN",
}
}
defer func() { _ = resp.Body.Close() }()

body, err := io.ReadAll(resp.Body)
if err != nil {
return doctorCheck{
Name: "api",
Status: doctorFail,
Detail: fmt.Sprintf("reading response: %s", err),
}
}

var whoami whoamiResponse
if err := json.Unmarshal(body, &whoami); err != nil {
return doctorCheck{
Name: "api",
Status: doctorWarn,
Detail: "connected, but the response was not recognized",
}
}

return doctorCheck{
Name: "api",
Status: doctorPass,
Detail: fmt.Sprintf("authenticated as %s (%s)", whoami.Data.User.Email, whoami.Data.Account.Name),
}
}

// doctorStatusLabel renders a status for table output.
func doctorStatusLabel(status string) string {
switch status {
case doctorPass:
return "OK"
case doctorWarn:
return "WARN"
case doctorSkip:
return "SKIP"
case doctorFail:
return "FAIL"
default:
return status
}
}
Loading
Loading