claude: fold in old sow-tools
This commit is contained in:
@@ -2,12 +2,11 @@
|
||||
// builders shared by the `crucible` dispatcher and the standalone
|
||||
// `crucible-<name>` 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.
|
||||
// Cutover state (Phase 5 → 6): the internal pipeline/topdata/erf/wiki/music
|
||||
// packages migrated from gitea/sow-tools now live in this tree, so wired
|
||||
// builders delegate to internal/app's legacy command surface (mapped per
|
||||
// docs/command-surface.md). A builder with no migrated logic yet (depot) keeps
|
||||
// the Wired=false fail-closed path: exit 70, never a faked artifact.
|
||||
package dispatch
|
||||
|
||||
import (
|
||||
@@ -16,6 +15,7 @@ import (
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/app"
|
||||
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/buildinfo"
|
||||
)
|
||||
|
||||
@@ -33,6 +33,21 @@ type Builder struct {
|
||||
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)
|
||||
Extra []string // additional callable subcommands beyond Legacy (e.g. cross-listed "music")
|
||||
Wired bool // true once the builder delegates to migrated internal/app logic
|
||||
}
|
||||
|
||||
// subcommands returns every legacy command a wired builder accepts: the
|
||||
// canonical Legacy set plus any cross-listed Extra commands.
|
||||
func (b Builder) subcommands() []string { return append(append([]string{}, b.Legacy...), b.Extra...) }
|
||||
|
||||
func (b Builder) accepts(sub string) bool {
|
||||
for _, s := range b.subcommands() {
|
||||
if s == sub {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Registry is the single source of truth for the Crucible command surface.
|
||||
@@ -43,30 +58,39 @@ var Registry = []Builder{
|
||||
Bin: "crucible-depot",
|
||||
Summary: "verify/move content-addressed depot blobs (SeaweedFS)",
|
||||
Legacy: nil,
|
||||
Wired: false, // no migrated logic yet; fails closed (exit 70)
|
||||
},
|
||||
{
|
||||
Name: "hak",
|
||||
Bin: "crucible-hak",
|
||||
Summary: "pack/unpack ERF/HAK archives + hak manifests",
|
||||
Legacy: []string{"build-haks", "apply-hak-manifest"},
|
||||
Extra: []string{"music"}, // music BMUs are packed into HAKs (command-surface.md)
|
||||
Wired: true,
|
||||
},
|
||||
{
|
||||
Name: "module",
|
||||
Bin: "crucible-module",
|
||||
Summary: "build/extract/validate/compare the .mod",
|
||||
Legacy: []string{"build", "build-module", "extract", "validate", "compare"},
|
||||
// apply-hak-manifest is canonically a hak command but reachable here too;
|
||||
// music is folded into the module build pipeline (command-surface.md).
|
||||
Extra: []string{"apply-hak-manifest", "music"},
|
||||
Wired: true,
|
||||
},
|
||||
{
|
||||
Name: "topdata",
|
||||
Bin: "crucible-topdata",
|
||||
Summary: "compile 2da/tlk topdata + packages",
|
||||
Legacy: []string{"validate-topdata", "build-topdata", "build-top-package", "compare-topdata", "convert-topdata"},
|
||||
Wired: true,
|
||||
},
|
||||
{
|
||||
Name: "wiki",
|
||||
Bin: "crucible-wiki",
|
||||
Summary: "render + deploy mechanical wiki pages",
|
||||
Legacy: []string{"build-wiki", "deploy-wiki"},
|
||||
Wired: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -102,6 +126,13 @@ func run(args []string, out, errw io.Writer) int {
|
||||
case "list":
|
||||
list(out)
|
||||
return exitOK
|
||||
case "config":
|
||||
// Global concern shared by every builder (command-surface.md): the legacy
|
||||
// `config` command group, surfaced verbatim.
|
||||
return delegateLegacy(args, errw)
|
||||
case "changelog":
|
||||
// Release tooling: global alias for the legacy `build-changelog` command.
|
||||
return delegateLegacy(append([]string{"build-changelog"}, args[1:]...), errw)
|
||||
}
|
||||
return runBuilder(args[0], args[1:], out, errw)
|
||||
}
|
||||
@@ -120,19 +151,43 @@ func runBuilder(name string, args []string, out, errw io.Writer) int {
|
||||
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
|
||||
if !b.Wired {
|
||||
// No migrated logic yet (depot): fail closed, never fake an artifact.
|
||||
fmt.Fprintf(errw, unwiredMsg, b.Name, b.Bin)
|
||||
return exitUnwired
|
||||
}
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(errw, "crucible %s: a subcommand is required\n\n", b.Name)
|
||||
builderHelp(errw, b)
|
||||
return exitUsage
|
||||
}
|
||||
sub := args[0]
|
||||
if !b.accepts(sub) {
|
||||
fmt.Fprintf(errw, "crucible %s: unknown subcommand %q\n\n", b.Name, sub)
|
||||
builderHelp(errw, b)
|
||||
return exitUsage
|
||||
}
|
||||
// Delegate to the migrated legacy command surface. args[0] is already the
|
||||
// legacy command name, so it is forwarded unchanged.
|
||||
return delegateLegacy(args, errw)
|
||||
}
|
||||
|
||||
// delegateLegacy runs a migrated nwn-tool command via internal/app and maps its
|
||||
// (code, err) result onto the dispatcher's exit code. app writes its own output
|
||||
// to os.Stdout/os.Stderr (the writers the entrypoints pass through), so only the
|
||||
// returned error is surfaced here.
|
||||
func delegateLegacy(args []string, errw io.Writer) int {
|
||||
code, err := app.Run(args)
|
||||
if err != nil {
|
||||
fmt.Fprintln(errw, err)
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
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.
|
||||
The %[2]s builder has no migrated logic in this tree yet, so it fails closed
|
||||
(exit 70) rather than producing a fake artifact. See docs/command-surface.md.
|
||||
`
|
||||
|
||||
func usage(w io.Writer) {
|
||||
@@ -147,6 +202,8 @@ func usage(w io.Writer) {
|
||||
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: name<TAB>bin<TAB>summary)\n")
|
||||
fmt.Fprintf(w, " config [args] inspect/validate effective configuration\n")
|
||||
fmt.Fprintf(w, " changelog [args] generate the release changelog\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")
|
||||
}
|
||||
@@ -161,15 +218,15 @@ func list(w io.Writer) {
|
||||
|
||||
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")
|
||||
if !b.Wired {
|
||||
fmt.Fprintf(w, "status: not wired — no migrated logic yet; fails closed (exit 70).\n")
|
||||
fmt.Fprintf(w, "See docs/command-surface.md.\n")
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "status: scaffolded, not wired (Phase 5). See docs/command-surface.md.\n")
|
||||
fmt.Fprintf(w, "usage: crucible %s <subcommand> [args] (or: %s <subcommand> [args])\n\n", b.Name, b.Bin)
|
||||
fmt.Fprintf(w, "subcommands:\n")
|
||||
for _, c := range b.subcommands() {
|
||||
fmt.Fprintf(w, " %s\n", c)
|
||||
}
|
||||
fmt.Fprintf(w, "\nstatus: wired to migrated nwn-tool logic. See docs/command-surface.md.\n")
|
||||
}
|
||||
|
||||
@@ -55,8 +55,11 @@ func TestUnknownBuilderFailsUsage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEveryBuilderFailsClosed(t *testing.T) {
|
||||
func TestUnwiredBuilderFailsClosed(t *testing.T) {
|
||||
for _, b := range Registry {
|
||||
if b.Wired {
|
||||
continue
|
||||
}
|
||||
var out, errw bytes.Buffer
|
||||
// Via dispatcher.
|
||||
if code := run([]string{b.Name}, &out, &errw); code != exitUnwired {
|
||||
@@ -74,6 +77,28 @@ func TestEveryBuilderFailsClosed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWiredBuilderRejectsBadInvocation(t *testing.T) {
|
||||
for _, b := range Registry {
|
||||
if !b.Wired {
|
||||
continue
|
||||
}
|
||||
// No subcommand is a usage error (it must never silently delegate).
|
||||
var out, errw bytes.Buffer
|
||||
if code := runBuilder(b.Name, nil, &out, &errw); code != exitUsage {
|
||||
t.Errorf("crucible %s (no subcommand): exit=%d want %d", b.Name, code, exitUsage)
|
||||
}
|
||||
// Unknown subcommand is a usage error, not a delegate.
|
||||
out.Reset()
|
||||
errw.Reset()
|
||||
if code := runBuilder(b.Name, []string{"frobnicate"}, &out, &errw); code != exitUsage {
|
||||
t.Errorf("crucible %s frobnicate: exit=%d want %d", b.Name, code, exitUsage)
|
||||
}
|
||||
if !strings.Contains(errw.String(), "unknown subcommand") {
|
||||
t.Errorf("crucible %s frobnicate: stderr=%q", b.Name, errw.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderHelpIsOK(t *testing.T) {
|
||||
for _, b := range Registry {
|
||||
var out, errw bytes.Buffer
|
||||
@@ -85,3 +110,28 @@ func TestBuilderHelpIsOK(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every legacy command named in command-surface.md must have exactly one
|
||||
// canonical home (Builder.Legacy), so the migrated surface has no gaps or
|
||||
// ambiguous homes.
|
||||
func TestLegacyCommandsHaveExactlyOneHome(t *testing.T) {
|
||||
legacy := []string{
|
||||
"build", "build-module", "extract", "validate", "compare",
|
||||
"build-haks", "apply-hak-manifest",
|
||||
"validate-topdata", "build-topdata", "build-top-package", "compare-topdata", "convert-topdata",
|
||||
"build-wiki", "deploy-wiki",
|
||||
}
|
||||
for _, cmd := range legacy {
|
||||
homes := 0
|
||||
for _, b := range Registry {
|
||||
for _, c := range b.Legacy {
|
||||
if c == cmd {
|
||||
homes++
|
||||
}
|
||||
}
|
||||
}
|
||||
if homes != 1 {
|
||||
t.Errorf("legacy command %q has %d canonical homes, want 1", cmd, homes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user