Files
GSP/toolkit/cmd/gsp/main.go

401 lines
10 KiB
Go

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 "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 init [path] [--name project-name] [--entry project.entry] [--force]
gsp ai-init [--root .] [--agents] [--skill generic|codex] [--all] [--force]
gsp version [--json]
gsp validate [--root .] [--out report.json]
gsp index [--root .] [--out index.json]
gsp trace <id> [--root .] [--depth 3] [--out trace.json]
gsp flatten <id> [--root .] [--depth 3] [--include-type a,b] [--exclude-type a,b] [--out flattened.json]
gsp pack <id> [--root .] [--depth 3] [--budget 12000] [--out context-pack.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 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")
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, 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,
"-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)
}