feat: lodge server, client and admin
This commit is contained in:
commit
95fc50e2c2
84 files changed
+7625
No files matched your search
@@ -0,0 +1,195 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
serviceLabel = "com.kotikot.lodge"
|
||||
systemdUnit = "lodge.service"
|
||||
hookName = "post-checkout"
|
||||
)
|
||||
|
||||
//go:embed skill.md
|
||||
var skill []byte
|
||||
|
||||
var skillDirs = []string{".claude/skills/lodge", ".agents/skills/lodge"}
|
||||
|
||||
func launchdPlist(binary, logFile string) string {
|
||||
return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key><string>%s</string>
|
||||
<key>ProgramArguments</key><array><string>%s</string><string>daemon</string></array>
|
||||
<key>RunAtLoad</key><true/>
|
||||
<key>KeepAlive</key><true/>
|
||||
<key>ProcessType</key><string>Background</string>
|
||||
<key>StandardErrorPath</key><string>%s</string>
|
||||
</dict>
|
||||
</plist>
|
||||
`, serviceLabel, binary, logFile)
|
||||
}
|
||||
|
||||
func systemdService(binary string) string {
|
||||
return fmt.Sprintf(`[Unit]
|
||||
Description=lodge sync daemon
|
||||
|
||||
[Service]
|
||||
ExecStart=%s daemon
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
`, binary)
|
||||
}
|
||||
|
||||
func command(name string, args ...string) error {
|
||||
out, err := exec.Command(name, args...).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %s: %w: %s", name, strings.Join(args, " "), err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func launchdTarget() string {
|
||||
return "gui/" + strconv.Itoa(os.Getuid())
|
||||
}
|
||||
|
||||
func plistPath(home string) string {
|
||||
return filepath.Join(home, "Library", "LaunchAgents", serviceLabel+".plist")
|
||||
}
|
||||
|
||||
func unitPath(home string) string {
|
||||
return filepath.Join(home, ".config", "systemd", "user", systemdUnit)
|
||||
}
|
||||
|
||||
func (c *Client) templateDir() string {
|
||||
return filepath.Join(c.DataDir, "git-template")
|
||||
}
|
||||
|
||||
func (c *Client) Install() error {
|
||||
binary, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.installHook(binary); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, dir := range skillDirs {
|
||||
if err := writeFile(filepath.Join(home, dir, "SKILL.md"), skill, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return startService(home, binary, filepath.Join(c.DataDir, "daemon.log"))
|
||||
}
|
||||
|
||||
func (c *Client) installHook(binary string) error {
|
||||
template, _ := exec.Command("git", "config", "--global", "--get", "init.templateDir").Output()
|
||||
dir := strings.TrimSpace(string(template))
|
||||
if dir == "" {
|
||||
dir = c.templateDir()
|
||||
if err := command("git", "config", "--global", "init.templateDir", dir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if home, err := os.UserHomeDir(); err == nil && strings.HasPrefix(dir, "~/") {
|
||||
dir = filepath.Join(home, dir[2:])
|
||||
}
|
||||
hook := filepath.Join(dir, "hooks", hookName)
|
||||
if existing, err := os.ReadFile(hook); err == nil && !strings.Contains(string(existing), "lodge hook") {
|
||||
fmt.Fprintf(os.Stderr, "lodge: %s already exists, add this line to it yourself: %q hook\n", hook, binary)
|
||||
return nil
|
||||
}
|
||||
script := fmt.Sprintf("#!/bin/sh\n[ -x %q ] && %q hook\nexit 0\n", binary, binary)
|
||||
return writeFile(hook, []byte(script), 0o755)
|
||||
}
|
||||
|
||||
func startService(home, binary, logFile string) error {
|
||||
if runtime.GOOS == "darwin" {
|
||||
plist := plistPath(home)
|
||||
if err := writeFile(plist, []byte(launchdPlist(binary, logFile)), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = command("launchctl", "bootout", launchdTarget()+"/"+serviceLabel)
|
||||
return command("launchctl", "bootstrap", launchdTarget(), plist)
|
||||
}
|
||||
if err := writeFile(unitPath(home), []byte(systemdService(binary)), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := command("systemctl", "--user", "daemon-reload"); err != nil {
|
||||
return err
|
||||
}
|
||||
return command("systemctl", "--user", "enable", "--now", systemdUnit)
|
||||
}
|
||||
|
||||
func restartService() error {
|
||||
if runtime.GOOS == "darwin" {
|
||||
return command("launchctl", "kickstart", "-k", launchdTarget()+"/"+serviceLabel)
|
||||
}
|
||||
return command("systemctl", "--user", "restart", systemdUnit)
|
||||
}
|
||||
|
||||
func (c *Client) Uninstall() error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if runtime.GOOS == "darwin" {
|
||||
_ = command("launchctl", "bootout", launchdTarget()+"/"+serviceLabel)
|
||||
_ = os.Remove(plistPath(home))
|
||||
} else {
|
||||
_ = command("systemctl", "--user", "disable", "--now", systemdUnit)
|
||||
_ = os.Remove(unitPath(home))
|
||||
}
|
||||
template, _ := exec.Command("git", "config", "--global", "--get", "init.templateDir").Output()
|
||||
if strings.TrimSpace(string(template)) == c.templateDir() {
|
||||
_ = command("git", "config", "--global", "--unset", "init.templateDir")
|
||||
}
|
||||
for _, dir := range skillDirs {
|
||||
_ = os.RemoveAll(filepath.Join(home, dir))
|
||||
}
|
||||
return os.RemoveAll(c.templateDir())
|
||||
}
|
||||
|
||||
func (c *Client) Update(ctx context.Context) (bool, error) {
|
||||
binary, err := os.Executable()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
current, err := os.ReadFile(binary)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/dl/lodge-%s-%s", runtime.GOOS, runtime.GOARCH), nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
latest, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if hashOf(latest) == hashOf(current) {
|
||||
return false, nil
|
||||
}
|
||||
if err := writeFile(binary, latest, 0o755); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, restartService()
|
||||
}
|
||||
Reference in new issue
Block a user