package main import ( "flag" "fmt" "net/http" "os" "path/filepath" "strings" "task/internal/config" "task/internal/server" "task/internal/sqlite" "task/internal/version" ) func main() { listen := flag.String("listen", ":8080", "HTTP listen address") dbPath := flag.String("db", "", "SQLite database path (default: /task-master.db)") secure := flag.Bool("secure", false, "Set Secure flag on session cookies") flag.Parse() args := flag.Args() if len(args) > 0 { switch args[0] { case "version", "--version", "-v": fmt.Printf("tm_server %s\n", version.BuildVersion) return case "help", "--help", "-h": printHelp() return case "import-json": if err := runImportJSON(args[1:]); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } return default: fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0]) printHelp() os.Exit(1) } } if err := runServer(*listen, *dbPath, *secure); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } } func openDB(dbPath string) (*sqlite.DB, *config.Config, error) { cfg, err := config.LoadOrCreate() if err != nil { return nil, nil, fmt.Errorf("config: %w", err) } path := dbPath if path == "" { path = filepath.Join(cfg.DataDir, "task-master.db") } secret := []byte(os.Getenv("TASK_MASTER_SECRET")) db, err := sqlite.Open(path, secret) if err != nil { return nil, nil, err } return db, cfg, nil } func bootstrapAdmin(db *sqlite.DB) error { n, err := db.CountUsers() if err != nil { return err } if n > 0 { return nil } user := strings.TrimSpace(os.Getenv("TM_ADMIN_USER")) pass := os.Getenv("TM_ADMIN_PASSWORD") if user == "" || pass == "" { fmt.Fprintln(os.Stderr, "no users in database; set TM_ADMIN_USER and TM_ADMIN_PASSWORD to create the first admin") return nil } u, err := db.CreateUser(user, pass, true) if err != nil { return err } fmt.Fprintf(os.Stderr, "created bootstrap admin %q (%s)\n", u.Username, u.ID) return nil } func runServer(listen, dbPath string, secure bool) error { db, cfg, err := openDB(dbPath) if err != nil { return err } defer db.Close() if err := bootstrapAdmin(db); err != nil { return err } srv := server.New(db) srv.DataDir = cfg.DataDir srv.Secure = secure if strings.EqualFold(os.Getenv("TM_SEED_DEMO"), "1") || strings.EqualFold(os.Getenv("TM_SEED_DEMO"), "true") { if err := srv.SeedDemo(server.SeedDemoOptions{ ClientDataDir: cfg.DataDir, BaseURL: server.SeedBaseURL(listen), }); err != nil { return fmt.Errorf("seed: %w", err) } projects, _ := srv.Projects.List() fmt.Fprintf(os.Stderr, "demo seed ready (%d projects; login admin/admin, alice/alice)\n", len(projects)) } fmt.Fprintf(os.Stderr, "tm_server %s listening on %s\n", version.BuildVersion, listen) return http.ListenAndServe(listen, srv.Handler()) } func runImportJSON(args []string) error { fs := flag.NewFlagSet("import-json", flag.ExitOnError) dataDir := fs.String("data", "", "JSON DataDir to import (default from config)") dbPath := fs.String("db", "", "SQLite database path") owner := fs.String("owner", "", "username to own imported projects") _ = fs.Parse(args) db, cfg, err := openDB(*dbPath) if err != nil { return err } defer db.Close() if err := bootstrapAdmin(db); err != nil { return err } dir := *dataDir if dir == "" { dir = cfg.DataDir } var ownerID string if *owner != "" { u, err := db.GetUserByUsername(*owner) if err != nil { return fmt.Errorf("owner: %w", err) } ownerID = u.ID } else { users, err := db.ListUsers() if err != nil { return err } if len(users) == 0 { return fmt.Errorf("no users; create admin first (TM_ADMIN_USER/TM_ADMIN_PASSWORD)") } for _, u := range users { if u.IsAdmin { ownerID = u.ID break } } if ownerID == "" { ownerID = users[0].ID } } srv := server.New(db) fmt.Fprintf(os.Stderr, "importing from %s …\n", dir) if err := srv.ImportJSONDataDir(dir, ownerID); err != nil { return err } fmt.Fprintln(os.Stderr, "import done") return nil } func printHelp() { fmt.Print(`tm_server — task-master web server Usage: tm_server [flags] tm_server import-json [-data DIR] [-db PATH] [-owner USER] tm_server version tm_server help Flags: -listen ADDR HTTP listen address (default :8080) -db PATH SQLite path (default /task-master.db) -secure Set Secure cookie flag Env: TASK_MASTER_HOME / TASK_MASTER_DATA_DIR same as tm TASK_MASTER_SECRET encrypts remote tokens TM_ADMIN_USER / TM_ADMIN_PASSWORD bootstrap first admin TM_SEED_DEMO=1 seed demo projects + wire TUI (Test, Website Relaunch) TM_SEED_BASE_URL override base URL written into TUI meta.json `) }