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 } if len(args) == 1 && (args[0] == "-v" || args[0] == "--version") { printVersion(false) return nil } switch args[0] { case "init": return runInit(args[1:]) case "ai-init": return runAIInit(args[1:]) case "version": return runVersion(args[1:]) case "completion": return runCompletion(args[1:]) 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 "links": return runLinks(args[1:]) case "impact": return runImpact(args[1:]) case "message": return runMessage(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 init [path] [--name project-name] [--entry project.entry] [--force] gsp ai-init [--root .] [--agents] [--skill generic|codex] [--all] [--force] gsp version [--json] gsp completion powershell|bash|zsh|fish gsp completion install powershell 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 .] [--for implement] [--stage implement] [--depth 3] [--budget 12000] [--format json|md|canvas] [--out context-pack.json] gsp links [--root .] [--depth -1] [--format json|md] [--out links.json] gsp impact [--root .] [--depth -1] [--format json|md|canvas] [--out impact.json] gsp message validate [--root .] [--out message-report.json] gsp graph [id] [--root .] [--depth 3] [--format json|mermaid|md|canvas] [--out graph.json] gsp stage-check --stage implement [--root .] [--out stage-report.json] `) } func runLinks(args []string) error { fs := flag.NewFlagSet("links", flag.ContinueOnError) root := commonRoot(fs) out := commonOut(fs) depth := fs.Int("depth", -1, "maximum relation depth; -1 means unlimited") format := fs.String("format", "json", "json or md") if err := fs.Parse(normalizeFlagArgs(args)); err != nil { return err } if fs.NArg() != 1 { return fmt.Errorf("links requires one GSP id") } project, err := gsp.LoadProject(*root) if err != nil { return err } result := project.Links(fs.Arg(0), *depth) switch *format { case "json": return writeJSON(*out, result) case "md": return writeText(*out, result.Markdown()) default: return fmt.Errorf("unsupported links format %q", *format) } } func runCompletion(args []string) error { if len(args) == 0 { return fmt.Errorf("completion requires powershell, bash, zsh, fish, or install powershell") } if args[0] == "install" { if len(args) != 2 { return fmt.Errorf("completion install requires one shell") } if args[1] != "powershell" { return fmt.Errorf("completion install currently supports powershell") } return installPowerShellCompletion() } if len(args) != 1 { return fmt.Errorf("completion accepts one shell") } switch args[0] { case "powershell": return writeText("", powerShellCompletionScript()) case "bash": return writeText("", bashCompletionScript()) case "zsh": return writeText("", zshCompletionScript()) case "fish": return writeText("", fishCompletionScript()) default: return fmt.Errorf("unsupported completion shell %q", args[0]) } } func installPowerShellCompletion() error { home, err := os.UserHomeDir() if err != nil { return err } completionDir := filepath.Join(home, ".gsp", "completion") completionFile := filepath.Join(completionDir, "gsp.ps1") if err := os.MkdirAll(completionDir, 0755); err != nil { return err } if err := os.WriteFile(completionFile, []byte(powerShellCompletionScript()), 0644); err != nil { return err } profilePath := filepath.Join(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1") if err := os.MkdirAll(filepath.Dir(profilePath), 0755); err != nil { return err } sourceLine := fmt.Sprintf(". '%s'", strings.ReplaceAll(completionFile, "'", "''")) existing, err := os.ReadFile(profilePath) if err != nil && !os.IsNotExist(err) { return err } if !strings.Contains(string(existing), sourceLine) { var builder strings.Builder if len(existing) > 0 { builder.Write(existing) if !strings.HasSuffix(string(existing), "\n") { builder.WriteString("\n") } } builder.WriteString(sourceLine) builder.WriteString("\n") if err := os.WriteFile(profilePath, []byte(builder.String()), 0644); err != nil { return err } } fmt.Printf("Installed PowerShell completion to %s\n", completionFile) fmt.Printf("Updated PowerShell profile %s\n", profilePath) fmt.Println("Open a new PowerShell terminal or run the profile file to enable completion.") return nil } func runAIInit(args []string) error { fs := flag.NewFlagSet("ai-init", flag.ContinueOnError) root := commonRoot(fs) agents := fs.Bool("agents", false, "generate AGENTS.md") skill := fs.String("skill", "", "generate a skill adapter: generic or codex") all := fs.Bool("all", false, "generate README.md, AI_USAGE.md, AGENTS.md, and generic skill") force := fs.Bool("force", false, "overwrite generated AI files when they already exist") if err := fs.Parse(args); err != nil { return err } if fs.NArg() != 0 { return fmt.Errorf("ai-init does not accept positional arguments; use --root") } result, err := gsp.InitAIUsage(*root, gsp.AIInitOptions{ Agents: *agents, Skill: *skill, All: *all, Force: *force, }) if err != nil { return err } return writeJSON("", result) } func runInit(args []string) error { fs := flag.NewFlagSet("init", flag.ContinueOnError) name := fs.String("name", "", "project name") entry := fs.String("entry", "project.entry", "entry GSP id") force := fs.Bool("force", false, "overwrite generated init files when they already exist") if err := fs.Parse(normalizeFlagArgs(args)); err != nil { return err } if fs.NArg() > 1 { return fmt.Errorf("init accepts zero or one path") } root := "." if fs.NArg() == 1 { root = fs.Arg(0) } result, err := gsp.InitProject(root, gsp.InitOptions{ Name: *name, Entry: *entry, Force: *force, GSPVersion: gsp.DefaultGSPVersion, ToolkitVersion: gsp.ToolkitVersion, DefaultOutputFolder: ".gsp", }) if err != nil { return err } return writeJSON("", result) } func runVersion(args []string) error { fs := flag.NewFlagSet("version", flag.ContinueOnError) asJSON := fs.Bool("json", false, "print version as JSON") if err := fs.Parse(args); err != nil { return err } printVersion(*asJSON) return nil } func printVersion(asJSON bool) { value := struct { ToolkitVersion string `json:"toolkitVersion"` DefaultGSPVersion string `json:"defaultGspVersion"` SupportedGSPVersion []string `json:"supportedGspVersions"` }{ ToolkitVersion: gsp.ToolkitVersion, DefaultGSPVersion: gsp.DefaultGSPVersion, SupportedGSPVersion: gsp.SupportedGSPVersions, } if asJSON { data, _ := json.MarshalIndent(value, "", " ") fmt.Println(string(data)) return } fmt.Printf("GSP Toolkit %s\nDefault GSP %s\nSupported GSP %s\n", gsp.ToolkitVersion, gsp.DefaultGSPVersion, strings.Join(gsp.SupportedGSPVersions, ", ")) } 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") intent := fs.String("for", "", "context purpose: design, implement, review, test, acceptance, handoff, or inspect") stage := fs.String("stage", "", "optional project stage") format := fs.String("format", "json", "json, md, or canvas") 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.PackFor(fs.Arg(0), *intent, *stage, *depth, *budget, gsp.Filter{ IncludeTypes: splitCSV(*includeType), ExcludeTypes: splitCSV(*excludeType), }) switch *format { case "json": return writeJSON(*out, result) case "md": return writeText(*out, result.Markdown()) case "canvas": data, err := project.Graph(fs.Arg(0), *depth).Canvas() if err != nil { return err } return writeBytes(*out, data) default: return fmt.Errorf("unsupported pack format %q", *format) } } func runImpact(args []string) error { fs := flag.NewFlagSet("impact", flag.ContinueOnError) root := commonRoot(fs) out := commonOut(fs) depth := fs.Int("depth", -1, "maximum reverse relation depth; -1 means unlimited") format := fs.String("format", "json", "json, md, or canvas") if err := fs.Parse(normalizeFlagArgs(args)); err != nil { return err } if fs.NArg() != 1 { return fmt.Errorf("impact requires one GSP id") } project, err := gsp.LoadProject(*root) if err != nil { return err } result := project.Impact(fs.Arg(0), *depth) switch *format { case "json": return writeJSON(*out, result) case "md": return writeText(*out, result.Markdown()) case "canvas": data, err := project.ImpactGraph(result).Canvas() if err != nil { return err } return writeBytes(*out, data) default: return fmt.Errorf("unsupported impact format %q", *format) } } func runMessage(args []string) error { if len(args) == 0 { return fmt.Errorf("message requires validate") } switch args[0] { case "validate": return runMessageValidate(args[1:]) default: return fmt.Errorf("unknown message command %q", args[0]) } } func runMessageValidate(args []string) error { fs := flag.NewFlagSet("message validate", flag.ContinueOnError) root := commonRoot(fs) out := commonOut(fs) if err := fs.Parse(normalizeFlagArgs(args)); err != nil { return err } if fs.NArg() != 1 { return fmt.Errorf("message validate requires one file") } project, err := gsp.LoadProject(*root) if err != nil { return err } message, err := gsp.ReadMessage(project.Root, fs.Arg(0)) if err != nil { return err } report := project.ValidateMessage(message) return writeReport(*out, report) } 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, mermaid, md, or canvas") 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) switch *format { case "mermaid": return writeText(*out, graph.Mermaid()) case "md": return writeText(*out, graph.Markdown()) case "canvas": data, err := graph.Canvas() if err != nil { return err } return writeBytes(*out, data) case "json": return writeJSON(*out, graph) default: return fmt.Errorf("unsupported graph format %q", *format) } } 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, "-for": true, "--for": true, "-name": true, "--name": true, "-entry": true, "--entry": 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) }