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
150 changes: 150 additions & 0 deletions internal/cli/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package cli

import (
"encoding/json"
"fmt"
"os"
"strconv"

"github.com/agentregistry-dev/agentregistry/internal/client"
"github.com/agentregistry-dev/agentregistry/internal/version"
"github.com/agentregistry-dev/agentregistry/pkg/printer"
"github.com/spf13/cobra"
)

type statusResult struct {
Registry registryStatus `json:"registry"`
CLI cliStatus `json:"cli"`
Artifacts artifactCounts `json:"artifacts"`
Deployments int `json:"deployments"`
}

type registryStatus struct {
URL string `json:"url"`
Reachable bool `json:"reachable"`
Version string `json:"version,omitempty"`
}

type cliStatus struct {
Version string `json:"version"`
GitCommit string `json:"git_commit"`
}

type artifactCounts struct {
MCPServers int `json:"mcp_servers"`
Agents int `json:"agents"`
Skills int `json:"skills"`
Prompts int `json:"prompts"`
}

var statusJSON bool

var StatusCmd = &cobra.Command{
Use: "status",
Short: "Show registry connectivity, artifact counts, and CLI info",
Long: `Displays an at-a-glance health summary: whether the registry is reachable,
how many artifacts are published, active deployments, and CLI/server versions.`,
Run: func(cmd *cobra.Command, args []string) {
result := statusResult{
CLI: cliStatus{
Version: version.Version,
GitCommit: version.GitCommit,
},
}

// Build a best-effort client (status command skips root pre-run)
c := apiClient
if c == nil {
c = client.NewClient(os.Getenv("ARCTL_API_BASE_URL"), os.Getenv("ARCTL_API_TOKEN"))
}
result.Registry.URL = c.BaseURL

// Check connectivity
if err := c.Ping(); err != nil {
result.Registry.Reachable = false
if statusJSON {
printStatusJSON(result)
} else {
printStatusTable(result)
}
return
}
result.Registry.Reachable = true

// Server version
if v, err := c.GetVersion(); err == nil {
result.Registry.Version = v.Version
}

// Artifact counts (best-effort, don't fail on individual errors)
if servers, err := c.GetPublishedServers(); err == nil {
result.Artifacts.MCPServers = len(servers)
}
if agents, err := c.GetAgents(); err == nil {
result.Artifacts.Agents = len(agents)
}
if skills, err := c.GetSkills(); err == nil {
result.Artifacts.Skills = len(skills)
}
if prompts, err := c.GetPrompts(); err == nil {
result.Artifacts.Prompts = len(prompts)
}

// Deployments
if deployments, err := c.GetDeployedServers(); err == nil {
result.Deployments = len(deployments)
}

if statusJSON {
printStatusJSON(result)
} else {
printStatusTable(result)
}
},
}

func init() {
StatusCmd.Flags().BoolVar(&statusJSON, "json", false, "Output status in JSON format")
}

func printStatusJSON(r statusResult) {
data, err := json.MarshalIndent(r, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "Error marshaling JSON: %v\n", err)
return
}
fmt.Println(string(data))
}

func printStatusTable(r statusResult) {
fmt.Println()

// Registry connectivity
if r.Registry.Reachable {
printer.PrintSuccess(fmt.Sprintf("Registry: %s (v%s)", r.Registry.URL, r.Registry.Version))
} else {
printer.PrintError(fmt.Sprintf("Registry: %s (unreachable)", r.Registry.URL))
fmt.Println()
printer.PrintInfo("Start the daemon with: arctl daemon start")
fmt.Println()
return
}

// CLI info
printer.PrintInfo(fmt.Sprintf("CLI: v%s (%s)", r.CLI.Version, r.CLI.GitCommit))
fmt.Println()

// Artifact table
headers := []string{"Artifact", "Count"}
rows := [][]string{
{"MCP Servers", strconv.Itoa(r.Artifacts.MCPServers)},
{"Agents", strconv.Itoa(r.Artifacts.Agents)},
{"Skills", strconv.Itoa(r.Artifacts.Skills)},
{"Prompts", strconv.Itoa(r.Artifacts.Prompts)},
{"Deployments", strconv.Itoa(r.Deployments)},
}
if err := printer.PrintTable(headers, rows); err != nil {
fmt.Fprintf(os.Stderr, "Error printing table: %v\n", err)
}
fmt.Println()
}
124 changes: 124 additions & 0 deletions internal/cli/status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package cli

import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"

"github.com/agentregistry-dev/agentregistry/internal/client"
)

func TestStatusCmd_Reachable(t *testing.T) {
// Create a mock server that responds to /ping and /version
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/ping":
w.WriteHeader(http.StatusOK)
case "/version":
json.NewEncoder(w).Encode(map[string]string{
"version": "0.3.2",
"git_commit": "abc123",
"build_time": "2026-03-30",
})
default:
// Return empty arrays for list endpoints
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode([]interface{}{})
}
}))
defer srv.Close()

// Set the API client to use the mock server
apiClient = client.NewClient(srv.URL, "")
defer func() { apiClient = nil }()

// Capture stdout
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w

StatusCmd.Run(StatusCmd, []string{})

w.Close()
os.Stdout = old

var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()

if len(output) == 0 {
t.Error("Expected non-empty output from status command")
}
}

func TestStatusCmd_Unreachable(t *testing.T) {
// Point at a non-existent server
apiClient = client.NewClient("http://127.0.0.1:1", "")
defer func() { apiClient = nil }()

// Capture stdout
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w

StatusCmd.Run(StatusCmd, []string{})

w.Close()
os.Stdout = old

var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()

if len(output) == 0 {
t.Error("Expected non-empty output from status command")
}
}

func TestStatusCmd_JSON(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/ping":
w.WriteHeader(http.StatusOK)
case "/version":
json.NewEncoder(w).Encode(map[string]string{
"version": "0.3.2",
})
default:
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode([]interface{}{})
}
}))
defer srv.Close()

apiClient = client.NewClient(srv.URL, "")
defer func() { apiClient = nil }()

// Set JSON flag
statusJSON = true
defer func() { statusJSON = false }()

old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w

StatusCmd.Run(StatusCmd, []string{})

w.Close()
os.Stdout = old

var buf bytes.Buffer
io.Copy(&buf, r)

var result statusResult
if err := json.Unmarshal(buf.Bytes(), &result); err != nil {
t.Fatalf("Expected valid JSON output, got error: %v\nOutput: %s", err, buf.String())
}
if !result.Registry.Reachable {
t.Error("Expected registry to be reachable")
}
}
2 changes: 2 additions & 0 deletions pkg/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ func init() {
rootCmd.AddCommand(prompt.PromptCmd)
rootCmd.AddCommand(configure.ConfigureCmd)
rootCmd.AddCommand(cli.VersionCmd)
rootCmd.AddCommand(cli.StatusCmd)
rootCmd.AddCommand(cli.ImportCmd)
rootCmd.AddCommand(cli.ExportCmd)
rootCmd.AddCommand(cli.EmbeddingsCmd)
Expand Down Expand Up @@ -155,6 +156,7 @@ var preRunSkipCommands = map[string]map[string]bool{
"arctl": {
"completion": true,
"configure": true,
"status": true,
"version": true,
},
"agent": {
Expand Down
Loading