Dev seed now issues write tokens and writes local taskmaster meta under TASK_MASTER_HOME, so make run-dev can sync without manual setup. Co-authored-by: Cursor <cursoragent@cursor.com>
193 lines
4.7 KiB
Go
193 lines
4.7 KiB
Go
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: <dataDir>/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.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 <dataDir>/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
|
|
`)
|
|
}
|