diff --git a/.gitignore b/.gitignore
index 6517223e15d4d3953260ae070504d165726e671c..e776f403bc8d3332b94578f4d21b2a61c651080a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@
 *.lzh
 .*.swp
 dupl.html
+unit-test-output
diff --git a/Makefile b/Makefile
index b3047eaa25696707579c1026ba59b313c8cedc7d..623e009bff6b6648bd47839ad23ac31f0f9fed83 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,7 @@
-.PHONY: all
-all:
+.PHONY: all dupl build test docs
+all: build test
+
+build:
 	go generate ./...
 	go install ./...
 
@@ -10,7 +12,16 @@ dupl:
 		> dupl.html
 	xdg-open dupl.html
 
+test:
+	@mkdir -p unit-test-output
+	@go vet ./...
+	@CGO_ENABLED=1 go test -cover -coverpkg=./... -coverprofile=unit-test-output/coverage.out -race ./...
+	@go tool cover -html=unit-test-output/coverage.out
+	@go tool cover -func unit-test-output/coverage.out \
+	  | tail -1 | sed 's/[ \t(:][ \t(:]*/ /g;s/)/:/'
+
 docs:
 	go tool golang.org/x/pkgsite/cmd/pkgsite -open
+
 # vim:ft=make
 #
diff --git a/batch/batch.go b/batch/batch.go
index 0176b7b9095848d6d4d25e2c1e735b2fe152daef..ed2ea56d8ddebe1b58ac945cbaa23d105e1b708b 100644
--- a/batch/batch.go
+++ b/batch/batch.go
@@ -26,10 +26,10 @@ import (
 	"text/template"
 	"time"
 
-	"git.lyda.ie/kevin/bulletin/ask"
-	"git.lyda.ie/kevin/bulletin/key"
-	"git.lyda.ie/kevin/bulletin/storage"
-	"git.lyda.ie/kevin/bulletin/users"
+	"git.lyda.ie/pp/bulletin/ask"
+	"git.lyda.ie/pp/bulletin/key"
+	"git.lyda.ie/pp/bulletin/storage"
+	"git.lyda.ie/pp/bulletin/users"
 	"github.com/adrg/xdg"
 )
 
