diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index dafc4b7..fdb3071 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,7 +1,7 @@ # Implementation Plan: ReleaseKit Adoption **Last updated:** 2026-10-07 -**Status:** Ready for implementation; no stages completed. +**Status:** Stage 1 complete; Stages 2–5 pending. **Audience:** Luna coding agent. ## Scope and Execution @@ -64,7 +64,7 @@ Gitea with delivery credentials, or wait for CI. Deployment observations belong to the first adopting release and its separate delivery status. No extra release gates or platform guarantees are introduced by these adoption checks. -## Stage 1 — Add the Dependency and Version Command +## Stage 1 — Add the Dependency and Version Command [x] **Primary files:** `go.mod`, `go.sum`, `internal/cli/run.go`, and focused tests in `internal/cli`. @@ -92,6 +92,12 @@ gates or platform guarantees are introduced by these adoption checks. module diff, and commit. Real release-version injection is verified in Stage 5; no test should hardcode an upstream private linker symbol. +**Completed 2026-10-07:** Added ReleaseKit v0.1.0 and the configuration-free +`audita version` command with root/help routing and usage-error handling. +Checks passed: `go test ./internal/cli ./cmd/audita`, +`go build -o ./bin/audita ./cmd/audita`, `./bin/audita version`, and +`git diff --check`. + ## Stage 2 — Introduce the Product Profile and Thin Local Wrapper **Primary files:** new `releasekit.json`, `scripts/release.sh`, and diff --git a/go.mod b/go.mod index e37181e..e81a71e 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.5 require ( gitea.maximumdirect.net/eric/ConfigKit v0.5.0 + gitea.maximumdirect.net/eric/ReleaseKit v0.1.0 gitea.maximumdirect.net/eric/promptkit v0.12.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index cb485d6..2e596fd 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ gitea.maximumdirect.net/eric/ConfigKit v0.5.0 h1:2KwG10+kApWM3avOoYA+p768xcbHUp7tQ2rqizBNpyg= gitea.maximumdirect.net/eric/ConfigKit v0.5.0/go.mod h1:tYWpsrbLyu05r8+1MapWp5fN6ZrX9I8hU2xEPedYmYk= +gitea.maximumdirect.net/eric/ReleaseKit v0.1.0 h1:Ejk85+9Of+A8M7Mod91iB3KxrrQYgnJcz9hPA6fL1a4= +gitea.maximumdirect.net/eric/ReleaseKit v0.1.0/go.mod h1:KmH607Vc/RK/zKrCB96cWh0QZYvkbxBFvtqV+BkKaxs= gitea.maximumdirect.net/eric/promptkit v0.12.0 h1:oIFWEg6GPxtmX3Plqv0euP+FnK1LCk/t4xDy6oKCpP4= gitea.maximumdirect.net/eric/promptkit v0.12.0/go.mod h1:pFOOy5v1zH17stEi9rmHYmfbuLjnfVMcUhfmk+4VtJY= gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v3 v3.20260930.0 h1:lzfCDbzJQRGzIlRPRprnAV9hVVL5zUQy80VMQ0kbYa0= diff --git a/internal/cli/run.go b/internal/cli/run.go index f89dc88..b532aeb 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -2,9 +2,11 @@ package cli import ( "context" + "flag" "fmt" "io" + "gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" "gitea.maximumdirect.net/eric/audita/internal/core/schema" "gitea.maximumdirect.net/eric/audita/internal/framework/llm" ) @@ -42,6 +44,9 @@ func Run(ctx context.Context, args []string, stdout, stderr io.Writer) int { case "config": writeConfigUsage(stdout) return 0 + case "version": + writeVersionUsage(stdout) + return 0 } } @@ -56,12 +61,47 @@ func Run(ctx context.Context, args []string, stdout, stderr io.Writer) int { if args[0] == "config" { return runConfig(ctx, args[1:], stdout, stderr) } + if args[0] == "version" { + return runVersion(args[1:], stdout, stderr) + } fmt.Fprintf(stderr, "audita: unknown command %q\n\n", args[0]) writeRootUsage(stderr) return 2 } +func runVersion(args []string, stdout, stderr io.Writer) int { + if len(args) == 1 && (args[0] == "-h" || args[0] == "--help") { + writeVersionUsage(stdout) + return 0 + } + for _, arg := range args { + if arg == "-h" || arg == "--help" { + fmt.Fprintln(stderr, "audita version: help flags cannot be combined with other arguments") + fmt.Fprintln(stderr) + writeVersionUsage(stderr) + return 2 + } + } + flags := flag.NewFlagSet("version", flag.ContinueOnError) + flags.SetOutput(stderr) + flags.Usage = func() { writeVersionUsage(stderr) } + if err := flags.Parse(args); err != nil { + if err == flag.ErrHelp { + writeVersionUsage(stdout) + return 0 + } + return 2 + } + if positional := flags.Args(); len(positional) > 0 { + fmt.Fprintf(stderr, "audita version: unexpected argument %q\n\n", positional[0]) + writeVersionUsage(stderr) + return 2 + } + fmt.Fprintf(stdout, "audita %s\n", buildinfo.Version()) + return 0 +} + func runConfig(ctx context.Context, args []string, stdout, stderr io.Writer) int { if len(args) == 0 || isHelpCommand(args) || args[0] == "--help" || args[0] == "-h" { writeConfigUsage(stdout) @@ -177,11 +217,19 @@ func writeRootUsage(w io.Writer) { fmt.Fprintln(w, "Commands:") fmt.Fprintln(w, " process Process a transcript JSON file") fmt.Fprintln(w, " config Validate and inspect config") + fmt.Fprintln(w, " version Print the Audita version") fmt.Fprintln(w) fmt.Fprintln(w, "Example:") fmt.Fprintln(w, " audita process transcript.json --glossary glossary.yaml --output corrected.json") } +func writeVersionUsage(w io.Writer) { + fmt.Fprintln(w, "Print the Audita version.") + fmt.Fprintln(w) + fmt.Fprintln(w, "Usage:") + fmt.Fprintln(w, " audita version") +} + func writeConfigUsage(w io.Writer) { fmt.Fprintln(w, "Validate and inspect Audita config.") fmt.Fprintln(w) diff --git a/internal/cli/version_test.go b/internal/cli/version_test.go new file mode 100644 index 0000000..c3ecf73 --- /dev/null +++ b/internal/cli/version_test.go @@ -0,0 +1,72 @@ +package cli + +import ( + "bytes" + "context" + "fmt" + "strings" + "testing" + + "gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" +) + +func TestVersionCommandRunsWithoutApplicationSetup(t *testing.T) { + t.Chdir(t.TempDir()) + var stdout, stderr bytes.Buffer + if code := Run(context.Background(), []string{"version"}, &stdout, &stderr); code != 0 { + t.Fatalf("exit=%d, stdout=%q, stderr=%q", code, stdout.String(), stderr.String()) + } + if got, want := stdout.String(), fmt.Sprintf("audita %s\n", buildinfo.Version()); got != want { + t.Fatalf("stdout=%q, want %q", got, want) + } + if got := stderr.String(); got != "" { + t.Fatalf("stderr=%q, want empty", got) + } +} + +func TestVersionCommandHelp(t *testing.T) { + for _, test := range []struct { + name string + args []string + want string + }{ + {name: "root help", args: []string{"--help"}, want: "version Print the Audita version"}, + {name: "help version", args: []string{"help", "version"}, want: "audita version"}, + {name: "short flag", args: []string{"version", "-h"}, want: "audita version"}, + {name: "long flag", args: []string{"version", "--help"}, want: "audita version"}, + } { + t.Run(test.name, func(t *testing.T) { + var stdout, stderr bytes.Buffer + if code := Run(context.Background(), test.args, &stdout, &stderr); code != 0 { + t.Fatalf("exit=%d, stdout=%q, stderr=%q", code, stdout.String(), stderr.String()) + } + if !strings.Contains(stdout.String(), test.want) { + t.Fatalf("stdout=%q, want %q", stdout.String(), test.want) + } + if got := stderr.String(); got != "" { + t.Fatalf("stderr=%q, want empty", got) + } + }) + } +} + +func TestVersionCommandRejectsUnexpectedArguments(t *testing.T) { + for _, args := range [][]string{ + {"version", "extra"}, + {"version", "--unknown"}, + {"version", "--help", "extra"}, + } { + t.Run(strings.Join(args, " "), func(t *testing.T) { + var stdout, stderr bytes.Buffer + if code := Run(context.Background(), args, &stdout, &stderr); code != 2 { + t.Fatalf("exit=%d, stdout=%q, stderr=%q", code, stdout.String(), stderr.String()) + } + if got := stdout.String(); got != "" { + t.Fatalf("stdout=%q, want empty", got) + } + if got := stderr.String(); got == "" { + t.Fatal("expected usage error on stderr") + } + }) + } +}