package app import ( "errors" "fmt" "io" "os" "path/filepath" "strings" "gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/pipeline" "gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project" "gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/validator" ) type command struct { name string description string run func(context) error } type context struct { stdout io.Writer stderr io.Writer cwd string } var commands = []command{ { name: "build", description: "Build module and HAK outputs into build/.", run: runBuild, }, { name: "build-module", description: "Build only the module archive from src/ into build/.", run: runBuildModule, }, { name: "build-haks", description: "Build only HAK archives and manifest from assets/ into build/.", run: runBuildHAKs, }, { name: "extract", description: "Extract a module archive back into the src/ tree.", run: runExtract, }, { name: "validate", description: "Validate project layout, config, and source inventory.", run: runValidate, }, { name: "compare", description: "Compare current source content against the built archives.", run: runCompare, }, { name: "apply-hak-manifest", description: "Apply a generated HAK manifest to src/module/module.ifo.json.", run: runApplyHAKManifest, }, } func Run(args []string) (int, error) { ctx, err := newContext() if err != nil { return 1, err } if len(args) == 0 { printUsage(ctx.stdout) return 0, nil } switch args[0] { case "-h", "--help", "help": printUsage(ctx.stdout) return 0, nil default: for _, cmd := range commands { if cmd.name == args[0] { if err := cmd.run(ctx); err != nil { return 1, err } return 0, nil } } } return 1, fmt.Errorf("unknown command %q", args[0]) } func newContext() (context, error) { cwd, err := os.Getwd() if err != nil { return context{}, fmt.Errorf("resolve working directory: %w", err) } return context{ stdout: os.Stdout, stderr: os.Stderr, cwd: cwd, }, nil } func runBuild(ctx context) error { p, err := loadProject(ctx.cwd) if err != nil { return err } result, err := pipeline.Build(p) if err != nil { return err } fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name) fmt.Fprintf(ctx.stdout, "module: %s\n", result.ModulePath) fmt.Fprintf(ctx.stdout, "resources: %d\n", result.Resources) if len(result.HAKPaths) > 0 { fmt.Fprintf(ctx.stdout, "hak archives: %d\n", len(result.HAKPaths)) fmt.Fprintf(ctx.stdout, "hak assets: %d\n", result.HAKAssets) fmt.Fprintf(ctx.stdout, "hak manifest: %s\n", result.Manifest) } return nil } func runBuildModule(ctx context) error { p, err := loadProject(ctx.cwd) if err != nil { return err } result, err := pipeline.BuildModule(p) if err != nil { return err } fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name) fmt.Fprintf(ctx.stdout, "module: %s\n", result.ModulePath) fmt.Fprintf(ctx.stdout, "resources: %d\n", result.Resources) return nil } func runBuildHAKs(ctx context) error { p, err := loadProject(ctx.cwd) if err != nil { return err } result, err := pipeline.BuildHAKs(p) if err != nil { return err } fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name) fmt.Fprintf(ctx.stdout, "hak archives: %d\n", len(result.HAKPaths)) fmt.Fprintf(ctx.stdout, "hak assets: %d\n", result.HAKAssets) if result.Manifest != "" { fmt.Fprintf(ctx.stdout, "hak manifest: %s\n", result.Manifest) } return nil } func runExtract(ctx context) error { p, err := loadProject(ctx.cwd) if err != nil { return err } result, err := pipeline.Extract(p) if err != nil { return err } fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name) fmt.Fprintf(ctx.stdout, "module: %s\n", result.ModulePath) if len(result.HAKPaths) > 0 { fmt.Fprintf(ctx.stdout, "hak archives: %d\n", len(result.HAKPaths)) } fmt.Fprintf(ctx.stdout, "written: %d\n", result.Written) fmt.Fprintf(ctx.stdout, "overwritten: %d\n", result.Overwritten) fmt.Fprintf(ctx.stdout, "removed: %d\n", result.Removed) fmt.Fprintf(ctx.stdout, "skipped: %d\n", result.Skipped) return nil } func runValidate(ctx context) error { p, err := loadProject(ctx.cwd) if err != nil { return err } validationReport := validator.ValidateProject(p) if validationReport.HasErrors() { for _, diagnostic := range validationReport.Diagnostics { if diagnostic.Severity == validator.SeverityWarning { fprintfTarget := ctx.stderr fmt.Fprintf(fprintfTarget, "warning: %s: %s\n", diagnostic.Path, diagnostic.Message) continue } fmt.Fprintf(ctx.stderr, "error: %s: %s\n", diagnostic.Path, diagnostic.Message) } return fmt.Errorf("validation failed with %d error(s)", validationReport.ErrorCount()) } for _, diagnostic := range validationReport.Diagnostics { if diagnostic.Severity == validator.SeverityWarning { fmt.Fprintf(ctx.stderr, "warning: %s: %s\n", diagnostic.Path, diagnostic.Message) } } inventoryReport := p.Inventory.Report() fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name) fmt.Fprintf(ctx.stdout, "root: %s\n", p.Root) fmt.Fprintf(ctx.stdout, "source files: %d\n", inventoryReport.SourceFiles) fmt.Fprintf(ctx.stdout, "script files: %d\n", inventoryReport.ScriptFiles) fmt.Fprintf(ctx.stdout, "asset files: %d\n", inventoryReport.AssetFiles) fmt.Fprintf(ctx.stdout, "module file types: %s\n", strings.Join(inventoryReport.Extensions, ", ")) if warnings := validationReport.WarningCount(); warnings > 0 { fmt.Fprintf(ctx.stdout, "warnings: %d\n", warnings) } fmt.Fprintf(ctx.stdout, "validation: ok\n") return nil } func runCompare(ctx context) error { p, err := loadProject(ctx.cwd) if err != nil { return err } result, err := pipeline.Compare(p) if err != nil { return err } fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name) fmt.Fprintf(ctx.stdout, "module: %s\n", result.ModulePath) if len(result.HAKPaths) > 0 { fmt.Fprintf(ctx.stdout, "hak archives: %d\n", len(result.HAKPaths)) } fmt.Fprintf(ctx.stdout, "checked resources: %d\n", result.Checked) fmt.Fprintf(ctx.stdout, "compare: ok\n") return nil } func runApplyHAKManifest(ctx context) error { p, err := loadProject(ctx.cwd) if err != nil { return err } manifestPath := filepath.Join(p.BuildDir(), "haks.json") if len(os.Args) > 2 { manifestPath = os.Args[2] if !filepath.IsAbs(manifestPath) { manifestPath = filepath.Join(ctx.cwd, manifestPath) } } result, err := pipeline.ApplyHAKManifest(p, manifestPath) if err != nil { return err } fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name) fmt.Fprintf(ctx.stdout, "manifest: %s\n", result.ManifestPath) fmt.Fprintf(ctx.stdout, "module source: %s\n", result.ModuleSource) fmt.Fprintf(ctx.stdout, "hak entries: %d\n", result.HAKCount) return nil } func loadProject(cwd string) (*project.Project, error) { root, err := project.FindRoot(cwd) if err != nil { return nil, err } p, err := project.Load(root) if err != nil { return nil, err } var failures []error if err := p.ValidateLayout(); err != nil { failures = append(failures, err) } if err := p.Scan(); err != nil { failures = append(failures, err) } if len(failures) > 0 { return nil, errors.Join(failures...) } return p, nil } func printUsage(w io.Writer) { exe := filepath.Base(os.Args[0]) fmt.Fprintf(w, "%s \n\n", exe) fmt.Fprintln(w, "Commands:") for _, cmd := range commands { fmt.Fprintf(w, " %-8s %s\n", cmd.name, cmd.description) } }