diff --git a/dclish/dclish.go b/dclish/dclish.go
index ecffd54662d5e3e3f253bbdee5d82f5c272520a6..bdc5000a0b57d329b304b2ce95fed40b73a4b215 100644
--- a/dclish/dclish.go
+++ b/dclish/dclish.go
@@ -33,6 +33,7 @@ commands.
 package dclish
 
 import (
+	"errors"
 	"fmt"
 	"strings"
 	"unicode"
@@ -187,15 +188,13 @@ func (c Commands) run(words []string) error {
 		}
 		switch len(possibles) {
 		case 0:
-			fmt.Printf("ERROR: Unknown command '%s'\n", words[0])
-			return nil
+			return fmt.Errorf("Unknown command '%s'", words[0])
 		case 1:
 			wordup = possibles[0]
 			cmd = c[wordup]
 		default:
-			fmt.Printf("ERROR: Ambiguous command '%s' (matches %s)\n",
+			return fmt.Errorf("Ambiguous command '%s' (matches %s)",
 				words[0], strings.Join(possibles, ", "))
-			return nil
 		}
 	}
 
@@ -203,8 +202,7 @@ func (c Commands) run(words []string) error {
 	if len(cmd.Commands) > 0 {
 		if len(words) == 1 {
 			if cmd.Action == nil {
-				fmt.Printf("ERROR: missing subcommand for %s.\n", wordup)
-				return nil
+				return fmt.Errorf("missing subcommand for %s", wordup)
 			}
 			return cmd.Action(cmd)
 		}
@@ -212,8 +210,7 @@ func (c Commands) run(words []string) error {
 	}
 
 	if cmd.Action == nil {
-		fmt.Printf("ERROR: Command not implemented:\n%s\n", cmd.Description)
-		return nil
+		return fmt.Errorf("Command not implemented:\n%s", cmd.Description)
 	}
 	for flg := range cmd.Flags {
 		cmd.Flags[flg].Value = cmd.Flags[flg].Default
@@ -223,8 +220,7 @@ func (c Commands) run(words []string) error {
 
 	if len(words) == 1 {
 		if len(cmd.Args) < cmd.MinArgs {
-			fmt.Println("ERROR: Not enough args.")
-			return nil
+			return errors.New("Not enough args")
 		}
 		return cmd.Action(cmd)
 	}
@@ -244,18 +240,15 @@ func (c Commands) run(words []string) error {
 				wordup = strings.Replace(wordup, "/NO", "/", 1)
 				flg, ok = cmd.Flags[wordup]
 				if !ok {
-					fmt.Printf("ERROR: Flag '%s' not recognised.\n", args[i])
-					return nil
+					return fmt.Errorf("Flag '%s' not recognised", args[i])
 				}
 				toggleValue = "false"
 			}
 			if !flg.OptArg && assigned {
-				fmt.Printf("ERROR: Flag '%s' is a toggle.\n", args[i])
-				return nil
+				return fmt.Errorf("Flag '%s' is a toggle", args[i])
 			}
 			if flg.Set {
-				fmt.Printf("ERROR: Flag '%s' is already set.\n", args[i])
-				return nil
+				return fmt.Errorf("Flag '%s' is already set", args[i])
 			}
 			flg.Set = true
 			if flg.OptArg {
@@ -267,15 +260,13 @@ func (c Commands) run(words []string) error {
 			}
 		} else {
 			if len(cmd.Args) == cmd.MaxArgs {
-				fmt.Printf("ERROR: Too many args at '%s'\n", args[i])
-				return nil
+				return fmt.Errorf("Too many args at '%s'", args[i])
 			}
 			cmd.Args = append(cmd.Args, args[i])
 		}
 	}
 	if len(cmd.Args) < cmd.MinArgs {
-		fmt.Println("ERROR: Not enough args.")
-		return nil
+		return errors.New("Not enough args")
 	}
 	return cmd.Action(cmd)
 }
diff --git a/folders/folders.go b/folders/folders.go
index 2b907efc98d2db2d8de28f951b392b6e0c703e6e..b378593fc3201c8aec2a86a8325bb9019237bc07 100644
--- a/folders/folders.go
+++ b/folders/folders.go
@@ -12,8 +12,8 @@ import (
 	"errors"
 	"strings"
 
-	"git.lyda.ie/kevin/bulletin/storage"
-	"git.lyda.ie/kevin/bulletin/this"
+	"git.lyda.ie/pp/bulletin/storage"
+	"git.lyda.ie/pp/bulletin/this"
 )
 
 // ValidFolder validates the folder name for this user.
diff --git a/folders/messages.go b/folders/messages.go
index 34d1a70bb62815adeceaecfc3f56c3f267dacce7..9355d7d8170c66766b21a216ffdc55f5b04ce056 100644
--- a/folders/messages.go
+++ b/folders/messages.go
@@ -4,8 +4,8 @@ import (
 	"errors"
 	"time"
 
-	"git.lyda.ie/kevin/bulletin/storage"
-	"git.lyda.ie/kevin/bulletin/this"
+	"git.lyda.ie/pp/bulletin/storage"
+	"git.lyda.ie/pp/bulletin/this"
 )
 
 // CreateMessage creates a new folder.
diff --git a/folders/users.go b/folders/users.go
index b685ccb9039c94b231524cb56da0d1efa8882f16..6ed12a8809b1c4c199b40ee96e2f95faf209e13d 100644
--- a/folders/users.go
+++ b/folders/users.go
@@ -3,8 +3,8 @@ package folders
 import (
 	"strings"
 
-	"git.lyda.ie/kevin/bulletin/storage"
-	"git.lyda.ie/kevin/bulletin/this"
+	"git.lyda.ie/pp/bulletin/storage"
+	"git.lyda.ie/pp/bulletin/this"
 )
 
 // GetUser gets a user.
diff --git a/go.mod b/go.mod
index 92a4473711ae56605afe70b7c29ef2dc3ba5eeed..508aa6114f18a8ecdc21d7e1603bcffbe2d2aa48 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module git.lyda.ie/kevin/bulletin
+module git.lyda.ie/pp/bulletin
 
 go 1.24.2
 
@@ -10,6 +10,7 @@ require (
 	github.com/golang-migrate/migrate/v4 v4.18.3
 	github.com/jmoiron/sqlx v1.4.0
 	github.com/rivo/tview v0.0.0-20250501113434-0c592cd31026
+	github.com/stretchr/testify v1.10.0
 	github.com/urfave/cli/v3 v3.3.3
 	golang.org/x/crypto v0.38.0
 	golang.org/x/sys v0.33.0
@@ -21,15 +22,20 @@ require (
 	cel.dev/expr v0.23.1 // indirect
 	filippo.io/edwards25519 v1.1.0 // indirect
 	github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
+	github.com/bitfield/gotestdox v0.2.2 // indirect
 	github.com/cubicdaiya/gonp v1.0.4 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/dnephin/pflag v1.0.7 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/fatih/color v1.18.0 // indirect
 	github.com/fatih/structtag v1.2.0 // indirect
+	github.com/fsnotify/fsnotify v1.8.0 // indirect
 	github.com/gdamore/encoding v1.0.1 // indirect
 	github.com/go-sql-driver/mysql v1.9.2 // indirect
 	github.com/google/cel-go v0.25.0 // indirect
 	github.com/google/licensecheck v0.3.1 // indirect
 	github.com/google/safehtml v0.0.3-0.20211026203422-d6f0e11a5516 // indirect
+	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -41,6 +47,7 @@ require (
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/kr/pretty v0.3.1 // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/mibk/dupl v1.0.0 // indirect
@@ -50,6 +57,7 @@ require (
 	github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect
 	github.com/pingcap/log v1.1.0 // indirect
 	github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/riza-io/grpc-go v0.2.0 // indirect
@@ -76,6 +84,7 @@ require (
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
+	gotest.tools/gotestsum v1.12.2 // indirect
 	modernc.org/libc v1.65.8 // indirect
 	modernc.org/mathutil v1.7.1 // indirect
 	modernc.org/memory v1.11.0 // indirect
@@ -83,19 +92,8 @@ require (
 )
 
 tool (
-	git.lyda.ie/kevin/bulletin
-	git.lyda.ie/kevin/bulletin/ask
-	git.lyda.ie/kevin/bulletin/batch
-	git.lyda.ie/kevin/bulletin/dclish
-	git.lyda.ie/kevin/bulletin/editor
-	git.lyda.ie/kevin/bulletin/folders
-	git.lyda.ie/kevin/bulletin/key
-	git.lyda.ie/kevin/bulletin/pager
-	git.lyda.ie/kevin/bulletin/repl
-	git.lyda.ie/kevin/bulletin/storage
-	git.lyda.ie/kevin/bulletin/this
-	git.lyda.ie/kevin/bulletin/users
 	github.com/mibk/dupl
 	github.com/sqlc-dev/sqlc/cmd/sqlc
 	golang.org/x/pkgsite/cmd/pkgsite
+	gotest.tools/gotestsum
 )
diff --git a/go.sum b/go.sum
index 11237019470f4b464920da9fc28cf955da1c1aa2..ef09851b8e8ead2a3c8962431ecea27e676dc836 100644
--- a/go.sum
+++ b/go.sum
@@ -8,6 +8,8 @@ github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ
 github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
 github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/bitfield/gotestdox v0.2.2 h1:x6RcPAbBbErKLnapz1QeAlf3ospg8efBsedU93CDsnE=
+github.com/bitfield/gotestdox v0.2.2/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY=
 github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
 github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8=
 github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
@@ -23,10 +25,16 @@ github.com/cubicdaiya/gonp v1.0.4/go.mod h1:iWGuP/7+JVTn02OWhRemVbMmG1DOUnmrGTYY
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk=
+github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
 github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
 github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
+github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
+github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
 github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
 github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
@@ -55,6 +63,8 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k
 github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
 github.com/google/safehtml v0.0.3-0.20211026203422-d6f0e11a5516 h1:pSEdbeokt55L2hwtWo6A2k7u5SG08rmw0LhWEyrdWgk=
 github.com/google/safehtml v0.0.3-0.20211026203422-d6f0e11a5516/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -87,6 +97,9 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
@@ -150,6 +163,8 @@ github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07/go.mod h1:Ak17
 github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4=
 github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
+github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
 go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
@@ -222,6 +237,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -289,6 +305,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/gotestsum v1.12.2 h1:eli4tu9Q2D/ogDsEGSr8XfQfl7mT0JsGOG6DFtUiZ/Q=
+gotest.tools/gotestsum v1.12.2/go.mod h1:kjRtCglPZVsSU0hFHX3M5VWBM6Y63emHuB14ER1/sow=
 modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
 modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
 modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
diff --git a/main.go b/main.go
index eee4c466d3f417ad8210d7806d7b9bcc270ebfc0..822ea551f426c3a36a84b8747143c0deb2fc00cd 100644
--- a/main.go
+++ b/main.go
@@ -24,9 +24,9 @@ import (
 	"os"
 	"strings"
 
-	"git.lyda.ie/kevin/bulletin/batch"
-	"git.lyda.ie/kevin/bulletin/repl"
-	"git.lyda.ie/kevin/bulletin/this"
+	"git.lyda.ie/pp/bulletin/batch"
+	"git.lyda.ie/pp/bulletin/repl"
+	"git.lyda.ie/pp/bulletin/this"
 
 	"github.com/urfave/cli/v3"
 )
diff --git a/repl/accounts.go b/repl/accounts.go
index 88866bbd2c6bac6c72137dc466e0e4e9acf2ab1e..46aa5ead7a8640436960ffb657d5c0559a6d279e 100644
--- a/repl/accounts.go
+++ b/repl/accounts.go
@@ -5,12 +5,12 @@ import (
 	"fmt"
 	"strings"
 
-	"git.lyda.ie/kevin/bulletin/ask"
-	"git.lyda.ie/kevin/bulletin/dclish"
-	"git.lyda.ie/kevin/bulletin/key"
-	"git.lyda.ie/kevin/bulletin/storage"
-	"git.lyda.ie/kevin/bulletin/this"
-	"git.lyda.ie/kevin/bulletin/users"
+	"git.lyda.ie/pp/bulletin/ask"
+	"git.lyda.ie/pp/bulletin/dclish"
+	"git.lyda.ie/pp/bulletin/key"
+	"git.lyda.ie/pp/bulletin/storage"
+	"git.lyda.ie/pp/bulletin/this"
+	"git.lyda.ie/pp/bulletin/users"
 )
 
 // ActionUser handles the USER command - it prints out help for all the
diff --git a/repl/all_test.go b/repl/all_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..3cb9497e691b438284e06d6e078078744b8212cf
--- /dev/null
+++ b/repl/all_test.go
@@ -0,0 +1,29 @@
+package repl
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	_ "modernc.org/sqlite" // db engine.
+
+	"git.lyda.ie/pp/bulletin/storage"
+)
+
+func TestRepl(t *testing.T) {
+	a := assert.New(t)
+	r := require.New(t)
+
+	storage.ConfigureTestDB(t)
+	defer storage.RemoveTestDB()
+	db, err := storage.Open()
+	a.NotNil(db)
+	a.Nil(err)
+
+	err = commands.ParseAndRun("BADCOMMAND")
+	r.NotNil(err)
+	a.Equal("Unknown command 'BADCOMMAND'", err.Error(), "Unexpected error.")
+
+	//err = commands.ParseAndRun("INDEX")
+	//.Nil(err)
+}
diff --git a/repl/command.go b/repl/command.go
index e0949dbacba4eda64a2961188b49b7fea9398450..d9370cd405efb7d9ceeb613ebd2e1da4dfe226c3 100644
--- a/repl/command.go
+++ b/repl/command.go
@@ -1,6 +1,6 @@
 package repl
 
-import "git.lyda.ie/kevin/bulletin/dclish"
+import "git.lyda.ie/pp/bulletin/dclish"
 
 var commands = dclish.Commands{
 	"ADD": {
diff --git a/repl/folders.go b/repl/folders.go
index 3b8160ae2202c819932daf50a6b90ab0e4cdc8fe..7ad4d9de56765f64e10850cb79fbe23f70795f39 100644
--- a/repl/folders.go
+++ b/repl/folders.go
@@ -6,11 +6,11 @@ import (
 	"strconv"
 	"strings"
 
-	"git.lyda.ie/kevin/bulletin/ask"
-	"git.lyda.ie/kevin/bulletin/dclish"
-	"git.lyda.ie/kevin/bulletin/folders"
-	"git.lyda.ie/kevin/bulletin/storage"
-	"git.lyda.ie/kevin/bulletin/this"
+	"git.lyda.ie/pp/bulletin/ask"
+	"git.lyda.ie/pp/bulletin/dclish"
+	"git.lyda.ie/pp/bulletin/folders"
+	"git.lyda.ie/pp/bulletin/storage"
+	"git.lyda.ie/pp/bulletin/this"
 )
 
 // ActionIndex handles the `INDEX` command.  This lists all the folders.
diff --git a/repl/help.go b/repl/help.go
index d868081a7645df581101638c42b770f51e373b54..476c6fa589c901584538de732418d8577772acae 100644
--- a/repl/help.go
+++ b/repl/help.go
@@ -5,8 +5,8 @@ import (
 	"sort"
 	"strings"
 
-	"git.lyda.ie/kevin/bulletin/dclish"
-	"git.lyda.ie/kevin/bulletin/pager"
+	"git.lyda.ie/pp/bulletin/dclish"
+	"git.lyda.ie/pp/bulletin/pager"
 )
 
 var helpmap = map[string]string{
diff --git a/repl/mail.go b/repl/mail.go
index 643c80096e24bc5ac8af484086fd1e358b158eb0..5b936669e955ec20c029b07218ea60635f97f5ce 100644
--- a/repl/mail.go
+++ b/repl/mail.go
@@ -3,7 +3,7 @@ package repl
 import (
 	"errors"
 
-	"git.lyda.ie/kevin/bulletin/dclish"
+	"git.lyda.ie/pp/bulletin/dclish"
 )
 
 // ActionForward handles the `FORWARD` command.
diff --git a/repl/messages.go b/repl/messages.go
index db9cccbcfe97f01c3acc03d7b852493c5b4104a3..eafe02d10da420b808bbdcfd18b64862d28d0e04 100644
--- a/repl/messages.go
+++ b/repl/messages.go
@@ -7,13 +7,13 @@ import (
 	"strings"
 	"time"
 
-	"git.lyda.ie/kevin/bulletin/ask"
-	"git.lyda.ie/kevin/bulletin/dclish"
-	"git.lyda.ie/kevin/bulletin/editor"
-	"git.lyda.ie/kevin/bulletin/folders"
-	"git.lyda.ie/kevin/bulletin/pager"
-	"git.lyda.ie/kevin/bulletin/storage"
-	"git.lyda.ie/kevin/bulletin/this"
+	"git.lyda.ie/pp/bulletin/ask"
+	"git.lyda.ie/pp/bulletin/dclish"
+	"git.lyda.ie/pp/bulletin/editor"
+	"git.lyda.ie/pp/bulletin/folders"
+	"git.lyda.ie/pp/bulletin/pager"
+	"git.lyda.ie/pp/bulletin/storage"
+	"git.lyda.ie/pp/bulletin/this"
 )
 
 // ActionDirectory handles the `DIRECTORY` command.  This lists all the
diff --git a/repl/misc.go b/repl/misc.go
index 498f4d2b9687a96dabe8d8823dd9ec6b8714dca9..dd84befe85223d2ac4fe0fa9d323bb96676b534e 100644
--- a/repl/misc.go
+++ b/repl/misc.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"os"
 
-	"git.lyda.ie/kevin/bulletin/dclish"
+	"git.lyda.ie/pp/bulletin/dclish"
 )
 
 // ActionQuit handles the `QUIT` command.  This exits BULLETIN.
diff --git a/repl/repl.go b/repl/repl.go
index 3756497c4bbcf068a4ab3ddc2123d85d8fa50d26..0c45455a70f8a724b5d8c3a5a92c7c5b4a07a3ab 100644
--- a/repl/repl.go
+++ b/repl/repl.go
@@ -9,8 +9,8 @@ import (
 	"strings"
 	"unicode"
 
-	"git.lyda.ie/kevin/bulletin/dclish"
-	"git.lyda.ie/kevin/bulletin/this"
+	"git.lyda.ie/pp/bulletin/dclish"
+	"git.lyda.ie/pp/bulletin/this"
 	"github.com/adrg/xdg"
 	"github.com/chzyer/readline"
 )
diff --git a/repl/set.go b/repl/set.go
index 581d286d9b89284136b5c9f7dcb801cfd2c95d2b..14e30138e02c90a75783d195515f013ca2be3c38 100644
--- a/repl/set.go
+++ b/repl/set.go
@@ -5,10 +5,10 @@ import (
 	"fmt"
 	"strconv"
 
-	"git.lyda.ie/kevin/bulletin/dclish"
-	"git.lyda.ie/kevin/bulletin/folders"
-	"git.lyda.ie/kevin/bulletin/storage"
-	"git.lyda.ie/kevin/bulletin/this"
+	"git.lyda.ie/pp/bulletin/dclish"
+	"git.lyda.ie/pp/bulletin/folders"
+	"git.lyda.ie/pp/bulletin/storage"
+	"git.lyda.ie/pp/bulletin/this"
 )
 
 func setAlert(cmd *dclish.Command, alert int64) error {
diff --git a/repl/show.go b/repl/show.go
index 8d59e5914171eb6f73cd899462b4f69593cfadf9..d3511b267bad4f3aca9e79fdf6a52b35444a4061 100644
--- a/repl/show.go
+++ b/repl/show.go
@@ -6,11 +6,11 @@ import (
 
 	"github.com/carlmjohnson/versioninfo"
 
-	"git.lyda.ie/kevin/bulletin/ask"
-	"git.lyda.ie/kevin/bulletin/dclish"
-	"git.lyda.ie/kevin/bulletin/folders"
-	"git.lyda.ie/kevin/bulletin/storage"
-	"git.lyda.ie/kevin/bulletin/this"
+	"git.lyda.ie/pp/bulletin/ask"
+	"git.lyda.ie/pp/bulletin/dclish"
+	"git.lyda.ie/pp/bulletin/folders"
+	"git.lyda.ie/pp/bulletin/storage"
+	"git.lyda.ie/pp/bulletin/this"
 )
 
 // ActionShow handles the `SHOW` command.
diff --git a/repl/xfer.go b/repl/xfer.go
index ebd2cf2436567ff3506f22615abfa2841d8760d1..94b9be0ae11d0a8e516135fcaeeba9b424e5ac12 100644
--- a/repl/xfer.go
+++ b/repl/xfer.go
@@ -3,8 +3,8 @@ package repl
 import (
 	"fmt"
 
-	"git.lyda.ie/kevin/bulletin/dclish"
-	"git.lyda.ie/kevin/bulletin/this"
+	"git.lyda.ie/pp/bulletin/dclish"
+	"git.lyda.ie/pp/bulletin/this"
 )
 
 // ActionCopy handles the `COPY` command.
diff --git a/storage/connection.go b/storage/connection.go
index d4c9eb5669f2a51cc5fdaffb246a32ed9c1fb0bb..6ab55a45cc327ba8e7d0b819a25d3ad7aafcddef 100644
--- a/storage/connection.go
+++ b/storage/connection.go
@@ -2,10 +2,12 @@ package storage
 
 import (
 	"context"
+	"database/sql"
 	"embed"
 	"errors"
 	"os"
 	"path"
+	"strings"
 
 	// Included to connect to sqlite.
 	_ "github.com/golang-migrate/migrate/v4/database/sqlite"
@@ -20,15 +22,53 @@ import (
 //go:embed migrations/*.sql
 var migrationsFS embed.FS
 
+var dbfile string
+
+// ConfigureTestDBParam has the test functions we need.
+type ConfigureTestDBParam interface {
+	Helper()
+	Fatalf(string, ...any)
+}
+
+// ConfigureTestDB configures a test db.
+func ConfigureTestDB(t ConfigureTestDBParam) {
+	t.Helper()
+	tmpFile, err := os.CreateTemp("", "bulletin-test-*.db")
+	if err != nil {
+		t.Fatalf("failed to create temp file: %v", err)
+	}
+	tmpFile.Close()
+
+	dbfile = tmpFile.Name()
+	db, err := sql.Open("sqlite", dbfile)
+	if err != nil {
+		os.Remove(dbfile)
+		t.Fatalf("failed to open sqlite db: %v", err)
+	}
+	if err := db.Close(); err != nil {
+		os.Remove(dbfile)
+		t.Fatalf("failed to close sqlite db: %v", err)
+	}
+}
+
+// RemoveTestDB configures a test db.
+func RemoveTestDB() {
+	if strings.Contains(dbfile, "bulletin-test-") {
+		os.Remove(dbfile)
+	}
+}
+
 // Open opens the bulletin database.
 func Open() (*sqlx.DB, error) {
-	// Determine path names and create components.
-	bulldir := path.Join(xdg.DataHome, "BULLETIN")
-	err := os.MkdirAll(bulldir, 0700)
-	if err != nil {
-		return nil, errors.New("bulletin directory problem")
+	if dbfile == "" {
+		// Determine path names and create components.
+		bulldir := path.Join(xdg.DataHome, "BULLETIN")
+		err := os.MkdirAll(bulldir, 0700)
+		if err != nil {
+			return nil, errors.New("bulletin directory problem")
+		}
+		dbfile = path.Join(bulldir, "bulletin.db")
 	}
-	dbfile := path.Join(bulldir, "bulletin.db")
 
 	// Run db migrations if needed.
 	migrations, err := iofs.New(migrationsFS, "migrations")
@@ -49,7 +89,7 @@ func Open() (*sqlx.DB, error) {
 	// Connect to the db.
 	db, err := sqlx.Connect("sqlite", "file://"+dbfile+"?_pragma=foreign_keys(1)")
 	if err != nil {
-		return nil, errors.New("bulletin database problem")
+		return nil, err
 	}
 
 	// Prepare to watch for schema skew - see version.go.
diff --git a/this/alert.go b/this/alert.go
index 5f405ba9ce0a9d924f8426280a5dd0bd7a884737..f0a7fb5754d08fe662c3a186425d7b6e2bd6b99c 100644
--- a/this/alert.go
+++ b/this/alert.go
@@ -6,8 +6,8 @@ import (
 	"strings"
 	"time"
 
-	"git.lyda.ie/kevin/bulletin/pager"
-	"git.lyda.ie/kevin/bulletin/storage"
+	"git.lyda.ie/pp/bulletin/pager"
+	"git.lyda.ie/pp/bulletin/storage"
 )
 
 // ShowBroadcast print broadcast and shutdown messages.
diff --git a/this/this.go b/this/this.go
index 548bfeea6c5affc19d352c70c1e28d84abc68410..dead7944bb75fdacb6dce211b6f341ba99d269db 100644
--- a/this/this.go
+++ b/this/this.go
@@ -12,9 +12,9 @@ import (
 	"fmt"
 	"strings"
 
-	"git.lyda.ie/kevin/bulletin/ask"
-	"git.lyda.ie/kevin/bulletin/storage"
-	"git.lyda.ie/kevin/bulletin/users"
+	"git.lyda.ie/pp/bulletin/ask"
+	"git.lyda.ie/pp/bulletin/storage"
+	"git.lyda.ie/pp/bulletin/users"
 	"github.com/jmoiron/sqlx"
 )
 
diff --git a/users/users.go b/users/users.go
index d8ac1c8041ec8480fbd35d46471d77a17fb841e7..9438a5d252c1be93bf8b6c6f80c9b6a08b99671a 100644
--- a/users/users.go
+++ b/users/users.go
@@ -6,7 +6,7 @@ import (
 	"fmt"
 	"strings"
 
-	"git.lyda.ie/kevin/bulletin/storage"
+	"git.lyda.ie/pp/bulletin/storage"
 	_ "modernc.org/sqlite" // Loads sqlite driver.
 )