diff --git a/tools/nidusctl/main.go b/tools/nidusctl/main.go index f8ded83..b9b7271 100644 --- a/tools/nidusctl/main.go +++ b/tools/nidusctl/main.go @@ -8,7 +8,6 @@ package main import ( - "flag" "fmt" "os" "path/filepath" @@ -23,18 +22,12 @@ func main() { } func run(args []string) int { - fs := flag.NewFlagSet("nidusctl", flag.ContinueOnError) - cfgPath := fs.String("config", "config.yaml", "path to configuration file") - if err := fs.Parse(args); err != nil { - return 2 - } - rest := fs.Args() - if len(rest) < 1 { + if len(args) < 1 { usage() return 2 } - cfg, err := config.Load(*cfgPath) + cfg, err := config.Load() if err != nil { fmt.Fprintf(os.Stderr, "error loading config: %v\n", err) return 1 @@ -54,20 +47,20 @@ func run(args []string) int { return 1 } - switch rest[0] { + switch args[0] { case "user": - return runUser(dbase, rest[1:]) + return runUser(dbase, args[1:]) case "calendar", "cal": - return runCalendar(dbase, st, rest[1:]) + return runCalendar(dbase, st, args[1:]) case "addressbook", "card": - return runAddressBook(dbase, st, rest[1:]) + return runAddressBook(dbase, st, args[1:]) case "migrate": - return runMigrate(st, rest[1:]) + return runMigrate(st, args[1:]) case "help", "-h", "--help": usage() return 0 default: - fmt.Fprintf(os.Stderr, "unknown command %q\n\n", rest[0]) + fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0]) usage() return 2 } @@ -77,43 +70,41 @@ func usage() { fmt.Fprint(os.Stderr, `nidusctl - manage nidus DAV server users, resources, and sharing grants Usage: - nidusctl [-config config.yaml] user create [--display-name NAME] [--email EMAIL] [--password PW] - nidusctl [-config config.yaml] user delete - nidusctl [-config config.yaml] user list - nidusctl [-config config.yaml] user passwd [--password PW] + nidusctl user create [--display-name NAME] [--email EMAIL] [--password PW] + nidusctl user delete + nidusctl user list + nidusctl user passwd [--password PW] - nidusctl [-config config.yaml] calendar create [--color '#RRGGBB'] - nidusctl [-config config.yaml] calendar color - nidusctl [-config config.yaml] calendar delete - nidusctl [-config config.yaml] calendar list - nidusctl [-config config.yaml] calendar share - nidusctl [-config config.yaml] calendar unshare - nidusctl [-config config.yaml] calendar shares + nidusctl calendar create [--color '#RRGGBB'] + nidusctl calendar color + nidusctl calendar delete + nidusctl calendar list + nidusctl calendar share + nidusctl calendar unshare + nidusctl calendar shares - nidusctl [-config config.yaml] addressbook create - nidusctl [-config config.yaml] addressbook delete - nidusctl [-config config.yaml] addressbook list - nidusctl [-config config.yaml] addressbook share - nidusctl [-config config.yaml] addressbook unshare - nidusctl [-config config.yaml] addressbook shares + nidusctl addressbook create + nidusctl addressbook delete + nidusctl addressbook list + nidusctl addressbook share + nidusctl addressbook unshare + nidusctl addressbook shares - nidusctl [-config config.yaml] migrate [--verbose] + nidusctl migrate [--verbose] + + nidusctl help + +Configuration is done via environment variables: + NIDUS_DATA_DIR - data directory (default: ./data) Examples: nidusctl user create alice --display-name "Alice Smith" --email alice@example.com nidusctl calendar create alice work - nidusctl calendar share alice work bob write - nidusctl calendar shares alice work - nidusctl calendar unshare alice work bob - nidusctl migrate - `) -} - -// newFlagSet creates a flag.FlagSet configured for subcommand parsing -// (flags may appear before or after positional args, since callers parse -// flags first with fs.Parse then read fs.Args() for the rest). -func newFlagSet(name string) *flag.FlagSet { - return flag.NewFlagSet(name, flag.ContinueOnError) + nidusctl calendar share alice work bob write + nidusctl calendar shares alice work + nidusctl calendar unshare alice work bob + nidusctl migrate +`) } func runCalendar(dbase *db.DB, st *store.Store, args []string) int { @@ -123,17 +114,17 @@ func runCalendar(dbase *db.DB, st *store.Store, args []string) int { } switch args[0] { case "create": - fs := newFlagSet("calendar create") - color := fs.String("color", "", "hex color like #3b82f6 (optional)") - if err := fs.Parse(args[1:]); err != nil { - return 2 + color := "" + if len(args) > 2 && args[1] == "--color" { + color = args[2] + args = args[:1] } - if fs.NArg() != 2 { + if len(args) != 3 { fmt.Fprintln(os.Stderr, "usage: nidusctl calendar create [--color '#RRGGBB']") return 2 } - owner, calName := fs.Arg(0), fs.Arg(1) - if err := dbase.CreateCalendarWithColor(owner, calName, *color); err != nil { + owner, calName := args[1], args[2] + if err := dbase.CreateCalendarWithColor(owner, calName, color); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return 1 } @@ -365,11 +356,7 @@ func runAddressBook(dbase *db.DB, st *store.Store, args []string) int { } func runMigrate(st *store.Store, args []string) int { - fs := newFlagSet("migrate") - if err := fs.Parse(args); err != nil { - return 2 - } - + // Ignore args for now (could add --verbose flag in future if needed) if err := st.Migrate(); err != nil { fmt.Fprintf(os.Stderr, "migration failed: %v\n", err) return 1 diff --git a/tools/nidusctl/main_test.go b/tools/nidusctl/main_test.go index 8f1e55f..389d7f7 100644 --- a/tools/nidusctl/main_test.go +++ b/tools/nidusctl/main_test.go @@ -12,25 +12,6 @@ import ( "git.arnef.de/arnef/nidus/internal/db" ) -// writeTestConfig creates a minimal config.yaml in dir and returns its path. -func writeTestConfig(t *testing.T, dir string) string { - t.Helper() - cfgPath := filepath.Join(dir, "config.yaml") - dataDir := filepath.Join(dir, "data") - content := "storage:\n data_dir: " + dataDir + "\n" + - "users:\n" + - " alice:\n" + - " password: \"$2a$10$9.WEs0uz5TaNJLQbSTLrX.Te.BIe8XTTykVRzZbSHQMEkyFb8Sq/O\"\n" + - " bob:\n" + - " password: \"$2a$10$9.WEs0uz5TaNJLQbSTLrX.Te.BIe8XTTykVRzZbSHQMEkyFb8Sq/O\"\n" - if err := os.WriteFile(cfgPath, []byte(content), 0o644); err != nil { - t.Fatalf("writing test config: %v", err) - } - return cfgPath -} - -// runCLI runs the CLI's run() function, capturing stdout/stderr, and -// returns (exit code, combined stdout+stderr). func runCLI(t *testing.T, args ...string) (int, string) { t.Helper() @@ -56,9 +37,9 @@ func runCLI(t *testing.T, args ...string) (int, string) { func TestCalendarShareUnshareLifecycle(t *testing.T) { dir := t.TempDir() - cfgPath := writeTestConfig(t, dir) + t.Setenv("NIDUS_DATA_DIR", filepath.Join(dir, "data")) - code, out := runCLI(t, "-config", cfgPath, "calendar", "share", "alice", "work", "bob", "write") + code, out := runCLI(t, "calendar", "share", "alice", "work", "bob", "write") if code != 0 { t.Fatalf("share exit code = %d, output: %s", code, out) } @@ -66,7 +47,7 @@ func TestCalendarShareUnshareLifecycle(t *testing.T) { t.Errorf("output = %q, want to contain 'shared'", out) } - code, out = runCLI(t, "-config", cfgPath, "calendar", "shares", "alice", "work") + code, out = runCLI(t, "calendar", "shares", "alice", "work") if code != 0 { t.Fatalf("shares exit code = %d, output: %s", code, out) } @@ -74,12 +55,12 @@ func TestCalendarShareUnshareLifecycle(t *testing.T) { t.Errorf("output = %q, want to contain bob/write", out) } - code, out = runCLI(t, "-config", cfgPath, "calendar", "unshare", "alice", "work", "bob") + code, out = runCLI(t, "calendar", "unshare", "alice", "work", "bob") if code != 0 { t.Fatalf("unshare exit code = %d, output: %s", code, out) } - code, out = runCLI(t, "-config", cfgPath, "calendar", "shares", "alice", "work") + code, out = runCLI(t, "calendar", "shares", "alice", "work") if code != 0 { t.Fatalf("shares (after unshare) exit code = %d, output: %s", code, out) } @@ -90,9 +71,9 @@ func TestCalendarShareUnshareLifecycle(t *testing.T) { func TestCalendarShareInvalidPermission(t *testing.T) { dir := t.TempDir() - cfgPath := writeTestConfig(t, dir) + t.Setenv("NIDUS_DATA_DIR", filepath.Join(dir, "data")) - code, out := runCLI(t, "-config", cfgPath, "calendar", "share", "alice", "work", "bob", "admin") + code, out := runCLI(t, "calendar", "share", "alice", "work", "bob", "admin") if code != 2 { t.Errorf("exit code = %d, want 2; output: %s", code, out) } @@ -103,9 +84,9 @@ func TestCalendarShareInvalidPermission(t *testing.T) { func TestCalendarUnshareNotFound(t *testing.T) { dir := t.TempDir() - cfgPath := writeTestConfig(t, dir) + t.Setenv("NIDUS_DATA_DIR", filepath.Join(dir, "data")) - code, out := runCLI(t, "-config", cfgPath, "calendar", "unshare", "alice", "ghost", "bob") + code, out := runCLI(t, "calendar", "unshare", "alice", "ghost", "bob") if code != 1 { t.Errorf("exit code = %d, want 1; output: %s", code, out) } @@ -116,14 +97,14 @@ func TestCalendarUnshareNotFound(t *testing.T) { func TestAddressBookShareUnshareLifecycle(t *testing.T) { dir := t.TempDir() - cfgPath := writeTestConfig(t, dir) + t.Setenv("NIDUS_DATA_DIR", filepath.Join(dir, "data")) - code, out := runCLI(t, "-config", cfgPath, "addressbook", "share", "alice", "contacts", "bob", "read") + code, out := runCLI(t, "addressbook", "share", "alice", "contacts", "bob", "read") if code != 0 { t.Fatalf("share exit code = %d, output: %s", code, out) } - code, out = runCLI(t, "-config", cfgPath, "addressbook", "shares", "alice", "contacts") + code, out = runCLI(t, "addressbook", "shares", "alice", "contacts") if code != 0 { t.Fatalf("shares exit code = %d, output: %s", code, out) } @@ -131,7 +112,7 @@ func TestAddressBookShareUnshareLifecycle(t *testing.T) { t.Errorf("output = %q, want to contain bob/read", out) } - code, out = runCLI(t, "-config", cfgPath, "addressbook", "unshare", "alice", "contacts", "bob") + code, out = runCLI(t, "addressbook", "unshare", "alice", "contacts", "bob") if code != 0 { t.Fatalf("unshare exit code = %d, output: %s", code, out) } @@ -139,9 +120,9 @@ func TestAddressBookShareUnshareLifecycle(t *testing.T) { func TestUnknownUserWarningDoesNotBlockShare(t *testing.T) { dir := t.TempDir() - cfgPath := writeTestConfig(t, dir) + t.Setenv("NIDUS_DATA_DIR", filepath.Join(dir, "data")) - code, out := runCLI(t, "-config", cfgPath, "calendar", "share", "alice", "work", "carol", "read") + code, out := runCLI(t, "calendar", "share", "alice", "work", "carol", "read") if code != 0 { t.Fatalf("exit code = %d, output: %s", code, out) } @@ -154,10 +135,9 @@ func TestUnknownUserWarningDoesNotBlockShare(t *testing.T) { } func TestNoArgsShowsUsage(t *testing.T) { - dir := t.TempDir() // No config needed since usage() is printed before config.Load for // missing subcommands. - code, out := runCLI(t, "-config", filepath.Join(dir, "missing.yaml")) + code, out := runCLI(t) if code != 2 { t.Errorf("exit code = %d, want 2", code) } @@ -168,9 +148,9 @@ func TestNoArgsShowsUsage(t *testing.T) { func TestUnknownCommand(t *testing.T) { dir := t.TempDir() - cfgPath := writeTestConfig(t, dir) + t.Setenv("NIDUS_DATA_DIR", filepath.Join(dir, "data")) - code, out := runCLI(t, "-config", cfgPath, "bogus") + code, out := runCLI(t, "bogus") if code != 2 { t.Errorf("exit code = %d, want 2", code) } diff --git a/tools/nidusctl/users.go b/tools/nidusctl/users.go index 01898fd..361fcc0 100644 --- a/tools/nidusctl/users.go +++ b/tools/nidusctl/users.go @@ -31,31 +31,47 @@ func runUser(dbase *db.DB, args []string) int { } func userCreate(dbase *db.DB, args []string) int { - fs := newFlagSet("nidusctl user create") - displayName := fs.String("display-name", "", "display name shown in DAV clients") - email := fs.String("email", "", "email address") - password := fs.String("password", "", "password (omit to be prompted, recommended)") - if err := fs.Parse(args); err != nil { - return 2 + var displayName, email, password string + var rest []string + + for i := 0; i < len(args); i++ { + switch args[i] { + case "--display-name": + if i+1 < len(args) { + displayName = args[i+1] + i++ + } + case "--email": + if i+1 < len(args) { + email = args[i+1] + i++ + } + case "--password": + if i+1 < len(args) { + password = args[i+1] + i++ + } + default: + rest = append(rest, args[i]) + } } - rest := fs.Args() + if len(rest) != 1 { fmt.Fprintln(os.Stderr, "usage: nidusctl user create [--display-name NAME] [--email EMAIL] [--password PASSWORD]") return 2 } username := rest[0] - pw := *password - if pw == "" { + if password == "" { var err error - pw, err = promptPassword(username) + password, err = promptPassword(username) if err != nil { fmt.Fprintf(os.Stderr, "error reading password: %v\n", err) return 1 } } - if err := dbase.CreateUser(username, pw, *displayName, *email); err != nil { + if err := dbase.CreateUser(username, password, displayName, email); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return 1 } @@ -98,29 +114,37 @@ func userList(dbase *db.DB, args []string) int { } func userPasswd(dbase *db.DB, args []string) int { - fs := newFlagSet("nidusctl user passwd") - password := fs.String("password", "", "new password (omit to be prompted, recommended)") - if err := fs.Parse(args); err != nil { - return 2 + var password string + var rest []string + + for i := 0; i < len(args); i++ { + switch args[i] { + case "--password": + if i+1 < len(args) { + password = args[i+1] + i++ + } + default: + rest = append(rest, args[i]) + } } - rest := fs.Args() + if len(rest) != 1 { fmt.Fprintln(os.Stderr, "usage: nidusctl user passwd [--password PASSWORD]") return 2 } username := rest[0] - pw := *password - if pw == "" { + if password == "" { var err error - pw, err = promptPassword(username) + password, err = promptPassword(username) if err != nil { fmt.Fprintf(os.Stderr, "error reading password: %v\n", err) return 1 } } - if err := dbase.SetPassword(username, pw); err != nil { + if err := dbase.SetPassword(username, password); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return 1 }