// Package cli implements the vellum command-line subcommands that operate // directly on the database: user administration and backups. // // cmd/vellum calls Run before booting the HTTP server. When Run reports // handled == true the process exits without starting the server; when it // reports handled == false the caller falls through to its default behaviour. package cli import ( "context" "database/sql" "errors" "fmt" "io" "log/slog" "os" "github.com/mithral/vellum/internal/auth" "github.com/mithral/vellum/internal/core" "github.com/mithral/vellum/migrations" ) // Run dispatches a CLI subcommand. args is os.Args[1:]. // // handled is false when args names no known subcommand, so the caller can // continue with its normal startup. When handled is true the database has been // opened and migrated, and a non-nil error is fatal to the invocation. func Run(args []string, cfg core.Config, log *slog.Logger) (handled bool, err error) { return run(context.Background(), args, cfg, log, os.Stdout) } // run is the testable core of Run. It opens and migrates the database only // once a subcommand has actually been matched, so an unrecognised argument // leaves the database untouched. func run(ctx context.Context, args []string, cfg core.Config, log *slog.Logger, out io.Writer) (bool, error) { if len(args) == 0 { return false, nil } switch args[0] { case "user", "backup": default: return false, nil } db, err := core.OpenDB(cfg.DBPath) if err != nil { return true, err } defer db.Close() if err := core.Migrate(ctx, db, migrations.FS, log); err != nil { return true, err } switch args[0] { case "user": return true, userCmd(ctx, db, args[1:], out) case "backup": return true, backupCmd(ctx, db, args[1:], out) } return false, nil } // ── user ─────────────────────────────────────────────────────────────── func userCmd(ctx context.Context, db *sql.DB, args []string, out io.Writer) error { if len(args) == 0 { return errors.New("user: missing subcommand (add, passwd, list)") } switch args[0] { case "add": return userAdd(ctx, db, args[1:], out) case "passwd": return userPasswd(ctx, db, args[1:], out) case "list": return userList(ctx, db, out) default: return fmt.Errorf("user: unknown subcommand %q", args[0]) } } func userAdd(ctx context.Context, db *sql.DB, args []string, out io.Writer) error { admin := false var positional []string for _, a := range args { if a == "--admin" { admin = true continue } positional = append(positional, a) } if len(positional) != 2 { return errors.New("usage: user add [--admin]") } u, err := auth.CreateUser(ctx, db, positional[0], "", positional[1], admin) if err != nil { return err } _, err = fmt.Fprintf(out, "created user %d %s\n", u.ID, u.Username) return err } func userPasswd(ctx context.Context, db *sql.DB, args []string, out io.Writer) error { if len(args) != 2 { return errors.New("usage: user passwd ") } username, password := args[0], args[1] u, err := auth.GetUserByUsername(ctx, db, username) if errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("user %q not found", username) } if err != nil { return err } if err := auth.SetPassword(ctx, db, u.ID, password); err != nil { return err } _, err = fmt.Fprintf(out, "updated password for user %d %s\n", u.ID, u.Username) return err } func userList(ctx context.Context, db *sql.DB, out io.Writer) error { users, err := auth.ListUsers(ctx, db) if err != nil { return err } for _, u := range users { if _, err := fmt.Fprintf(out, "%d\t%s\tadmin=%t\n", u.ID, u.Username, u.IsAdmin); err != nil { return err } } return nil } // ── backup ───────────────────────────────────────────────────────────── func backupCmd(ctx context.Context, db *sql.DB, args []string, out io.Writer) error { if len(args) != 1 { return errors.New("usage: backup ") } path := args[0] if _, err := db.ExecContext(ctx, "VACUUM INTO ?", path); err != nil { return fmt.Errorf("backup: %w", err) } _, err := fmt.Fprintf(out, "backed up database to %s\n", path) return err }