// Package dispatch is the Crucible command surface: a single registry of // builders shared by the `crucible` dispatcher and the standalone // `crucible-` shims. // // Scaffold contract (Phase 5): every builder is registered but UNWIRED. Running // a builder fails closed (exit 70) with an operator-cutover message; it never // fakes an artifact. The internal pipeline/topdata/erf/wiki/music packages from // gitea/sow-tools are migrated by the operator at cutover (the workspace hard // rules forbid transplanting source here). Once a builder's handler is wired, // flip its Wired flag and register the handler. package dispatch import ( "fmt" "io" "os" "text/tabwriter" "git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/buildinfo" ) // Exit codes follow the sysexits(3) convention so CI can distinguish // "you called it wrong" from "the tool is not wired yet". const ( exitOK = 0 exitUsage = 64 // EX_USAGE — bad invocation / unknown builder exitUnwired = 70 // EX_SOFTWARE — builder registered but not yet implemented ) // Builder is one tool surface in the Crucible suite. type Builder struct { Name string // canonical dispatcher subcommand, e.g. "module" Bin string // standalone binary name, e.g. "crucible-module" Summary string // one-line description Legacy []string // nwn-tool commands this subsumes (migration aid; see docs/command-surface.md) } // Registry is the single source of truth for the Crucible command surface. // Keep it in sync with docs/command-surface.md and the cmd/ directory. var Registry = []Builder{ { Name: "depot", Bin: "crucible-depot", Summary: "verify/move content-addressed depot blobs (SeaweedFS)", Legacy: nil, }, { Name: "hak", Bin: "crucible-hak", Summary: "pack/unpack ERF/HAK archives + hak manifests", Legacy: []string{"build-haks", "apply-hak-manifest"}, }, { Name: "module", Bin: "crucible-module", Summary: "build/extract/validate/compare the .mod", Legacy: []string{"build", "build-module", "extract", "validate", "compare"}, }, { Name: "topdata", Bin: "crucible-topdata", Summary: "compile 2da/tlk topdata + packages", Legacy: []string{"validate-topdata", "build-topdata", "build-top-package", "compare-topdata", "convert-topdata"}, }, { Name: "wiki", Bin: "crucible-wiki", Summary: "render + deploy mechanical wiki pages", Legacy: []string{"build-wiki", "deploy-wiki"}, }, } func find(name string) (Builder, bool) { for _, b := range Registry { if b.Name == name { return b, true } } return Builder{}, false } // Main is the `crucible` dispatcher entrypoint. func Main(args []string) int { return run(args, os.Stdout, os.Stderr) } // RunBuilder is the standalone `crucible-` entrypoint. func RunBuilder(name string, args []string) int { return runBuilder(name, args, os.Stdout, os.Stderr) } func run(args []string, out, errw io.Writer) int { if len(args) == 0 { usage(out) return exitUsage } switch args[0] { case "-h", "--help", "help": usage(out) return exitOK case "-V", "--version", "version": fmt.Fprintln(out, buildinfo.String()) return exitOK case "list": list(out) return exitOK } return runBuilder(args[0], args[1:], out, errw) } func runBuilder(name string, args []string, out, errw io.Writer) int { b, ok := find(name) if !ok { fmt.Fprintf(errw, "crucible: unknown builder %q\n\n", name) usage(errw) return exitUsage } if len(args) > 0 { switch args[0] { case "-h", "--help", "help": builderHelp(out, b) return exitOK } } // Every builder is unwired in the Phase 5 scaffold: fail closed, never fake. fmt.Fprintf(errw, unwiredMsg, b.Name, b.Bin) return exitUnwired } const unwiredMsg = `crucible %[1]s: not wired yet. The Crucible suite is scaffolded (Phase 5). The %[2]s implementation is migrated from gitea/sow-tools internal packages at operator cutover; the workspace hard rules forbid transplanting that source into this tree automatically. This command fails closed (exit 70) rather than producing a fake artifact. See docs/migration-from-nwn-tool.md. ` func usage(w io.Writer) { fmt.Fprintf(w, "crucible — Shadows Over Westgate build/conversion/sync toolchain\n") fmt.Fprintf(w, "%s\n\n", buildinfo.String()) fmt.Fprintf(w, "usage:\n") fmt.Fprintf(w, " crucible [args] dispatch to a builder\n") fmt.Fprintf(w, " crucible- [args] equivalent standalone binary\n\n") fmt.Fprintf(w, "builders:\n") list(w) fmt.Fprintf(w, "\nglobal commands:\n") fmt.Fprintf(w, " help | -h show this help\n") fmt.Fprintf(w, " version | -V show the build version\n") fmt.Fprintf(w, " list list builders (one per line: namebinsummary)\n") fmt.Fprintf(w, "\nNWN_ROOT must be passed explicitly (env or flag); crucible never guesses\n") fmt.Fprintf(w, "from $HOME. See docs/consumer-contract.md.\n") } func list(w io.Writer) { tw := tabwriter.NewWriter(w, 0, 2, 2, ' ', 0) for _, b := range Registry { fmt.Fprintf(tw, " %s\t%s\t%s\n", b.Name, b.Bin, b.Summary) } tw.Flush() } func builderHelp(w io.Writer, b Builder) { fmt.Fprintf(w, "crucible %s (%s) — %s\n\n", b.Name, b.Bin, b.Summary) if len(b.Legacy) > 0 { fmt.Fprintf(w, "subsumes nwn-tool commands: ") for i, c := range b.Legacy { if i > 0 { fmt.Fprintf(w, ", ") } fmt.Fprintf(w, "%s", c) } fmt.Fprintf(w, "\n\n") } fmt.Fprintf(w, "status: scaffolded, not wired (Phase 5). See docs/command-surface.md.\n") }