From 23d0e4a733179324e91e9dc4e0f44374498feb44 Mon Sep 17 00:00:00 2001 From: Daniel Bos Date: Thu, 30 Jul 2026 09:55:32 +0800 Subject: [PATCH 1/5] feat: make commands embeddable in other projects --- cmd/edit.go | 40 ++++++++++++++++++---------------------- cmd/init.go | 39 ++++++++++++++++----------------------- cmd/list.go | 27 +++++++++++---------------- cmd/new.go | 33 ++++++++++++++------------------- cmd/root.go | 40 +++++++++++++++++++++++++++------------- cmd/show.go | 41 ++++++++++++++++++----------------------- cmd/update.go | 41 ++++++++++++++++++----------------------- cmd/version.go | 27 +++++++++++---------------- main.go | 7 ++++++- 9 files changed, 139 insertions(+), 156 deletions(-) diff --git a/cmd/edit.go b/cmd/edit.go index be4f14a..74c849e 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -9,29 +9,25 @@ import ( "github.com/spf13/cobra" ) -// editCmd represents the edit command. -// -//nolint:exhaustruct,gochecknoglobals -var editCmd = &cobra.Command{ - Use: "edit ", - Short: "open the adr with number in the default editor", - Run: func(_ *cobra.Command, args []string) { - ctx := context.TODO() +func NewEditCommand() *cobra.Command { + //nolint:exhaustruct + return &cobra.Command{ + Use: "edit ", + Short: "open the adr with number in the default editor", + Args: cobra.ExactArgs(1), + Run: func(_ *cobra.Command, args []string) { + ctx := context.TODO() - number, err := strconv.Atoi(args[0]) - if err != nil { - log.Printf("invalid argument: %v", err) + number, err := strconv.Atoi(args[0]) + if err != nil { + log.Printf("invalid argument: %v", err) - return - } + return + } - if err := app.Edit(ctx, number); err != nil { - log.Printf("couldn't edit adr %d: %v", number, err) - } - }, -} - -//nolint:gochecknoinits -func init() { - rootCmd.AddCommand(editCmd) + if err := app.Edit(ctx, number); err != nil { + log.Printf("couldn't edit adr %d: %v", number, err) + } + }, + } } diff --git a/cmd/init.go b/cmd/init.go index 7dfd3b7..19ff62d 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -7,29 +7,22 @@ import ( "github.com/spf13/cobra" ) -// initCmd represents the init command. -// -//nolint:exhaustruct,gochecknoglobals -var initCmd = &cobra.Command{ - Use: "init [path]", - Short: "initialize the adr path (default is `docs/adr`)", - Args: cobra.MaximumNArgs(1), - Run: func(_ *cobra.Command, args []string) { - path := "docs/adr" +func NewInitCommand() *cobra.Command { + //nolint:exhaustruct + return &cobra.Command{ + Use: "init [path]", + Short: "initialize the adr path (default is `docs/adr`)", + Args: cobra.MaximumNArgs(1), + Run: func(_ *cobra.Command, args []string) { + path := "docs/adr" - if len(args) > 0 { - path = args[0] - } + if len(args) > 0 { + path = args[0] + } - if err := app.Init(path); err != nil { - log.Printf("couldn't initialize adr: %v", err) - - return - } - }, -} - -//nolint:gochecknoinits -func init() { - rootCmd.AddCommand(initCmd) + if err := app.Init(path); err != nil { + log.Printf("couldn't initialize adr: %v", err) + } + }, + } } diff --git a/cmd/list.go b/cmd/list.go index f5d6f6f..9e3d32a 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -7,20 +7,15 @@ import ( "github.com/spf13/cobra" ) -// listCmd represents the list command. -// -//nolint:exhaustruct,gochecknoglobals -var listCmd = &cobra.Command{ - Use: "list", - Short: "list all ADRs with their id, date and status", - Run: func(_ *cobra.Command, _ []string) { - if err := app.List(); err != nil { - log.Printf("couldn't list adrs: %v", err) - } - }, -} - -//nolint:gochecknoinits -func init() { - rootCmd.AddCommand(listCmd) +func NewListCommand() *cobra.Command { + //nolint:exhaustruct + return &cobra.Command{ + Use: "list", + Short: "list all ADRs with their id, date and status", + Run: func(_ *cobra.Command, _ []string) { + if err := app.List(); err != nil { + log.Printf("couldn't list adrs: %v", err) + } + }, + } } diff --git a/cmd/new.go b/cmd/new.go index 9110867..5e1b5bd 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -9,24 +9,19 @@ import ( "github.com/spf13/cobra" ) -// newCmd represents the new command. -// -//nolint:exhaustruct,gochecknoglobals -var newCmd = &cobra.Command{ - Use: "new [title]", - Aliases: []string{"add", "create"}, - Short: "create a new ADR with optional title", - Run: func(_ *cobra.Command, args []string) { - ctx := context.TODO() - title := strings.Join(args, " ") +func NewNewCommand() *cobra.Command { + //nolint:exhaustruct + return &cobra.Command{ + Use: "new [title]", + Aliases: []string{"add", "create"}, + Short: "create a new ADR with optional title", + Run: func(_ *cobra.Command, args []string) { + ctx := context.TODO() + title := strings.Join(args, " ") - if err := app.Create(ctx, title); err != nil { - log.Printf("couldn't create adr: %v", err) - } - }, -} - -//nolint:gochecknoinits -func init() { - rootCmd.AddCommand(newCmd) + if err := app.Create(ctx, title); err != nil { + log.Printf("couldn't create adr: %v", err) + } + }, + } } diff --git a/cmd/root.go b/cmd/root.go index f8df855..baa224f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,23 +1,37 @@ package cmd import ( - "os" - "github.com/spf13/cobra" ) -// rootCmd represents the base command when called without any subcommands. -// -//nolint:exhaustruct,gochecknoglobals -var rootCmd = &cobra.Command{ - Use: "adr", - Short: "A command line tool to maintain Architecture Decision Records", +func NewRootCommand(name string) *cobra.Command { + //nolint:exhaustruct + root := &cobra.Command{ + Use: name, + Short: "A command line tool to maintain Architecture Decision Records", + } + + root.AddCommand( + NewInitCommand(), + NewNewCommand(), + NewListCommand(), + NewShowCommand(), + NewEditCommand(), + NewUpdateCommand(), + NewVersionCommand(), + ) + + return root } -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - if err := rootCmd.Execute(); err != nil { - os.Exit(1) +// ADRCommands returns the commands suitable for embedding in another CLI. +func ADRCommands() []*cobra.Command { + return []*cobra.Command{ + NewInitCommand(), + NewNewCommand(), + NewListCommand(), + NewShowCommand(), + NewEditCommand(), + NewUpdateCommand(), } } diff --git a/cmd/show.go b/cmd/show.go index 3ba0ac5..16e3c9a 100644 --- a/cmd/show.go +++ b/cmd/show.go @@ -8,29 +8,24 @@ import ( "github.com/spf13/cobra" ) -// showCmd represents the show command. -// -//nolint:exhaustruct,gochecknoglobals -var showCmd = &cobra.Command{ - Use: "show ", - Aliases: []string{"view"}, - Short: "show the adr with number ", - Args: cobra.ExactArgs(1), - Run: func(_ *cobra.Command, args []string) { - number, err := strconv.Atoi(args[0]) - if err != nil { - log.Printf("invalid argument: %v", err) +func NewShowCommand() *cobra.Command { + //nolint:exhaustruct + return &cobra.Command{ + Use: "show ", + Aliases: []string{"view"}, + Short: "show the adr with number ", + Args: cobra.ExactArgs(1), + Run: func(_ *cobra.Command, args []string) { + number, err := strconv.Atoi(args[0]) + if err != nil { + log.Printf("invalid argument: %v", err) - return - } + return + } - if err := app.Show(number); err != nil { - log.Printf("couldn't show adr %d: %v", number, err) - } - }, -} - -//nolint:gochecknoinits -func init() { - rootCmd.AddCommand(showCmd) + if err := app.Show(number); err != nil { + log.Printf("couldn't show adr %d: %v", number, err) + } + }, + } } diff --git a/cmd/update.go b/cmd/update.go index 707666d..48f83f8 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -9,30 +9,25 @@ import ( "github.com/spf13/cobra" ) -// updateCmd represents the update command. -// -//nolint:exhaustruct,gochecknoglobals -var updateCmd = &cobra.Command{ - Use: "update ", - Short: "update the adr with number to status ", - Args: cobra.ExactArgs(2), //nolint:mnd - Run: func(_ *cobra.Command, args []string) { - number, err := strconv.Atoi(args[0]) - if err != nil { - log.Printf("invalid argument: %v", err) +func NewUpdateCommand() *cobra.Command { + //nolint:exhaustruct + return &cobra.Command{ + Use: "update ", + Short: "update the adr with number to status ", + Args: cobra.ExactArgs(2), //nolint:mnd + Run: func(_ *cobra.Command, args []string) { + number, err := strconv.Atoi(args[0]) + if err != nil { + log.Printf("invalid argument: %v", err) - return - } + return + } - status := strings.ToLower(args[1]) + status := strings.ToLower(args[1]) - if err := app.Update(number, status); err != nil { - log.Printf("couldn't update adr %d: %v", number, err) - } - }, -} - -//nolint:gochecknoinits -func init() { - rootCmd.AddCommand(updateCmd) + if err := app.Update(number, status); err != nil { + log.Printf("couldn't update adr %d: %v", number, err) + } + }, + } } diff --git a/cmd/version.go b/cmd/version.go index 6f45dc7..dde55d6 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -8,20 +8,15 @@ import ( "github.com/spf13/cobra" ) -// versionCmd represents the version command. -// -//nolint:exhaustruct,gochecknoglobals -var versionCmd = &cobra.Command{ - Use: "version", - Short: "show the version information", - Run: func(_ *cobra.Command, _ []string) { - if err := app.Version(os.Args[0]); err != nil { - log.Printf("couldn't show version: %v", err) - } - }, -} - -//nolint:gochecknoinits -func init() { - rootCmd.AddCommand(versionCmd) +func NewVersionCommand() *cobra.Command { + //nolint:exhaustruct + return &cobra.Command{ + Use: "version", + Short: "show the version information", + Run: func(_ *cobra.Command, _ []string) { + if err := app.Version(os.Args[0]); err != nil { + log.Printf("couldn't show version: %v", err) + } + }, + } } diff --git a/main.go b/main.go index 9cf4a3c..4cfa9c7 100644 --- a/main.go +++ b/main.go @@ -19,12 +19,17 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + package main import ( + "os" + "github.com/corani/adr/cmd" ) func main() { - cmd.Execute() + if err := cmd.NewRootCommand(os.Args[0]).Execute(); err != nil { + os.Exit(1) + } } From 9f6f19a860a94bbf322121dbbc87eb9bcac843e6 Mon Sep 17 00:00:00 2001 From: Daniel Bos Date: Thu, 30 Jul 2026 09:55:53 +0800 Subject: [PATCH 2/5] chore: add package documentation --- cmd/doc.go | 4 ++++ doc.go | 2 ++ internal/adr/doc.go | 4 ++++ internal/app/doc.go | 4 ++++ internal/config/doc.go | 4 ++++ internal/template/doc.go | 3 +++ 6 files changed, 21 insertions(+) create mode 100644 cmd/doc.go create mode 100644 doc.go create mode 100644 internal/adr/doc.go create mode 100644 internal/app/doc.go create mode 100644 internal/config/doc.go create mode 100644 internal/template/doc.go diff --git a/cmd/doc.go b/cmd/doc.go new file mode 100644 index 0000000..35bb718 --- /dev/null +++ b/cmd/doc.go @@ -0,0 +1,4 @@ +// Package cmd defines the cobra commands for the adr CLI. +// Use NewRootCommand to build a standalone CLI, or ADRCommands to embed the +// ADR subcommands into another cobra-based CLI. +package cmd diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..3b7d563 --- /dev/null +++ b/doc.go @@ -0,0 +1,2 @@ +// Package main is the entry point for the adr CLI. +package main diff --git a/internal/adr/doc.go b/internal/adr/doc.go new file mode 100644 index 0000000..44ffc9a --- /dev/null +++ b/internal/adr/doc.go @@ -0,0 +1,4 @@ +// Package adr provides the core ADR domain types and file I/O operations. +// It defines the Adr struct, status values, and functions for reading and +// writing ADR files on disk. +package adr diff --git a/internal/app/doc.go b/internal/app/doc.go new file mode 100644 index 0000000..190fe3b --- /dev/null +++ b/internal/app/doc.go @@ -0,0 +1,4 @@ +// Package app implements the application logic for each adr command. +// Each function corresponds to a CLI command (Init, Create, List, Show, Edit, +// Update, Version) and orchestrates the adr, config, and template packages. +package app diff --git a/internal/config/doc.go b/internal/config/doc.go new file mode 100644 index 0000000..34d6972 --- /dev/null +++ b/internal/config/doc.go @@ -0,0 +1,4 @@ +// Package config handles reading and writing the adr configuration file. +// The configuration is stored as YAML alongside the ADR files and controls +// the project root, ADR directory, and template paths. +package config diff --git a/internal/template/doc.go b/internal/template/doc.go new file mode 100644 index 0000000..b5931a6 --- /dev/null +++ b/internal/template/doc.go @@ -0,0 +1,3 @@ +// Package template provides the embedded default templates for ADR and index +// files, and functions to write them to disk during initialisation. +package template From 047708c402debcc2de2f82f1760c0fa49ef25d51 Mon Sep 17 00:00:00 2001 From: Daniel Bos Date: Thu, 30 Jul 2026 11:04:26 +0800 Subject: [PATCH 3/5] feat: make config injectable when embedding commands --- cmd/doc.go | 4 ++-- cmd/edit.go | 5 ++-- cmd/init.go | 25 ++++++++++++++++++-- cmd/list.go | 5 ++-- cmd/new.go | 5 ++-- cmd/root.go | 48 +++++++++++++++++++-------------------- cmd/show.go | 5 ++-- cmd/update.go | 5 ++-- config/config.go | 8 +++++++ config/doc.go | 4 ++++ config/error.go | 5 ++++ internal/adr/adr.go | 2 +- internal/adr/doc.go | 2 +- internal/app/edit.go | 12 ++++------ internal/app/init.go | 26 ++++++++++++--------- internal/app/list.go | 11 +++------ internal/app/new.go | 11 +++------ internal/app/show.go | 9 ++------ internal/app/update.go | 14 ++++-------- internal/config/config.go | 22 ++++++++---------- internal/config/doc.go | 5 ++-- internal/template/doc.go | 2 +- main.go | 22 +++++++++++++++++- 23 files changed, 147 insertions(+), 110 deletions(-) create mode 100644 config/config.go create mode 100644 config/doc.go create mode 100644 config/error.go diff --git a/cmd/doc.go b/cmd/doc.go index 35bb718..23f3f75 100644 --- a/cmd/doc.go +++ b/cmd/doc.go @@ -1,4 +1,4 @@ // Package cmd defines the cobra commands for the adr CLI. -// Use NewRootCommand to build a standalone CLI, or ADRCommands to embed the -// ADR subcommands into another cobra-based CLI. +// Use AdrCommands to build the full standalone command set, or EmbedCommands +// to embed ADR subcommands (without init-config and version) in another CLI. package cmd diff --git a/cmd/edit.go b/cmd/edit.go index 74c849e..65307e7 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -5,11 +5,12 @@ import ( "log" "strconv" + "github.com/corani/adr/config" "github.com/corani/adr/internal/app" "github.com/spf13/cobra" ) -func NewEditCommand() *cobra.Command { +func NewEditCommand(conf *config.Config) *cobra.Command { //nolint:exhaustruct return &cobra.Command{ Use: "edit ", @@ -25,7 +26,7 @@ func NewEditCommand() *cobra.Command { return } - if err := app.Edit(ctx, number); err != nil { + if err := app.Edit(ctx, conf, number); err != nil { log.Printf("couldn't edit adr %d: %v", number, err) } }, diff --git a/cmd/init.go b/cmd/init.go index 19ff62d..84db226 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -3,11 +3,12 @@ package cmd import ( "log" + "github.com/corani/adr/config" "github.com/corani/adr/internal/app" "github.com/spf13/cobra" ) -func NewInitCommand() *cobra.Command { +func NewInitConfigCommand() *cobra.Command { //nolint:exhaustruct return &cobra.Command{ Use: "init [path]", @@ -20,7 +21,27 @@ func NewInitCommand() *cobra.Command { path = args[0] } - if err := app.Init(path); err != nil { + conf, err := app.InitConfig(path) + if err != nil { + log.Printf("couldn't initialize adr: %v", err) + + return + } + + if err := app.Init(conf); err != nil { + log.Printf("couldn't initialize adr: %v", err) + } + }, + } +} + +func NewInitCommand(conf *config.Config) *cobra.Command { + //nolint:exhaustruct + return &cobra.Command{ + Use: "init", + Short: "initialize the adr path", + Run: func(_ *cobra.Command, args []string) { + if err := app.Init(conf); err != nil { log.Printf("couldn't initialize adr: %v", err) } }, diff --git a/cmd/list.go b/cmd/list.go index 9e3d32a..cb1122a 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -3,17 +3,18 @@ package cmd import ( "log" + "github.com/corani/adr/config" "github.com/corani/adr/internal/app" "github.com/spf13/cobra" ) -func NewListCommand() *cobra.Command { +func NewListCommand(conf *config.Config) *cobra.Command { //nolint:exhaustruct return &cobra.Command{ Use: "list", Short: "list all ADRs with their id, date and status", Run: func(_ *cobra.Command, _ []string) { - if err := app.List(); err != nil { + if err := app.List(conf); err != nil { log.Printf("couldn't list adrs: %v", err) } }, diff --git a/cmd/new.go b/cmd/new.go index 5e1b5bd..92875a6 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -5,11 +5,12 @@ import ( "log" "strings" + "github.com/corani/adr/config" "github.com/corani/adr/internal/app" "github.com/spf13/cobra" ) -func NewNewCommand() *cobra.Command { +func NewNewCommand(conf *config.Config) *cobra.Command { //nolint:exhaustruct return &cobra.Command{ Use: "new [title]", @@ -19,7 +20,7 @@ func NewNewCommand() *cobra.Command { ctx := context.TODO() title := strings.Join(args, " ") - if err := app.Create(ctx, title); err != nil { + if err := app.Create(ctx, conf, title); err != nil { log.Printf("couldn't create adr: %v", err) } }, diff --git a/cmd/root.go b/cmd/root.go index baa224f..7ddf101 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,37 +1,35 @@ package cmd import ( + "github.com/corani/adr/config" "github.com/spf13/cobra" ) -func NewRootCommand(name string) *cobra.Command { - //nolint:exhaustruct - root := &cobra.Command{ - Use: name, - Short: "A command line tool to maintain Architecture Decision Records", +// EmbedCommands returns the commands suitable for embedding in another CLI. +// The caller is responsible for providing a fully populated conf; init-config +// and version are excluded as they are specific to the standalone adr CLI. +func EmbedCommands(conf *config.Config) []*cobra.Command { + return []*cobra.Command{ + NewInitCommand(conf), + NewNewCommand(conf), + NewListCommand(conf), + NewShowCommand(conf), + NewEditCommand(conf), + NewUpdateCommand(conf), } - - root.AddCommand( - NewInitCommand(), - NewNewCommand(), - NewListCommand(), - NewShowCommand(), - NewEditCommand(), - NewUpdateCommand(), - NewVersionCommand(), - ) - - return root } -// ADRCommands returns the commands suitable for embedding in another CLI. -func ADRCommands() []*cobra.Command { +// AdrCommands returns the full command set for the standalone adr CLI, +// including init-config (which discovers the project root and writes .adr.yaml) +// and version. +func AdrCommands(conf *config.Config) []*cobra.Command { return []*cobra.Command{ - NewInitCommand(), - NewNewCommand(), - NewListCommand(), - NewShowCommand(), - NewEditCommand(), - NewUpdateCommand(), + NewInitConfigCommand(), + NewVersionCommand(), + NewNewCommand(conf), + NewListCommand(conf), + NewShowCommand(conf), + NewEditCommand(conf), + NewUpdateCommand(conf), } } diff --git a/cmd/show.go b/cmd/show.go index 16e3c9a..4c3f2e9 100644 --- a/cmd/show.go +++ b/cmd/show.go @@ -4,11 +4,12 @@ import ( "log" "strconv" + "github.com/corani/adr/config" "github.com/corani/adr/internal/app" "github.com/spf13/cobra" ) -func NewShowCommand() *cobra.Command { +func NewShowCommand(conf *config.Config) *cobra.Command { //nolint:exhaustruct return &cobra.Command{ Use: "show ", @@ -23,7 +24,7 @@ func NewShowCommand() *cobra.Command { return } - if err := app.Show(number); err != nil { + if err := app.Show(conf, number); err != nil { log.Printf("couldn't show adr %d: %v", number, err) } }, diff --git a/cmd/update.go b/cmd/update.go index 48f83f8..5a2cc0c 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -5,11 +5,12 @@ import ( "strconv" "strings" + "github.com/corani/adr/config" "github.com/corani/adr/internal/app" "github.com/spf13/cobra" ) -func NewUpdateCommand() *cobra.Command { +func NewUpdateCommand(conf *config.Config) *cobra.Command { //nolint:exhaustruct return &cobra.Command{ Use: "update ", @@ -25,7 +26,7 @@ func NewUpdateCommand() *cobra.Command { status := strings.ToLower(args[1]) - if err := app.Update(number, status); err != nil { + if err := app.Update(conf, number, status); err != nil { log.Printf("couldn't update adr %d: %v", number, err) } }, diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..3fae8f4 --- /dev/null +++ b/config/config.go @@ -0,0 +1,8 @@ +package config + +type Config struct { + Project string `yaml:"-"` + Root string `yaml:"root"` + AdrTemplate string `yaml:"adrTemplate"` + IndexTemplate string `yaml:"indexTemplate"` +} diff --git a/config/doc.go b/config/doc.go new file mode 100644 index 0000000..e4bcef5 --- /dev/null +++ b/config/doc.go @@ -0,0 +1,4 @@ +// Package config handles reading and writing the ADR configuration file. +// The configuration is stored as YAML alongside the ADR files and controls +// the project root, ADR directory, and template paths. +package config diff --git a/config/error.go b/config/error.go new file mode 100644 index 0000000..157aa6b --- /dev/null +++ b/config/error.go @@ -0,0 +1,5 @@ +package config + +import "errors" + +var ErrConfig = errors.New("config") diff --git a/internal/adr/adr.go b/internal/adr/adr.go index 8214911..1e591e0 100644 --- a/internal/adr/adr.go +++ b/internal/adr/adr.go @@ -11,7 +11,7 @@ import ( "time" "github.com/adrg/frontmatter" - "github.com/corani/adr/internal/config" + "github.com/corani/adr/config" "github.com/gosimple/slug" "gopkg.in/yaml.v3" ) diff --git a/internal/adr/doc.go b/internal/adr/doc.go index 44ffc9a..e7ce9b4 100644 --- a/internal/adr/doc.go +++ b/internal/adr/doc.go @@ -1,4 +1,4 @@ -// Package adr provides the core ADR domain types and file I/O operations. +// Package adr provides the core ADR domain types and file IO operations. // It defines the Adr struct, status values, and functions for reading and // writing ADR files on disk. package adr diff --git a/internal/app/edit.go b/internal/app/edit.go index 1d354ff..331a5d5 100644 --- a/internal/app/edit.go +++ b/internal/app/edit.go @@ -8,16 +8,11 @@ import ( "os/exec" "path/filepath" + "github.com/corani/adr/config" "github.com/corani/adr/internal/adr" - "github.com/corani/adr/internal/config" ) -func Edit(ctx context.Context, number int) error { - conf, err := config.ReadConfig() - if err != nil { - return fmt.Errorf("%w: edit: %w", ErrInternal, err) - } - +func Edit(ctx context.Context, conf *config.Config, number int) error { found, err := adr.ByID(conf, adr.Number(number)) if err != nil { return fmt.Errorf("%w: edit: %w", ErrInternal, err) @@ -26,7 +21,8 @@ func Edit(ctx context.Context, number int) error { log.Printf("editing ADR: %v", filepath.Join(conf.Root, found.Filename)) // #nosec G204,G702 // Command injection via environment variable - cmd := exec.CommandContext(ctx, os.Getenv("EDITOR"), filepath.Join(conf.Project, conf.Root, found.Filename)) + cmd := exec.CommandContext(ctx, os.Getenv("EDITOR"), + filepath.Join(conf.Project, conf.Root, found.Filename)) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/internal/app/init.go b/internal/app/init.go index 29ccecc..9cfc23b 100644 --- a/internal/app/init.go +++ b/internal/app/init.go @@ -11,41 +11,45 @@ import ( "github.com/corani/adr/internal/template" ) -func Init(path string) error { +func InitConfig(path string) (*config.Config, error) { root, err := config.ProjectRoot() if err != nil { - return fmt.Errorf("%w: init: %w", ErrInternal, err) + return nil, fmt.Errorf("%w: init: %w", ErrInternal, err) } conf := &config.Config{ Root: path, AdrTemplate: filepath.Join(path, "adr-template.md"), IndexTemplate: filepath.Join(path, "index-template.md"), - Project: "", + Project: root, } - log.Printf("[CMD] mkdir -p %q", path) + log.Printf(`create ".adr.yaml"`) - //nolint:mnd,gofumpt - if err := os.MkdirAll(filepath.Join(root, path), 0750); err != nil { - return fmt.Errorf("%w: init: %w", ErrInternal, err) + if err := config.WriteConfig(root, conf); err != nil { + return nil, fmt.Errorf("%w: init: %w", ErrInternal, err) } - log.Printf(`create ".adr.yaml"`) + return conf, nil +} - if err := config.WriteConfig(root, conf); err != nil { +func Init(conf *config.Config) error { + log.Printf("[CMD] mkdir -p %q", conf.Root) + + //nolint:mnd,gofumpt + if err := os.MkdirAll(filepath.Join(conf.Project, conf.Root), 0o750); err != nil { return fmt.Errorf("%w: init: %w", ErrInternal, err) } log.Printf("create %q", conf.AdrTemplate) - if err := template.Write("adr.md", filepath.Join(root, conf.AdrTemplate)); err != nil { + if err := template.Write("adr.md", filepath.Join(conf.Project, conf.AdrTemplate)); err != nil { return fmt.Errorf("%w: init: %w", ErrInternal, err) } log.Printf("create %q", conf.IndexTemplate) - if err := template.Write("index.md", filepath.Join(root, conf.IndexTemplate)); err != nil { + if err := template.Write("index.md", filepath.Join(conf.Project, conf.IndexTemplate)); err != nil { return fmt.Errorf("%w: init: %w", ErrInternal, err) } diff --git a/internal/app/list.go b/internal/app/list.go index aa7dc98..5fe3e71 100644 --- a/internal/app/list.go +++ b/internal/app/list.go @@ -4,17 +4,12 @@ import ( "fmt" "os" + "github.com/corani/adr/config" "github.com/corani/adr/internal/adr" - "github.com/corani/adr/internal/config" "github.com/jedib0t/go-pretty/v6/table" ) -func List() error { - conf, err := config.ReadConfig() - if err != nil { - return fmt.Errorf("%w: list: %w", ErrInternal, err) - } - +func List(conf *config.Config) error { tbl := table.NewWriter() tbl.SetOutputMirror(os.Stdout) @@ -28,7 +23,7 @@ func List() error { }}) tbl.AppendHeader(table.Row{"#", "date", "status", "title"}) - err = adr.ForEach(conf, func(v *adr.Adr) error { + err := adr.ForEach(conf, func(v *adr.Adr) error { tbl.AppendRow(table.Row{fmt.Sprintf("%04d", v.Number), v.Date, v.Status, v.Title}) return nil diff --git a/internal/app/new.go b/internal/app/new.go index 6f01e5c..15f5ff3 100644 --- a/internal/app/new.go +++ b/internal/app/new.go @@ -4,20 +4,15 @@ import ( "context" "fmt" + "github.com/corani/adr/config" "github.com/corani/adr/internal/adr" - "github.com/corani/adr/internal/config" ) -func Create(ctx context.Context, title string) error { - conf, err := config.ReadConfig() - if err != nil { - return fmt.Errorf("%w: create: %w", ErrInternal, err) - } - +func Create(ctx context.Context, conf *config.Config, title string) error { v, err := adr.Create(conf, title) if err != nil { return fmt.Errorf("%w: create: %w", ErrInternal, err) } - return Edit(ctx, int(v.Number)) + return Edit(ctx, conf, int(v.Number)) } diff --git a/internal/app/show.go b/internal/app/show.go index fb85fc2..fd61731 100644 --- a/internal/app/show.go +++ b/internal/app/show.go @@ -5,17 +5,12 @@ import ( "os" markdown "github.com/MichaelMure/go-term-markdown" + "github.com/corani/adr/config" "github.com/corani/adr/internal/adr" - "github.com/corani/adr/internal/config" "github.com/jedib0t/go-pretty/v6/table" ) -func Show(number int) error { - conf, err := config.ReadConfig() - if err != nil { - return fmt.Errorf("%w: show: %w", ErrInternal, err) - } - +func Show(conf *config.Config, number int) error { found, err := adr.ByID(conf, adr.Number(number)) if err != nil { return fmt.Errorf("%w: show: %w", ErrInternal, err) diff --git a/internal/app/update.go b/internal/app/update.go index 9d779e6..110cab2 100644 --- a/internal/app/update.go +++ b/internal/app/update.go @@ -4,25 +4,21 @@ import ( "errors" "fmt" + "github.com/corani/adr/config" "github.com/corani/adr/internal/adr" - "github.com/corani/adr/internal/config" ) var ErrInvalidStatus = errors.New("invalid status") -func Update(number int, status string) error { +func Update(conf *config.Config, number int, status string) error { switch adr.Status(status) { - case adr.StatusProposed, adr.StatusAccepted, adr.StatusDeprecated, adr.StatusSuperseded: - // ok + case adr.StatusProposed, adr.StatusAccepted, + adr.StatusDeprecated, adr.StatusSuperseded: + // okay default: return fmt.Errorf("%w: %v", ErrInvalidStatus, status) } - conf, err := config.ReadConfig() - if err != nil { - return fmt.Errorf("%w: update: %w", ErrInternal, err) - } - found, err := adr.ByID(conf, adr.Number(number)) if err != nil { return fmt.Errorf("%w: update: %w", ErrInternal, err) diff --git a/internal/config/config.go b/internal/config/config.go index 6823f67..f84ea97 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,15 +6,11 @@ import ( "os" "path/filepath" + "github.com/corani/adr/config" "gopkg.in/yaml.v3" ) -type Config struct { - Project string `yaml:"-"` - Root string `yaml:"root"` - AdrTemplate string `yaml:"adrTemplate"` - IndexTemplate string `yaml:"indexTemplate"` -} +type Config = config.Config func ProjectRoot() (string, error) { cwd, err := os.Getwd() @@ -43,7 +39,7 @@ func ProjectRoot() (string, error) { return cwd, nil } -func ReadConfig() (*Config, error) { +func ReadConfig() (*config.Config, error) { root, err := ProjectRoot() if err != nil { return nil, err @@ -58,28 +54,28 @@ func ReadConfig() (*Config, error) { } defer out.Close() //nolint:errcheck - var config Config + var cfg config.Config - if err := yaml.NewDecoder(out).Decode(&config); err != nil { + if err := yaml.NewDecoder(out).Decode(&cfg); err != nil { return nil, fmt.Errorf("%w: read: %w", ErrConfig, err) } - config.Project = root + cfg.Project = root - return &config, nil + return &cfg, nil } return nil, os.ErrNotExist } -func WriteConfig(root string, config *Config) error { +func WriteConfig(root string, cfg *config.Config) error { out, err := os.Create(filepath.Join(root, ".adr.yaml")) // #nosec G304 if err != nil { return fmt.Errorf("%w: write: %w", ErrConfig, err) } defer out.Close() //nolint:errcheck - if err := yaml.NewEncoder(out).Encode(config); err != nil { + if err := yaml.NewEncoder(out).Encode(cfg); err != nil { return fmt.Errorf("%w: write: %w", ErrConfig, err) } diff --git a/internal/config/doc.go b/internal/config/doc.go index 34d6972..5f9b691 100644 --- a/internal/config/doc.go +++ b/internal/config/doc.go @@ -1,4 +1,3 @@ -// Package config handles reading and writing the adr configuration file. -// The configuration is stored as YAML alongside the ADR files and controls -// the project root, ADR directory, and template paths. +// Package config provides internal helpers for reading and writing the +// .adr.yaml config file and discovering the project root. package config diff --git a/internal/template/doc.go b/internal/template/doc.go index b5931a6..5075278 100644 --- a/internal/template/doc.go +++ b/internal/template/doc.go @@ -1,3 +1,3 @@ // Package template provides the embedded default templates for ADR and index -// files, and functions to write them to disk during initialisation. +// files, and functions to write them to disk during initialization. package template diff --git a/main.go b/main.go index 4cfa9c7..99e81cd 100644 --- a/main.go +++ b/main.go @@ -23,13 +23,33 @@ THE SOFTWARE. package main import ( + "log" "os" "github.com/corani/adr/cmd" + "github.com/corani/adr/internal/config" + "github.com/spf13/cobra" ) +func newRootCommand() *cobra.Command { + //nolint:exhaustruct + root := &cobra.Command{ + Use: os.Args[0], + Short: "A command line tool to maintain Architecture Decision Records", + } + + conf, err := config.ReadConfig() + if err != nil && !os.IsNotExist(err) { + log.Printf("couldn't read config: %v", err) + } + + root.AddCommand(cmd.AdrCommands(conf)...) + + return root +} + func main() { - if err := cmd.NewRootCommand(os.Args[0]).Execute(); err != nil { + if err := newRootCommand().Execute(); err != nil { os.Exit(1) } } From 25e4fda0518c82138d2f622ce2a6b0c12a3bb726 Mon Sep 17 00:00:00 2001 From: Daniel Bos Date: Thu, 30 Jul 2026 11:16:47 +0800 Subject: [PATCH 4/5] chore: improve command documentation --- cmd/edit.go | 6 +++++- cmd/init.go | 16 ++++++++++++++-- cmd/list.go | 2 +- cmd/new.go | 7 ++++++- cmd/root.go | 6 +++--- cmd/show.go | 6 +++++- cmd/update.go | 7 ++++++- cmd/version.go | 2 +- 8 files changed, 41 insertions(+), 11 deletions(-) diff --git a/cmd/edit.go b/cmd/edit.go index 65307e7..34c87d9 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -14,7 +14,11 @@ func NewEditCommand(conf *config.Config) *cobra.Command { //nolint:exhaustruct return &cobra.Command{ Use: "edit ", - Short: "open the adr with number in the default editor", + Short: "Open the ADR with number in the default editor", + Long: `Open the ADR with the given number in the default editor. + +The editor is determined by the $EDITOR environment variable. If $EDITOR is +not set, the command will fail.`, Args: cobra.ExactArgs(1), Run: func(_ *cobra.Command, args []string) { ctx := context.TODO() diff --git a/cmd/init.go b/cmd/init.go index 84db226..ad03b35 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -12,7 +12,16 @@ func NewInitConfigCommand() *cobra.Command { //nolint:exhaustruct return &cobra.Command{ Use: "init [path]", - Short: "initialize the adr path (default is `docs/adr`)", + Short: "Initialize the ADR path (default is `docs/adr`)", + Long: `Initialize the ADR directory and configuration. + +Creates the ADR directory, writes .adr.yaml at the project root, and copies +the default ADR and index templates into the directory. The project root is +determined by walking up from the current directory until a .git directory +is found. + +The path argument sets the ADR directory relative to the project root +(default: docs/adr).`, Args: cobra.MaximumNArgs(1), Run: func(_ *cobra.Command, args []string) { path := "docs/adr" @@ -39,7 +48,10 @@ func NewInitCommand(conf *config.Config) *cobra.Command { //nolint:exhaustruct return &cobra.Command{ Use: "init", - Short: "initialize the adr path", + Short: "Initialize the ADR path", + Long: `Initialize the ADR directory using the pre-configured path. + +Creates the ADR directory and copies the default ADR and index templates into it.`, Run: func(_ *cobra.Command, args []string) { if err := app.Init(conf); err != nil { log.Printf("couldn't initialize adr: %v", err) diff --git a/cmd/list.go b/cmd/list.go index cb1122a..534d3ca 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -12,7 +12,7 @@ func NewListCommand(conf *config.Config) *cobra.Command { //nolint:exhaustruct return &cobra.Command{ Use: "list", - Short: "list all ADRs with their id, date and status", + Short: "List all ADRs with their id, date and status", Run: func(_ *cobra.Command, _ []string) { if err := app.List(conf); err != nil { log.Printf("couldn't list adrs: %v", err) diff --git a/cmd/new.go b/cmd/new.go index 92875a6..4339b51 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -15,7 +15,12 @@ func NewNewCommand(conf *config.Config) *cobra.Command { return &cobra.Command{ Use: "new [title]", Aliases: []string{"add", "create"}, - Short: "create a new ADR with optional title", + Short: "Create a new ADR with optional title", + Long: `Create a new ADR with an auto-incremented number and optional title. + +The title can be supplied as arguments or left empty to be filled in later. +After creating the file, it is immediately opened in the default editor +(see $EDITOR).`, Run: func(_ *cobra.Command, args []string) { ctx := context.TODO() title := strings.Join(args, " ") diff --git a/cmd/root.go b/cmd/root.go index 7ddf101..8dc6f8e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,8 +6,8 @@ import ( ) // EmbedCommands returns the commands suitable for embedding in another CLI. -// The caller is responsible for providing a fully populated conf; init-config -// and version are excluded as they are specific to the standalone adr CLI. +// The caller is responsible for providing a fully populated `conf`; init-config +// and version are excluded as they are specific to the standalone `adr` CLI. func EmbedCommands(conf *config.Config) []*cobra.Command { return []*cobra.Command{ NewInitCommand(conf), @@ -19,7 +19,7 @@ func EmbedCommands(conf *config.Config) []*cobra.Command { } } -// AdrCommands returns the full command set for the standalone adr CLI, +// AdrCommands returns the full command set for the standalone `adr` CLI, // including init-config (which discovers the project root and writes .adr.yaml) // and version. func AdrCommands(conf *config.Config) []*cobra.Command { diff --git a/cmd/show.go b/cmd/show.go index 4c3f2e9..78f8d4f 100644 --- a/cmd/show.go +++ b/cmd/show.go @@ -14,7 +14,11 @@ func NewShowCommand(conf *config.Config) *cobra.Command { return &cobra.Command{ Use: "show ", Aliases: []string{"view"}, - Short: "show the adr with number ", + Short: "Show the ADR with number ", + Long: `Show the ADR with the given number. + +Prints a summary table (number, date, status, filename) followed by the +rendered markdown body of the ADR.`, Args: cobra.ExactArgs(1), Run: func(_ *cobra.Command, args []string) { number, err := strconv.Atoi(args[0]) diff --git a/cmd/update.go b/cmd/update.go index 5a2cc0c..2869d9a 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -14,7 +14,12 @@ func NewUpdateCommand(conf *config.Config) *cobra.Command { //nolint:exhaustruct return &cobra.Command{ Use: "update ", - Short: "update the adr with number to status ", + Short: "Update the ADR with number to status ", + Long: `Update the status of the ADR with the given number. + +Valid statuses are: proposed, accepted, deprecated, superseded. + +The index (README.md) is regenerated automatically after the update.`, Args: cobra.ExactArgs(2), //nolint:mnd Run: func(_ *cobra.Command, args []string) { number, err := strconv.Atoi(args[0]) diff --git a/cmd/version.go b/cmd/version.go index dde55d6..9e688a1 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -12,7 +12,7 @@ func NewVersionCommand() *cobra.Command { //nolint:exhaustruct return &cobra.Command{ Use: "version", - Short: "show the version information", + Short: "Show the version information", Run: func(_ *cobra.Command, _ []string) { if err := app.Version(os.Args[0]); err != nil { log.Printf("couldn't show version: %v", err) From 9d158e25f8a25a3b119657dfdc8ad5829a11bc2b Mon Sep 17 00:00:00 2001 From: Daniel Bos Date: Thu, 30 Jul 2026 11:26:03 +0800 Subject: [PATCH 5/5] fix: don't panic when config is not found --- main.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 99e81cd..fb43c34 100644 --- a/main.go +++ b/main.go @@ -32,15 +32,27 @@ import ( ) func newRootCommand() *cobra.Command { + conf, err := config.ReadConfig() + if err != nil && !os.IsNotExist(err) { + log.Fatalf("couldn't read config: %v", err) + } + //nolint:exhaustruct root := &cobra.Command{ Use: os.Args[0], Short: "A command line tool to maintain Architecture Decision Records", - } + PersistentPreRunE: func(c *cobra.Command, _ []string) error { + switch c.Name() { + case "init", "version", "help", "completion": + return nil + } - conf, err := config.ReadConfig() - if err != nil && !os.IsNotExist(err) { - log.Printf("couldn't read config: %v", err) + if conf == nil { + log.Fatal("no ADR configuration found, run 'adr init' first") + } + + return nil + }, } root.AddCommand(cmd.AdrCommands(conf)...)