package main import ( "encoding/json" "flag" "fmt" "os" "path/filepath" "strings" "gsp.toolkit/internal/gsp" ) func main() { if err := run(os.Args[1:]); err != nil { fmt.Fprintln(os.Stderr, "error:", err) os.Exit(1) } } func run(args []string) error { if len(args) == 0 { printHelp() return nil } switch args[0] { case "validate": return runValidate(args[1:]) case "index": return runIndex(args[1:]) case "trace": return runTrace(args[1:]) case "flatten": return runFlatten(args[1:]) case "pack": return runPack(args[1:]) case "graph": return runGraph(args[1:]) case "stage-check": return runStageCheck(args[1:]) case "help", "-h", "--help": printHelp() return nil default: return fmt.Errorf("unknown command %q", args[0]) } } func printHelp() { fmt.Print(`GSP Toolkit Usage: gsp validate [--root .] [--out report.json] gsp index [--root .] [--out index.json] gsp trace [--root .] [--depth 3] [--out trace.json] gsp flatten [--root .] [--depth 3] [--include-type a,b] [--exclude-type a,b] [--out flattened.json] gsp pack [--root .] [--depth 3] [--budget 12000] [--out context-pack.json] gsp graph [id] [--root .] [--depth 3] [--format json|mermaid] [--out graph.json] gsp stage-check --stage implement [--root .] [--out stage-report.json] `) } func commonRoot(fs *flag.FlagSet) *string { return fs.String("root", ".", "project root to scan") } func commonOut(fs *flag.FlagSet) *string { return fs.String("out", "", "optional output file") } func runValidate(args []string) error { fs := flag.NewFlagSet("validate", flag.ContinueOnError) root := commonRoot(fs) out := commonOut(fs) if err := fs.Parse(args); err != nil { return err } project, err := gsp.LoadProject(*root) if err != nil { return err } report := project.Validate("") return writeReport(*out, report) } func runIndex(args []string) error { fs := flag.NewFlagSet("index", flag.ContinueOnError) root := commonRoot(fs) out := commonOut(fs) if err := fs.Parse(args); err != nil { return err } project, err := gsp.LoadProject(*root) if err != nil { return err } return writeJSON(*out, project.Index()) } func runTrace(args []string) error { fs := flag.NewFlagSet("trace", flag.ContinueOnError) root := commonRoot(fs) out := commonOut(fs) depth := fs.Int("depth", 3, "maximum relation depth; -1 means unlimited") if err := fs.Parse(normalizeFlagArgs(args)); err != nil { return err } if fs.NArg() != 1 { return fmt.Errorf("trace requires one GSP id") } project, err := gsp.LoadProject(*root) if err != nil { return err } result := project.Trace(fs.Arg(0), *depth, gsp.Filter{}) return writeJSON(*out, result) } func runFlatten(args []string) error { fs := flag.NewFlagSet("flatten", flag.ContinueOnError) root := commonRoot(fs) out := commonOut(fs) depth := fs.Int("depth", 3, "maximum relation depth; -1 means unlimited") includeType := fs.String("include-type", "", "comma-separated type allow-list") excludeType := fs.String("exclude-type", "", "comma-separated type deny-list") if err := fs.Parse(normalizeFlagArgs(args)); err != nil { return err } if fs.NArg() != 1 { return fmt.Errorf("flatten requires one GSP id") } project, err := gsp.LoadProject(*root) if err != nil { return err } result := project.Flatten(fs.Arg(0), *depth, gsp.Filter{ IncludeTypes: splitCSV(*includeType), ExcludeTypes: splitCSV(*excludeType), }) return writeJSON(*out, result) } func runPack(args []string) error { fs := flag.NewFlagSet("pack", flag.ContinueOnError) root := commonRoot(fs) out := commonOut(fs) depth := fs.Int("depth", 3, "maximum relation depth; -1 means unlimited") budget := fs.Int("budget", 0, "approximate JSON character budget; 0 means unlimited") includeType := fs.String("include-type", "", "comma-separated type allow-list") excludeType := fs.String("exclude-type", "", "comma-separated type deny-list") if err := fs.Parse(normalizeFlagArgs(args)); err != nil { return err } if fs.NArg() != 1 { return fmt.Errorf("pack requires one GSP id") } project, err := gsp.LoadProject(*root) if err != nil { return err } result := project.Pack(fs.Arg(0), *depth, *budget, gsp.Filter{ IncludeTypes: splitCSV(*includeType), ExcludeTypes: splitCSV(*excludeType), }) return writeJSON(*out, result) } func runGraph(args []string) error { fs := flag.NewFlagSet("graph", flag.ContinueOnError) root := commonRoot(fs) out := commonOut(fs) depth := fs.Int("depth", 3, "maximum relation depth when id is provided; -1 means unlimited") format := fs.String("format", "json", "json or mermaid") if err := fs.Parse(normalizeFlagArgs(args)); err != nil { return err } id := "" if fs.NArg() > 1 { return fmt.Errorf("graph accepts zero or one GSP id") } if fs.NArg() == 1 { id = fs.Arg(0) } project, err := gsp.LoadProject(*root) if err != nil { return err } graph := project.Graph(id, *depth) if *format == "mermaid" { return writeText(*out, graph.Mermaid()) } if *format != "json" { return fmt.Errorf("unsupported graph format %q", *format) } return writeJSON(*out, graph) } func runStageCheck(args []string) error { fs := flag.NewFlagSet("stage-check", flag.ContinueOnError) root := commonRoot(fs) out := commonOut(fs) stage := fs.String("stage", "", "design, integrate, implement, bind, or release") if err := fs.Parse(args); err != nil { return err } if *stage == "" { return fmt.Errorf("stage-check requires --stage") } project, err := gsp.LoadProject(*root) if err != nil { return err } report := project.StageCheck(*stage) return writeReport(*out, report) } func splitCSV(value string) map[string]bool { result := map[string]bool{} for _, part := range strings.Split(value, ",") { part = strings.TrimSpace(part) if part != "" { result[part] = true } } return result } func normalizeFlagArgs(args []string) []string { valueFlags := map[string]bool{ "-root": true, "--root": true, "-out": true, "--out": true, "-depth": true, "--depth": true, "-include-type": true, "--include-type": true, "-exclude-type": true, "--exclude-type": true, "-budget": true, "--budget": true, "-format": true, "--format": true, "-stage": true, "--stage": true, } var flags []string var positionals []string for i := 0; i < len(args); i++ { arg := args[i] if !strings.HasPrefix(arg, "-") || arg == "-" { positionals = append(positionals, arg) continue } flags = append(flags, arg) name := arg if index := strings.Index(arg, "="); index >= 0 { name = arg[:index] } if valueFlags[name] && !strings.Contains(arg, "=") && i+1 < len(args) { flags = append(flags, args[i+1]) i++ } } return append(flags, positionals...) } func writeJSON(path string, value any) error { data, err := json.MarshalIndent(value, "", " ") if err != nil { return err } data = append(data, '\n') return writeBytes(path, data) } func writeReport(path string, report gsp.Report) error { if err := writeJSON(path, report); err != nil { return err } if !report.OK { return fmt.Errorf("GSP check failed") } return nil } func writeText(path string, value string) error { return writeBytes(path, []byte(value)) } func writeBytes(path string, data []byte) error { if path == "" { _, err := os.Stdout.Write(data) return err } if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return err } return os.WriteFile(path, data, 0644) }