diff --git a/internal/cli/status.go b/internal/cli/status.go new file mode 100644 index 00000000..24c7c3fc --- /dev/null +++ b/internal/cli/status.go @@ -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() +} diff --git a/internal/cli/status_test.go b/internal/cli/status_test.go new file mode 100644 index 00000000..97e2417b --- /dev/null +++ b/internal/cli/status_test.go @@ -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") + } +} diff --git a/pkg/cli/root.go b/pkg/cli/root.go index 2a2fd581..18275c12 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -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) @@ -155,6 +156,7 @@ var preRunSkipCommands = map[string]map[string]bool{ "arctl": { "completion": true, "configure": true, + "status": true, "version": true, }, "agent": {