status cmd

This commit is contained in:
2026-01-14 00:00:39 +00:00
parent 00fc6a07a0
commit 00680499ee
2 changed files with 151 additions and 3 deletions

150
main.go
View File

@@ -78,6 +78,7 @@ func defaultCommands() []command {
{name: "dev", description: "run the PocketBase binary locally", action: runDev},
{name: "setup", description: "provision the remote server and install PocketBase", action: runSetup},
{name: "deploy", description: "sync migrations/hooks/static assets (runs setup if needed)", action: runDeploy},
{name: "status", description: "show status for the remote PocketBase service", action: runStatus},
{name: "logs", description: "show PocketBase logs", action: runLogs},
{name: "secrets", description: "manage deployment secrets", action: runSecrets},
}
@@ -897,9 +898,12 @@ const (
totalSetupSteps = 5
remoteWindowSize = 10
remoteIndent = " "
remoteLineColor = "\033[96m"
headerColor = "\033[95m"
localTimeColor = "\033[92m"
remoteLineColor = ""
headerColor = "\033[1;37m"
localTimeColor = "\033[0;32m"
statusActiveColor = "\033[92m"
statusWarnColor = "\033[93m"
statusFailColor = "\033[91m"
remoteColorReset = "\033[0m"
)
@@ -1112,6 +1116,146 @@ func runDeploy() error {
return nil
}
func runStatus() error {
ctx, err := buildDeploymentContext()
if err != nil {
return err
}
defer closeSSHControlMaster(ctx.serverIP)
printInfo("Service", fmt.Sprintf("pb@%s", ctx.serviceName), remoteLineColor)
printInfo("Server", ctx.serverIP, remoteLineColor)
printInfo("Domain", ctx.domain, remoteLineColor)
printInfo("Port", fmt.Sprintf("%d", ctx.port), remoteLineColor)
props, err := querySystemdProperties(ctx)
if err != nil {
return fmt.Errorf("unable to query PocketBase service: %w", err)
}
state := strings.TrimSpace(props["ActiveState"])
if state == "" {
state = "unknown"
}
statusLine := state
if sub := strings.TrimSpace(props["SubState"]); sub != "" {
statusLine = fmt.Sprintf("%s (%s)", state, sub)
}
printInfo("Status", statusLine, statusColorFor(state))
if pid := strings.TrimSpace(props["ExecMainPID"]); pid != "" && pid != "0" {
printInfo("PID", pid, remoteLineColor)
}
if started := strings.TrimSpace(props["ActiveEnterTimestamp"]); started != "" {
printInfo("Active since", started, localTimeColor)
}
if uptime := computeUptime(ctx.serverIP, props["ActiveEnterTimestampMonotonic"]); uptime > 0 {
printInfo("Uptime", formatDuration(uptime), localTimeColor)
}
return nil
}
func printInfo(label, value, valueColor string) {
if valueColor == "" {
valueColor = remoteLineColor
}
fmt.Printf("%s%s:%s %s%s%s\n", headerColor, label, remoteColorReset, valueColor, value, remoteColorReset)
}
func statusColorFor(state string) string {
switch strings.ToLower(state) {
case "active":
return statusActiveColor
case "activating", "deactivating":
return statusWarnColor
case "failed", "inactive":
return statusFailColor
default:
return remoteLineColor
}
}
// querySystemdProperties asks systemd for a few service properties.
func querySystemdProperties(ctx *deploymentContext) (map[string]string, error) {
script := fmt.Sprintf(`set -euo pipefail
systemctl show pb@%s -p ActiveState -p SubState -p ActiveEnterTimestamp -p ActiveEnterTimestampMonotonic -p ExecMainPID
`, ctx.serviceName)
output, err := runSSHCollect(ctx.serverIP, script)
if err != nil {
return nil, err
}
return parseKeyValueLines(output), nil
}
func computeUptime(server, startMicro string) time.Duration {
startMicro = strings.TrimSpace(startMicro)
if startMicro == "" {
return 0
}
start, err := strconv.ParseInt(startMicro, 10, 64)
if err != nil || start <= 0 {
return 0
}
now, err := remoteMonotonicMicro(server)
if err != nil {
fmt.Fprintf(os.Stderr, "warning: unable to compute uptime: %v\n", err)
return 0
}
diff := now - start
if diff <= 0 {
return 0
}
return time.Duration(diff) * time.Microsecond
}
func remoteMonotonicMicro(server string) (int64, error) {
output, err := runSSHOutput(server, `awk '{printf "%d", $1*1000000}' /proc/uptime`)
if err != nil {
return 0, err
}
return strconv.ParseInt(strings.TrimSpace(output), 10, 64)
}
func parseKeyValueLines(input string) map[string]string {
props := make(map[string]string)
scanner := bufio.NewScanner(strings.NewReader(input))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
if idx := strings.Index(line, "="); idx > 0 {
key := strings.TrimSpace(line[:idx])
value := strings.TrimSpace(line[idx+1:])
props[key] = value
}
}
return props
}
func formatDuration(d time.Duration) string {
if d < time.Second {
return d.Round(time.Millisecond).String()
}
parts := make([]string, 0, 3)
if hours := d / time.Hour; hours > 0 {
parts = append(parts, fmt.Sprintf("%dh", hours))
d -= hours * time.Hour
}
if mins := d / time.Minute; mins > 0 {
parts = append(parts, fmt.Sprintf("%dm", mins))
d -= mins * time.Minute
}
seconds := int64(d / time.Second)
if seconds > 0 || len(parts) == 0 {
parts = append(parts, fmt.Sprintf("%ds", seconds))
}
return strings.Join(parts, " ")
}
func runLogs() error {
ctx, err := buildDeploymentContext()
if err != nil {