chore: import upstream snapshot with attribution
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/zzet/gortex/internal/daemon"
|
||||
)
|
||||
|
||||
var (
|
||||
queryIndex string
|
||||
queryDepth int
|
||||
queryFormat string
|
||||
queryLimit int
|
||||
)
|
||||
|
||||
var queryCmd = &cobra.Command{
|
||||
Use: "query",
|
||||
Short: "Query the knowledge graph",
|
||||
}
|
||||
|
||||
func init() {
|
||||
queryCmd.PersistentFlags().StringVar(&queryIndex, "index", ".", "repository path the daemon must track")
|
||||
queryCmd.PersistentFlags().IntVar(&queryDepth, "depth", 3, "traversal depth")
|
||||
queryCmd.PersistentFlags().StringVar(&queryFormat, "format", "text", "output format: text|json (traversal queries also support dot|mermaid)")
|
||||
queryCmd.PersistentFlags().IntVar(&queryLimit, "limit", 50, "max nodes in result")
|
||||
|
||||
queryCmd.AddCommand(querySymbolCmd)
|
||||
queryCmd.AddCommand(queryDepsCmd)
|
||||
queryCmd.AddCommand(queryDependentsCmd)
|
||||
queryCmd.AddCommand(queryCallersCmd)
|
||||
queryCmd.AddCommand(queryCallsCmd)
|
||||
queryCmd.AddCommand(queryImplementationsCmd)
|
||||
queryCmd.AddCommand(queryUsagesCmd)
|
||||
queryCmd.AddCommand(queryStatsCmd)
|
||||
|
||||
rootCmd.AddCommand(queryCmd)
|
||||
}
|
||||
|
||||
// requireDaemonTool runs a graph tool against the daemon that owns the
|
||||
// repo. The daemon is the single graph owner: when none is running, or it
|
||||
// does not track the repo, this returns an actionable error rather than
|
||||
// silently building a second-class in-process index.
|
||||
func requireDaemonTool(repoPath, tool string, args map[string]any) (json.RawMessage, error) {
|
||||
exec, err := resolveExecutor(repoPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNoExecutor) {
|
||||
return nil, daemonRequiredErr(repoPath)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer exec.Close()
|
||||
out, err := exec.CallTool(context.Background(), tool, args)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrRepoNotTracked) {
|
||||
return nil, daemonRequiredErr(repoPath)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// daemonRequiredErr explains how to make the daemon able to answer: start
|
||||
// it (when none runs) or track the repo (when it runs but does not own it).
|
||||
func daemonRequiredErr(repoPath string) error {
|
||||
abs, aerr := filepath.Abs(repoPath)
|
||||
if aerr != nil {
|
||||
abs = repoPath
|
||||
}
|
||||
if !daemon.IsRunning() {
|
||||
return fmt.Errorf("no gortex daemon is running — start it with `gortex daemon start --detach`, then track this repo with `gortex track %s`", abs)
|
||||
}
|
||||
return fmt.Errorf("the gortex daemon does not track %s — add it with `gortex track %s`", abs, abs)
|
||||
}
|
||||
|
||||
// emitDaemonJSON re-indents and prints a daemon tool result for the
|
||||
// --format json path (and as the fallback for unrecognised shapes).
|
||||
func emitDaemonJSON(cmd *cobra.Command, raw json.RawMessage) error {
|
||||
var v any
|
||||
if err := json.Unmarshal(raw, &v); err != nil {
|
||||
fmt.Fprintln(cmd.OutOrStdout(), string(raw))
|
||||
return nil
|
||||
}
|
||||
enc := json.NewEncoder(cmd.OutOrStdout())
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(v)
|
||||
}
|
||||
|
||||
func printDaemonSearchSymbols(cmd *cobra.Command, raw json.RawMessage) error {
|
||||
if queryFormat == "json" {
|
||||
return emitDaemonJSON(cmd, raw)
|
||||
}
|
||||
var payload struct {
|
||||
Results []struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
FilePath string `json:"file_path"`
|
||||
Line int `json:"start_line"`
|
||||
} `json:"results"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return emitDaemonJSON(cmd, raw)
|
||||
}
|
||||
for _, r := range payload.Results {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%-12s %-40s %s:%d\n", r.Kind, r.ID, r.FilePath, r.Line)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// printDaemonSubgraph renders the node-list shape shared by the graph
|
||||
// traversal tools (get_dependencies / dependents / callers / call_chain /
|
||||
// find_usages / find_implementations). Falls back to pretty JSON when the
|
||||
// payload is not the expected shape.
|
||||
func printDaemonSubgraph(cmd *cobra.Command, raw json.RawMessage) error {
|
||||
if queryFormat == "json" {
|
||||
return emitDaemonJSON(cmd, raw)
|
||||
}
|
||||
var payload struct {
|
||||
Nodes []struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
FilePath string `json:"file_path"`
|
||||
Line int `json:"line"`
|
||||
StartLine int `json:"start_line"`
|
||||
} `json:"nodes"`
|
||||
Truncated bool `json:"truncated"`
|
||||
TotalNodes int `json:"total_nodes"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &payload); err != nil || payload.Nodes == nil {
|
||||
return emitDaemonJSON(cmd, raw)
|
||||
}
|
||||
for _, n := range payload.Nodes {
|
||||
line := n.Line
|
||||
if line == 0 {
|
||||
line = n.StartLine
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%-12s %-40s %s:%d\n", n.Kind, n.ID, n.FilePath, line)
|
||||
}
|
||||
if payload.Truncated {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "... truncated (%d total)\n", payload.TotalNodes)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// emitSubgraph runs a graph traversal tool and renders it per --format.
|
||||
// text/json go through the node-list renderer; dot/mermaid ask the daemon
|
||||
// for the diagram directly (its query.SubGraph already knows how to draw
|
||||
// itself) and print it verbatim.
|
||||
func emitSubgraph(cmd *cobra.Command, repoPath, tool string, args map[string]any) error {
|
||||
diagram := queryFormat == "dot" || queryFormat == "mermaid"
|
||||
if diagram {
|
||||
args["format"] = queryFormat
|
||||
}
|
||||
out, err := requireDaemonTool(repoPath, tool, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if diagram {
|
||||
fmt.Fprintln(cmd.OutOrStdout(), string(out))
|
||||
return nil
|
||||
}
|
||||
return printDaemonSubgraph(cmd, out)
|
||||
}
|
||||
|
||||
func printDaemonStats(cmd *cobra.Command, raw json.RawMessage) error {
|
||||
if queryFormat == "json" {
|
||||
return emitDaemonJSON(cmd, raw)
|
||||
}
|
||||
var payload struct {
|
||||
TotalNodes int `json:"total_nodes"`
|
||||
TotalEdges int `json:"total_edges"`
|
||||
ByKind map[string]int `json:"by_kind"`
|
||||
ByLanguage map[string]int `json:"by_language"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return emitDaemonJSON(cmd, raw)
|
||||
}
|
||||
out := cmd.OutOrStdout()
|
||||
fmt.Fprintf(out, "Nodes: %d Edges: %d\n", payload.TotalNodes, payload.TotalEdges)
|
||||
if len(payload.ByKind) > 0 {
|
||||
fmt.Fprintln(out, "By kind:")
|
||||
for k, v := range payload.ByKind {
|
||||
fmt.Fprintf(out, " %-12s %d\n", k, v)
|
||||
}
|
||||
}
|
||||
if len(payload.ByLanguage) > 0 {
|
||||
fmt.Fprintln(out, "By language:")
|
||||
for k, v := range payload.ByLanguage {
|
||||
fmt.Fprintf(out, " %-12s %d\n", k, v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var querySymbolCmd = &cobra.Command{
|
||||
Use: "symbol <name>",
|
||||
Short: "Find symbols matching name",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
out, err := requireDaemonTool(queryIndex, "search_symbols",
|
||||
map[string]any{"query": args[0], "limit": queryLimit})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printDaemonSearchSymbols(cmd, out)
|
||||
},
|
||||
}
|
||||
|
||||
var queryDepsCmd = &cobra.Command{
|
||||
Use: "deps <id>",
|
||||
Short: "Show dependencies of a symbol",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return emitSubgraph(cmd, queryIndex, "get_dependencies",
|
||||
map[string]any{"id": args[0], "depth": queryDepth, "limit": queryLimit})
|
||||
},
|
||||
}
|
||||
|
||||
var queryDependentsCmd = &cobra.Command{
|
||||
Use: "dependents <id>",
|
||||
Short: "Show blast radius for a symbol",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return emitSubgraph(cmd, queryIndex, "get_dependents",
|
||||
map[string]any{"id": args[0], "depth": queryDepth, "limit": queryLimit})
|
||||
},
|
||||
}
|
||||
|
||||
var queryCallersCmd = &cobra.Command{
|
||||
Use: "callers <func-id>",
|
||||
Short: "Show who calls a function",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return emitSubgraph(cmd, queryIndex, "get_callers",
|
||||
map[string]any{"id": args[0], "depth": queryDepth, "limit": queryLimit})
|
||||
},
|
||||
}
|
||||
|
||||
var queryCallsCmd = &cobra.Command{
|
||||
Use: "calls <func-id>",
|
||||
Short: "Show what a function calls",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return emitSubgraph(cmd, queryIndex, "get_call_chain",
|
||||
map[string]any{"id": args[0], "depth": queryDepth, "limit": queryLimit})
|
||||
},
|
||||
}
|
||||
|
||||
var queryImplementationsCmd = &cobra.Command{
|
||||
Use: "implementations <interface-id>",
|
||||
Short: "Show implementations of an interface",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return emitSubgraph(cmd, queryIndex, "find_implementations",
|
||||
map[string]any{"id": args[0]})
|
||||
},
|
||||
}
|
||||
|
||||
var queryUsagesCmd = &cobra.Command{
|
||||
Use: "usages <id>",
|
||||
Short: "Show all usages of a symbol",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return emitSubgraph(cmd, queryIndex, "find_usages",
|
||||
map[string]any{"id": args[0], "limit": queryLimit})
|
||||
},
|
||||
}
|
||||
|
||||
var queryStatsCmd = &cobra.Command{
|
||||
Use: "stats",
|
||||
Short: "Show graph statistics",
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
out, err := requireDaemonTool(queryIndex, "graph_stats", map[string]any{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printDaemonStats(cmd, out)
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user