Compare commits
16
Commits
4207f7e6f8
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
302ad0c27f | ||
|
|
2acb893962 | ||
|
|
c7e6e3c2d3 | ||
|
|
417640e75d | ||
|
|
5676c3e3bb | ||
|
|
9d96502eb5 | ||
|
|
56544bd025 | ||
|
|
3b5c35ab8a | ||
|
|
a1188d806a | ||
|
|
82cb8f90a7 | ||
|
|
331c1f5b6f | ||
|
|
30cd772f08 | ||
|
|
e89cf7864a | ||
|
|
c2e9284a63 | ||
|
|
91d83f3032 | ||
|
|
45c81cfcaf |
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
||||
_commit: v0.1.0
|
||||
_commit: v0.3.0
|
||||
_src_path: https://git.kotikot.com/templates/blank.git
|
||||
layout: part
|
||||
part: backend
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
_commit: v0.1.2
|
||||
_commit: v0.1.4
|
||||
_src_path: https://git.kotikot.com/templates/caddy.git
|
||||
project_slug: lodge
|
||||
routes:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
_commit: v0.3.1
|
||||
_commit: v0.7.0
|
||||
_src_path: https://git.kotikot.com/templates/frontend-svelte.git
|
||||
include_impeccable: true
|
||||
install_deps: true
|
||||
layout: part
|
||||
linter: biome
|
||||
project_name: lodge
|
||||
project_slug: lodge
|
||||
serve: api
|
||||
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
_commit: v0.4.0
|
||||
_commit: v0.6.0
|
||||
_src_path: https://git.kotikot.com/templates/infra.git
|
||||
init_git: true
|
||||
layout: parts
|
||||
mcp_servers:
|
||||
- svelte
|
||||
parts:
|
||||
- backend
|
||||
- frontend
|
||||
|
||||
@@ -4,3 +4,6 @@
|
||||
docker-compose.override.yml
|
||||
.DS_Store
|
||||
.idea
|
||||
|
||||
# Local playground
|
||||
/t/
|
||||
@@ -1,10 +1,19 @@
|
||||
PARTS := $(patsubst %/Makefile,%,$(wildcard */Makefile))
|
||||
|
||||
.PHONY: env recreate rebuild restart logs down deploy fmt check build pre-deploy post-deploy
|
||||
.PHONY: env recreate rebuild restart logs down fmt check build
|
||||
|
||||
env:
|
||||
@test -f .env || awk 'FNR == 1 && NR > 1 { print "" } 1' .env.example $(addsuffix /.env.example,$(PARTS)) > .env
|
||||
@for part in $(PARTS); do test -f $$part/docker-compose.override.yml || cp $$part/docker-compose.override.yml.example $$part/docker-compose.override.yml; done
|
||||
@fresh=; test -f .env || { awk 'FNR == 1 && NR > 1 { print "" } 1' .env.example $(addsuffix /.env.example,$(PARTS)) > .env; fresh=1; }; \
|
||||
for part in $(PARTS); do \
|
||||
override=$$part/docker-compose.override.yml; \
|
||||
test -f $$override && continue; \
|
||||
cp $$override.example $$override; \
|
||||
for port in $$(sed -n 's/.*127\.0\.0\.1:\([0-9]*\):.*/\1/p' $$override | sort -u); do \
|
||||
random=$$((20000 + $$(od -An -N2 -tu2 /dev/urandom) % 40000)); \
|
||||
sed "s/127\.0\.0\.1:$$port:/127.0.0.1:$$random:/" $$override > $$override.tmp && mv $$override.tmp $$override; \
|
||||
test -z "$$fresh" || { sed "s|localhost:$$port/|localhost:$$random/|" .env > .env.tmp && mv .env.tmp .env; }; \
|
||||
done; \
|
||||
done
|
||||
|
||||
recreate:
|
||||
docker compose up -d --force-recreate
|
||||
@@ -21,9 +30,5 @@ logs:
|
||||
down:
|
||||
docker compose down
|
||||
|
||||
deploy: pre-deploy
|
||||
docker compose up -d
|
||||
$(MAKE) post-deploy
|
||||
|
||||
fmt check build pre-deploy post-deploy:
|
||||
fmt check build:
|
||||
@for part in $(PARTS); do $(MAKE) -C $$part $@ || exit 1; done
|
||||
@@ -2,18 +2,13 @@
|
||||
|
||||
sync for files not meant to be in git
|
||||
|
||||
Runs in Docker, locally the same way as in production. Each part in `backend/`, `frontend/` brings its own `docker-compose.yml`, included from the root one. `COMPOSE_PROFILES` in `.env` selects the services that run on this machine. `docker-compose.override.yml` in each part holds what differs between machines: published ports and the aliases on the external `caddy` network.
|
||||
|
||||
```sh
|
||||
make env
|
||||
make recreate
|
||||
make env # .env and docker-compose.override.yml from the examples
|
||||
make recreate # the services in COMPOSE_PROFILES
|
||||
make check
|
||||
```
|
||||
|
||||
`make env` creates `.env` from the examples of every part and copies each `docker-compose.override.yml.example`. `make recreate` starts the selected profiles, `make rebuild` builds the images first, `make logs`, `make restart` and `make down` do what they say.
|
||||
|
||||
To deploy, `git pull && make deploy`. It runs `make pre-deploy` in every part, which builds images only when their inputs changed and applies migrations, then `docker compose up -d`, then `make post-deploy`, which restarts the services whose mounted code changed. A deploy system with its own `up` step calls the two hooks around it. What was built and started is remembered as `refs/deploy/*` in the clone.
|
||||
|
||||
`make fmt`, `make check` and `make build` run in every part. Part-specific targets live in the part's own Makefile: `make -C backend ...`.
|
||||
Parts: `backend/`, `frontend/`, each with its own Makefile.
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -25,9 +20,11 @@ A repository is identified by the hash of its root commit, so nothing is registe
|
||||
shared docs/agents
|
||||
shared CLAUDE.local.md
|
||||
branch .agent
|
||||
ignore vault/.obsidian/workspace*.json
|
||||
ignore .DS_Store
|
||||
```
|
||||
|
||||
Synced paths are hidden through `.git/info/exclude`; `.gitignore` is never touched, and files tracked by git are never synced. When two machines edit the same file, the later edit wins and the other version stays in history.
|
||||
`ignore` keeps matching files on the machine that has them: they are never pushed, pulled or watched. Its pattern is a glob over the whole path, or over any single name when it has no slash. Synced paths are hidden through `.git/info/exclude`; `.gitignore` is never touched, and files tracked by git are never synced. When two machines edit the same file, the later edit wins and the other version stays in history.
|
||||
|
||||
## Adding a machine
|
||||
|
||||
@@ -41,6 +38,14 @@ Running the returned `curl ... | sh` on a Mac or a Linux machine downloads the c
|
||||
|
||||
`make -C backend dev` runs the server without Docker, `make -C backend client` builds the client into `backend/bin`.
|
||||
|
||||
## Several servers
|
||||
|
||||
A repository can live on another lodge server than the rest of the machine, for example a team's server during an event, while everything else stays on the personal one. Inside the repository, `lodge remote <url> <code>` claims a code from that server and binds the repository to it; further repositories on the same server need only `lodge remote <url>`. Outside a repository the same command only stores the server's token. The binding belongs to the repository key, so worktrees and later clones follow it, and the daemon listens to every server it holds a token for. `lodge remote` prints the server of the current repository, `lodge remote --default` sends it back to the default one. `lodge login` and `lodge update` only touch the default server, and a machine with no default server at all syncs just the repositories bound with `lodge remote`.
|
||||
|
||||
On a machine that is logged in elsewhere, the code's `remote_command`, `curl -fsSL https://lodge.example.com/i/<code>/remote | sh`, installs or updates the client, connects the machine to that server without touching the default one, restarts a running daemon so it runs the new binary, and installs the daemon only if it is not there yet. Repositories are then bound with `lodge remote <url>`. Keep the servers a machine talks to on the same version: an older client ignores bindings and syncs every repository with the default server, and `lodge update` brings the default server's client.
|
||||
|
||||
Where no service manager runs, in a container for example, `lodge install` says so and leaves it to `lodge daemon &`.
|
||||
|
||||
## The admin
|
||||
|
||||
The server serves the admin from `frontend/build`. History is drawn as a borehole log: every revision is a bed, newest on top, hatched by the machine that wrote it and labelled with the branch and commit of the main repository. Dragging along the column, or the arrow keys, shows the notes as they were at that revision, and every depth, file and view is a link. A file can be compared with its previous version and put back on the surface; a repository can be deleted with its history, which leaves the files on the machines alone. Machines are listed with their hatch and can be revoked.
|
||||
|
||||
@@ -44,6 +44,7 @@ linters:
|
||||
- (*github.com/fsnotify/fsnotify.Watcher).Close
|
||||
- os.Unsetenv
|
||||
- os.Remove
|
||||
- os.RemoveAll
|
||||
- fmt.Fprint
|
||||
- fmt.Fprintf
|
||||
- fmt.Fprintln
|
||||
|
||||
+1
-12
@@ -1,10 +1,7 @@
|
||||
COMPOSE := cd .. && docker compose
|
||||
IMAGE := lodge/backend
|
||||
LINT := go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.2
|
||||
changed = ! git diff --quiet $(1) -- $(2) 2>/dev/null
|
||||
stamp = { git update-ref $(1) HEAD 2>/dev/null || true; }
|
||||
|
||||
.PHONY: fmt check test build pre-deploy post-deploy client dev
|
||||
.PHONY: fmt check test build client dev
|
||||
|
||||
fmt:
|
||||
go mod tidy
|
||||
@@ -20,14 +17,6 @@ test:
|
||||
build:
|
||||
$(COMPOSE) build api
|
||||
|
||||
pre-deploy:
|
||||
@if $(call changed,refs/deploy/backend,.) || ! docker image inspect $(IMAGE) >/dev/null 2>&1; then \
|
||||
$(MAKE) build && $(call stamp,refs/deploy/backend); \
|
||||
fi
|
||||
|
||||
post-deploy:
|
||||
@:
|
||||
|
||||
client:
|
||||
go build -trimpath -ldflags "-s -w" -o bin/lodge ./cmd/lodge
|
||||
|
||||
|
||||
+118
-40
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -20,9 +21,11 @@ import (
|
||||
const usage = `lodge keeps agent files out of git and in sync across machines
|
||||
|
||||
lodge login <url> <code> connect this machine using a code from the admin page
|
||||
lodge remote <url> [code] sync the current repository with another server
|
||||
lodge remote [--default] show that server, or go back to the default one
|
||||
lodge install start the daemon, install the git hook and the agent skill
|
||||
lodge uninstall undo install
|
||||
lodge update replace this binary with the one the server ships
|
||||
lodge update replace this binary with the one the default server ships
|
||||
lodge add [dir] start syncing a repository that was cloned before install
|
||||
lodge forget [dir] stop syncing a repository
|
||||
lodge sync [dir] sync once, for machines without a daemon
|
||||
@@ -31,7 +34,7 @@ const usage = `lodge keeps agent files out of git and in sync across machines
|
||||
lodge show <rev|sha> <path> a file as of a revision or a commit of the main repository
|
||||
lodge daemon run the sync daemon in the foreground
|
||||
|
||||
status, log and sync accept --json
|
||||
status, log, sync and remote accept --json
|
||||
`
|
||||
|
||||
var hookEnvironment = []string{"GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_PREFIX"}
|
||||
@@ -39,12 +42,18 @@ var hookEnvironment = []string{"GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GI
|
||||
type status struct {
|
||||
Root string `json:"root"`
|
||||
Key string `json:"key"`
|
||||
Server string `json:"server"`
|
||||
Branch string `json:"branch"`
|
||||
Rules []client.Rule `json:"rules"`
|
||||
Revision int64 `json:"revision"`
|
||||
Spaces map[string]protocol.Manifest `json:"spaces"`
|
||||
}
|
||||
|
||||
type remoteStatus struct {
|
||||
URL string `json:"url"`
|
||||
Default bool `json:"default"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Print(usage)
|
||||
@@ -67,14 +76,15 @@ func hook(ctx context.Context) {
|
||||
for _, name := range hookEnvironment {
|
||||
os.Unsetenv(name)
|
||||
}
|
||||
if c, err := client.Load(); err == nil {
|
||||
_ = c.Hook(ctx, ".")
|
||||
if cfg, err := client.Load(); err == nil {
|
||||
_ = cfg.Hook(ctx, ".")
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context, name string, args []string) error {
|
||||
flags := flag.NewFlagSet(name, flag.ExitOnError)
|
||||
asJSON := flags.Bool("json", false, "machine-readable output")
|
||||
toDefault := flags.Bool("default", false, "go back to the default server")
|
||||
_ = flags.Parse(args)
|
||||
args = flags.Args()
|
||||
target := func(fallback string) string {
|
||||
@@ -84,45 +94,36 @@ func run(ctx context.Context, name string, args []string) error {
|
||||
return fallback
|
||||
}
|
||||
|
||||
if slices.Contains([]string{"help", "-h", "--help"}, name) {
|
||||
fmt.Print(usage)
|
||||
return nil
|
||||
}
|
||||
cfg, err := client.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch name {
|
||||
case "login":
|
||||
if len(args) != 2 {
|
||||
return errors.New("usage: lodge login <url> <code>")
|
||||
}
|
||||
return client.Login(ctx, args[0], args[1])
|
||||
case "help", "-h", "--help":
|
||||
fmt.Print(usage)
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := client.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch name {
|
||||
return cfg.Login(ctx, args[0], args[1])
|
||||
case "remote":
|
||||
return remote(ctx, cfg, args, *toDefault, *asJSON)
|
||||
case "install":
|
||||
return c.Install()
|
||||
return cfg.Install()
|
||||
case "uninstall":
|
||||
return c.Uninstall()
|
||||
return cfg.Uninstall()
|
||||
case "daemon":
|
||||
return c.Daemon(ctx)
|
||||
return cfg.Daemon(ctx)
|
||||
case "update":
|
||||
updated, err := c.Update(ctx)
|
||||
updated, err := cfg.Update(ctx)
|
||||
if err == nil && !updated {
|
||||
fmt.Println("already up to date")
|
||||
}
|
||||
return err
|
||||
case "add", "sync":
|
||||
res, err := c.Sync(ctx, target("."), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return output(*asJSON, res, func() {
|
||||
fmt.Printf("pushed %d, pulled %d\n", res.Pushed, res.Pulled)
|
||||
for _, path := range res.Skipped {
|
||||
fmt.Printf("skipped, over 5 MB: %s\n", path)
|
||||
}
|
||||
})
|
||||
return syncRepo(ctx, cfg, target("."), *asJSON)
|
||||
case "forget":
|
||||
root, err := filepath.Abs(target("."))
|
||||
if err != nil {
|
||||
@@ -131,16 +132,16 @@ func run(ctx context.Context, name string, args []string) error {
|
||||
if repo, err := gitrepo.Open(root); err == nil {
|
||||
root = repo.Root
|
||||
}
|
||||
return c.Forget(root)
|
||||
return cfg.Forget(root)
|
||||
case "status":
|
||||
return showStatus(ctx, c, target("."), *asJSON)
|
||||
return showStatus(ctx, cfg, target("."), *asJSON)
|
||||
case "log":
|
||||
return showLog(ctx, c, target(""), *asJSON)
|
||||
return showLog(ctx, cfg, target(""), *asJSON)
|
||||
case "show":
|
||||
if len(args) != 2 {
|
||||
return errors.New("usage: lodge show <revision|commit-sha> <path>")
|
||||
}
|
||||
return showFile(ctx, c, args[0], args[1])
|
||||
return showFile(ctx, cfg, args[0], args[1])
|
||||
}
|
||||
return fmt.Errorf("unknown command %q, see lodge help", name)
|
||||
}
|
||||
@@ -155,21 +156,86 @@ func output(asJSON bool, value any, human func()) error {
|
||||
return encoder.Encode(value)
|
||||
}
|
||||
|
||||
func showStatus(ctx context.Context, c *client.Client, dir string, asJSON bool) error {
|
||||
func syncRepo(ctx context.Context, cfg *client.Config, dir string, asJSON bool) error {
|
||||
res, err := cfg.Sync(ctx, dir, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return output(asJSON, res, func() {
|
||||
fmt.Printf("pushed %d, pulled %d\n", res.Pushed, res.Pulled)
|
||||
for _, path := range res.Skipped {
|
||||
fmt.Printf("skipped, over 5 MB: %s\n", path)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func remote(ctx context.Context, cfg *client.Config, args []string, toDefault, asJSON bool) error {
|
||||
if len(args) > 2 || toDefault && len(args) > 0 {
|
||||
return errors.New("usage: lodge remote [<url> [code]] | lodge remote --default")
|
||||
}
|
||||
if len(args) == 2 {
|
||||
if err := cfg.Connect(ctx, args[0], args[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
repo, err := gitrepo.Open(".")
|
||||
if err != nil && len(args) == 2 {
|
||||
fmt.Printf("connected to %s, run lodge remote %s inside a repository to sync it there\n", args[0], args[0])
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(args) == 0 && !toDefault {
|
||||
c, err := cfg.Server(repo.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
report := remoteStatus{URL: c.URL, Default: !cfg.Bound(repo.Key)}
|
||||
return output(asJSON, report, func() {
|
||||
if report.Default {
|
||||
fmt.Printf("%s (default)\n", report.URL)
|
||||
} else {
|
||||
fmt.Println(report.URL)
|
||||
}
|
||||
})
|
||||
}
|
||||
server := ""
|
||||
if len(args) > 0 {
|
||||
server = args[0]
|
||||
}
|
||||
if err := cfg.Bind(repo.Key, server); err != nil {
|
||||
return err
|
||||
}
|
||||
err = syncRepo(ctx, cfg, repo.Root, asJSON)
|
||||
if toDefault && errors.Is(err, client.ErrNotLoggedIn) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func showStatus(ctx context.Context, cfg *client.Config, dir string, asJSON bool) error {
|
||||
repo, err := gitrepo.Open(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := cfg.Server(repo.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state, err := c.State(ctx, repo.Key, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
report := status{Root: repo.Root, Key: repo.Key, Branch: repo.Branch(), Rules: client.LoadRules(repo.Root), Revision: state.Seq, Spaces: state.Spaces}
|
||||
report := status{Root: repo.Root, Key: repo.Key, Server: c.URL, Branch: repo.Branch(), Rules: client.LoadRules(repo.Root), Revision: state.Seq, Spaces: state.Spaces}
|
||||
return output(asJSON, report, func() {
|
||||
fmt.Printf("%s\nkey %s, branch %s, revision %d\n\nrules:\n", report.Root, report.Key, report.Branch, report.Revision)
|
||||
fmt.Printf("%s\nkey %s, branch %s, revision %d\nserver %s\n\nrules:\n", report.Root, report.Key, report.Branch, report.Revision, report.Server)
|
||||
for _, rule := range report.Rules {
|
||||
kind := "branch"
|
||||
if rule.Shared {
|
||||
switch {
|
||||
case rule.Ignore:
|
||||
kind = "ignore"
|
||||
case rule.Shared:
|
||||
kind = "shared"
|
||||
}
|
||||
fmt.Printf(" %s %s\n", kind, rule.Path)
|
||||
@@ -183,11 +249,15 @@ func showStatus(ctx context.Context, c *client.Client, dir string, asJSON bool)
|
||||
})
|
||||
}
|
||||
|
||||
func showLog(ctx context.Context, c *client.Client, path string, asJSON bool) error {
|
||||
func showLog(ctx context.Context, cfg *client.Config, path string, asJSON bool) error {
|
||||
repo, err := gitrepo.Open(".")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := cfg.Server(repo.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
revisions, err := c.Revisions(ctx, repo.Key, path)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -195,7 +265,11 @@ func showLog(ctx context.Context, c *client.Client, path string, asJSON bool) er
|
||||
return output(asJSON, revisions, func() {
|
||||
for _, rev := range revisions {
|
||||
when := time.UnixMilli(rev.Time).Format("2006-01-02 15:04")
|
||||
fmt.Printf("%d %s %s %s@%.10s\n", rev.Seq, when, rev.Machine, rev.MainBranch, rev.MainSHA)
|
||||
who := rev.Machine
|
||||
if rev.Author != "" {
|
||||
who = rev.Author + " via " + rev.Machine
|
||||
}
|
||||
fmt.Printf("%d %s %s %s@%.10s\n", rev.Seq, when, who, rev.MainBranch, rev.MainSHA)
|
||||
for _, change := range rev.Changes {
|
||||
action := "M"
|
||||
if change.Base == "" {
|
||||
@@ -209,11 +283,15 @@ func showLog(ctx context.Context, c *client.Client, path string, asJSON bool) er
|
||||
})
|
||||
}
|
||||
|
||||
func showFile(ctx context.Context, c *client.Client, at, path string) error {
|
||||
func showFile(ctx context.Context, cfg *client.Config, at, path string) error {
|
||||
repo, err := gitrepo.Open(".")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := cfg.Server(repo.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state, err := c.State(ctx, repo.Key, at)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+5
-5
@@ -4,17 +4,17 @@ go 1.27.1
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
modernc.org/sqlite v1.59.0
|
||||
modernc.org/sqlite v1.61.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/dustin/go-humanize v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/ncruces/go-strftime v1.1.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
modernc.org/libc v1.75.7 // indirect
|
||||
golang.org/x/sys v0.49.0 // indirect
|
||||
modernc.org/libc v1.79.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.12.1 // indirect
|
||||
)
|
||||
@@ -1,5 +1,7 @@
|
||||
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/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg=
|
||||
github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
|
||||
@@ -12,6 +14,8 @@ github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsRe
|
||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/ncruces/go-strftime v1.1.0 h1:vtPmPWMkb539RGV9WpWT9EwJGvrOCKvMNI4obWL47Hk=
|
||||
github.com/ncruces/go-strftime v1.1.0/go.mod h1:yVaq7iwCN09vHRrKBrrflXT0M9tTMG2KmZ9Fj7xeG/g=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
@@ -20,6 +24,10 @@ golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
modernc.org/cc/v4 v4.29.2 h1:h6+9ciCnPKutf4I03CvheAvDLX7+IHlqR6Iy6J+cgd8=
|
||||
@@ -36,6 +44,10 @@ modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.75.7 h1:o3DTP9/0p9pKmY2WCKQaySW6wIiZhNM7wc2lUoyhfew=
|
||||
modernc.org/libc v1.75.7/go.mod h1:bO5o2ztHxBb2rjz0PgdHN0sSMw57CgxGFLZ3Qd/QpVQ=
|
||||
modernc.org/libc v1.77.1 h1:Ct8j47QtiZ1Enj2DtFXQtUqrPCAjdCmPjtCuvrYQ0Hs=
|
||||
modernc.org/libc v1.77.1/go.mod h1:87/pZ4L6nD1zqW4nItuS12YO7hN1igAah34xjnQo/W0=
|
||||
modernc.org/libc v1.79.0 h1:9Dvvv++bAMSkSuBzuOFAatbTV5OIoq6Y6FohmD3e7q8=
|
||||
modernc.org/libc v1.79.0/go.mod h1:uT3kR61NsbeKJSLqq83GJwSnZMVLlSjyAz4DfTaBy1M=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
|
||||
@@ -46,6 +58,10 @@ modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.59.0 h1:X1es1GpqBlS/5T+vbM4HLUdaa8OtQx468DF2vrx+38A=
|
||||
modernc.org/sqlite v1.59.0/go.mod h1:+paeT2A3iPRHkQDwG7oA6Tk0zQd5woMEI8q7orfry8k=
|
||||
modernc.org/sqlite v1.60.1 h1:/blz53O951KWFOso4QQvEs/Fq6cDBKLtMVrYNSeJVKw=
|
||||
modernc.org/sqlite v1.60.1/go.mod h1:1dIoEagfDE72QytD5scH1lxARtaUgKgHC/NuApA27r0=
|
||||
modernc.org/sqlite v1.61.0 h1:sQi63oLQ9uquAUA4CzOisH35Q3pEszkPBuHM0tFL8CY=
|
||||
modernc.org/sqlite v1.61.0/go.mod h1:f4gr5tbpx2umklaT/QVtbi/50hNdM+4ng+2zMo8KXJ0=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -29,14 +30,23 @@ const (
|
||||
|
||||
var (
|
||||
httpClient = &http.Client{Transport: newTransport()}
|
||||
ErrNotLoggedIn = errors.New("not logged in: run the install command from the lodge admin page")
|
||||
ErrNotLoggedIn = errors.New("not logged in: run the install command from the lodge admin page, or lodge remote <url> <code> inside the repository")
|
||||
errConflict = errors.New("server state changed")
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
URL string `json:"url"`
|
||||
Token string `json:"token"`
|
||||
type Config struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Servers map[string]string `json:"servers,omitempty"`
|
||||
Repos map[string]string `json:"repos,omitempty"`
|
||||
DataDir string `json:"-"`
|
||||
path string
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
URL string
|
||||
Token string
|
||||
DataDir string
|
||||
}
|
||||
|
||||
func configPath() string {
|
||||
@@ -49,30 +59,128 @@ func dataDir() string {
|
||||
return filepath.Join(cmp.Or(os.Getenv("XDG_DATA_HOME"), filepath.Join(home, ".local", "share")), "lodge")
|
||||
}
|
||||
|
||||
func Load() (*Client, error) {
|
||||
c := &Client{DataDir: dataDir()}
|
||||
if data, err := os.ReadFile(configPath()); err == nil {
|
||||
if err := json.Unmarshal(data, c); err != nil {
|
||||
func serverURL(raw string) string {
|
||||
return strings.TrimSuffix(raw, "/")
|
||||
}
|
||||
|
||||
func Load() (*Config, error) {
|
||||
return loadConfig(configPath(), dataDir())
|
||||
}
|
||||
|
||||
func loadConfig(file, dir string) (*Config, error) {
|
||||
cfg := &Config{DataDir: dir, path: file}
|
||||
if data, err := os.ReadFile(file); err == nil {
|
||||
if err := json.Unmarshal(data, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
c.URL = strings.TrimSuffix(cmp.Or(os.Getenv("LODGE_URL"), c.URL), "/")
|
||||
c.Token = cmp.Or(os.Getenv("LODGE_TOKEN"), c.Token)
|
||||
if c.URL == "" || c.Token == "" {
|
||||
return nil, ErrNotLoggedIn
|
||||
}
|
||||
return c, nil
|
||||
cfg.URL = serverURL(cmp.Or(os.Getenv("LODGE_URL"), cfg.URL))
|
||||
cfg.Token = cmp.Or(os.Getenv("LODGE_TOKEN"), cfg.Token)
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func Login(ctx context.Context, serverURL, code string) error {
|
||||
name, _ := os.Hostname()
|
||||
c := &Client{URL: strings.TrimSuffix(serverURL, "/")}
|
||||
var claim protocol.ClaimResponse
|
||||
if err := c.call(ctx, http.MethodPost, "/api/claim", protocol.ClaimRequest{Code: code, Name: name}, &claim); err != nil {
|
||||
func (cfg *Config) reload() (*Config, error) {
|
||||
return loadConfig(cfg.path, cfg.DataDir)
|
||||
}
|
||||
|
||||
func (cfg *Config) update(change func(*Config)) error {
|
||||
unlock, err := flock(cfg.path + ".lock")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Token = claim.Token
|
||||
return writeJSONFile(configPath(), c)
|
||||
defer unlock()
|
||||
saved := &Config{Servers: map[string]string{}, Repos: map[string]string{}}
|
||||
if data, err := os.ReadFile(cfg.path); err == nil {
|
||||
if err := json.Unmarshal(data, saved); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
change(saved)
|
||||
if err := writeJSONFile(cfg.path, saved); err != nil {
|
||||
return err
|
||||
}
|
||||
fresh, err := cfg.reload()
|
||||
if err == nil {
|
||||
*cfg = *fresh
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (cfg *Config) serverOf(key string) string {
|
||||
return cmp.Or(cfg.Repos[key], cfg.URL)
|
||||
}
|
||||
|
||||
func (cfg *Config) Bound(key string) bool {
|
||||
return cfg.Repos[key] != ""
|
||||
}
|
||||
|
||||
func (cfg *Config) Server(key string) (*Client, error) {
|
||||
return cfg.client(cfg.serverOf(key))
|
||||
}
|
||||
|
||||
func (cfg *Config) client(server string) (*Client, error) {
|
||||
token := cfg.Servers[server]
|
||||
if server == cfg.URL {
|
||||
token = cmp.Or(cfg.Token, token)
|
||||
}
|
||||
switch {
|
||||
case server == "":
|
||||
return nil, ErrNotLoggedIn
|
||||
case token == "":
|
||||
return nil, fmt.Errorf("no token for %s: run lodge remote %s <code> with a code from that server", server, server)
|
||||
}
|
||||
return &Client{URL: server, Token: token, DataDir: cfg.DataDir}, nil
|
||||
}
|
||||
|
||||
func (cfg *Config) clients() map[Client]bool {
|
||||
clients := map[Client]bool{}
|
||||
for _, server := range append(slices.Collect(maps.Keys(cfg.Servers)), cfg.URL) {
|
||||
if c, err := cfg.client(server); err == nil {
|
||||
clients[*c] = true
|
||||
}
|
||||
}
|
||||
return clients
|
||||
}
|
||||
|
||||
func claim(ctx context.Context, server, code string) (string, error) {
|
||||
name, _ := os.Hostname()
|
||||
var res protocol.ClaimResponse
|
||||
err := (&Client{URL: server}).call(ctx, http.MethodPost, "/api/claim", protocol.ClaimRequest{Code: code, Name: name}, &res)
|
||||
return res.Token, err
|
||||
}
|
||||
|
||||
func (cfg *Config) Login(ctx context.Context, server, code string) error {
|
||||
server = serverURL(server)
|
||||
token, err := claim(ctx, server, code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cfg.update(func(saved *Config) { saved.URL, saved.Token = server, token })
|
||||
}
|
||||
|
||||
func (cfg *Config) Connect(ctx context.Context, server, code string) error {
|
||||
server = serverURL(server)
|
||||
token, err := claim(ctx, server, code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cfg.update(func(saved *Config) { saved.Servers[server] = token })
|
||||
}
|
||||
|
||||
func (cfg *Config) Bind(key, server string) error {
|
||||
server = serverURL(server)
|
||||
if server != "" {
|
||||
if _, err := cfg.client(server); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return cfg.update(func(saved *Config) {
|
||||
if server == "" {
|
||||
delete(saved.Repos, key)
|
||||
} else {
|
||||
saved.Repos[key] = server
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func writeJSONFile(path string, value any) error {
|
||||
@@ -205,20 +313,20 @@ func (c *Client) uploadBlob(ctx context.Context, hash string) error {
|
||||
return res.Body.Close()
|
||||
}
|
||||
|
||||
func (c *Client) registryPath() string {
|
||||
return filepath.Join(c.DataDir, "worktrees.json")
|
||||
func (cfg *Config) registryPath() string {
|
||||
return filepath.Join(cfg.DataDir, "worktrees.json")
|
||||
}
|
||||
|
||||
func (c *Client) Worktrees() []string {
|
||||
func (cfg *Config) Worktrees() []string {
|
||||
var roots []string
|
||||
if data, err := os.ReadFile(c.registryPath()); err == nil {
|
||||
if data, err := os.ReadFile(cfg.registryPath()); err == nil {
|
||||
_ = json.Unmarshal(data, &roots)
|
||||
}
|
||||
return roots
|
||||
}
|
||||
|
||||
func (c *Client) register(root string) error {
|
||||
roots := c.Worktrees()
|
||||
func (cfg *Config) register(root string) error {
|
||||
roots := cfg.Worktrees()
|
||||
if slices.Contains(roots, root) {
|
||||
return nil
|
||||
}
|
||||
@@ -226,10 +334,10 @@ func (c *Client) register(root string) error {
|
||||
_, err := os.Stat(known)
|
||||
return err != nil
|
||||
})
|
||||
return writeJSONFile(c.registryPath(), append(roots, root))
|
||||
return writeJSONFile(cfg.registryPath(), append(roots, root))
|
||||
}
|
||||
|
||||
func (c *Client) Forget(root string) error {
|
||||
roots := slices.DeleteFunc(c.Worktrees(), func(known string) bool { return known == root })
|
||||
return writeJSONFile(c.registryPath(), roots)
|
||||
func (cfg *Config) Forget(root string) error {
|
||||
roots := slices.DeleteFunc(cfg.Worktrees(), func(known string) bool { return known == root })
|
||||
return writeJSONFile(cfg.registryPath(), roots)
|
||||
}
|
||||
@@ -36,38 +36,46 @@ type tree struct {
|
||||
}
|
||||
|
||||
type daemon struct {
|
||||
client *Client
|
||||
config *Config
|
||||
watcher *fsnotify.Watcher
|
||||
queue chan string
|
||||
|
||||
mu sync.Mutex
|
||||
current *Config
|
||||
listeners map[Client]context.CancelFunc
|
||||
owners map[string]string
|
||||
trees map[string]tree
|
||||
pending map[string]*time.Timer
|
||||
}
|
||||
|
||||
func (c *Client) Daemon(ctx context.Context) error {
|
||||
func (cfg *Config) Daemon(ctx context.Context) error {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer watcher.Close()
|
||||
if err := os.MkdirAll(c.DataDir, 0o755); err != nil {
|
||||
for _, dir := range []string{cfg.DataDir, filepath.Dir(cfg.path)} {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := watcher.Add(c.DataDir); err != nil {
|
||||
if err := watcher.Add(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
d := &daemon{
|
||||
client: c,
|
||||
config: cfg,
|
||||
watcher: watcher,
|
||||
queue: make(chan string, 256),
|
||||
current: cfg,
|
||||
listeners: map[Client]context.CancelFunc{},
|
||||
owners: map[string]string{},
|
||||
trees: map[string]tree{},
|
||||
pending: map[string]*time.Timer{},
|
||||
}
|
||||
go d.listen(ctx)
|
||||
go d.work(ctx)
|
||||
d.connect(ctx)
|
||||
var workers sync.WaitGroup
|
||||
defer workers.Wait()
|
||||
workers.Go(func() { d.work(ctx) })
|
||||
d.syncAll()
|
||||
|
||||
ticker := time.NewTicker(fullSyncEvery)
|
||||
@@ -81,13 +89,43 @@ func (c *Client) Daemon(ctx context.Context) error {
|
||||
case err := <-watcher.Errors:
|
||||
log.Printf("watcher: %v", err)
|
||||
case event := <-watcher.Events:
|
||||
d.handle(event)
|
||||
d.handle(ctx, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *daemon) handle(event fsnotify.Event) {
|
||||
if event.Name == d.client.registryPath() {
|
||||
func (d *daemon) connect(ctx context.Context) {
|
||||
current, err := d.config.reload()
|
||||
if err != nil {
|
||||
log.Printf("config: %v", err)
|
||||
return
|
||||
}
|
||||
clients := current.clients()
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.current = current
|
||||
for c, stop := range d.listeners {
|
||||
if !clients[c] {
|
||||
stop()
|
||||
delete(d.listeners, c)
|
||||
}
|
||||
}
|
||||
for c := range clients {
|
||||
if d.listeners[c] == nil {
|
||||
listenCtx, stop := context.WithCancel(ctx)
|
||||
d.listeners[c] = stop
|
||||
go d.listen(listenCtx, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *daemon) handle(ctx context.Context, event fsnotify.Event) {
|
||||
switch event.Name {
|
||||
case d.config.registryPath():
|
||||
d.syncAll()
|
||||
return
|
||||
case d.config.path:
|
||||
d.connect(ctx)
|
||||
d.syncAll()
|
||||
return
|
||||
}
|
||||
@@ -112,8 +150,8 @@ func (t tree) relevant(root, name string) bool {
|
||||
return false
|
||||
}
|
||||
rel = filepath.ToSlash(rel)
|
||||
return slices.ContainsFunc(t.rules, func(rule Rule) bool {
|
||||
return within(rel, rule.Path) || within(rule.Path, rel)
|
||||
return !t.rules.ignored(rel) && slices.ContainsFunc(t.rules, func(rule Rule) bool {
|
||||
return !rule.Ignore && (within(rel, rule.Path) || within(rule.Path, rel))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -133,7 +171,7 @@ func (d *daemon) schedule(root string) {
|
||||
}
|
||||
|
||||
func (d *daemon) syncAll() {
|
||||
for _, root := range d.client.Worktrees() {
|
||||
for _, root := range d.config.Worktrees() {
|
||||
if _, err := os.Stat(root); errors.Is(err, fs.ErrNotExist) {
|
||||
d.forget(root)
|
||||
} else {
|
||||
@@ -143,7 +181,7 @@ func (d *daemon) syncAll() {
|
||||
}
|
||||
|
||||
func (d *daemon) forget(root string) {
|
||||
if err := d.client.Forget(root); err != nil {
|
||||
if err := d.config.Forget(root); err != nil {
|
||||
log.Printf("%s: %v", root, err)
|
||||
return
|
||||
}
|
||||
@@ -160,10 +198,13 @@ func (d *daemon) work(ctx context.Context) {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case root := <-d.queue:
|
||||
res, err := d.client.Sync(ctx, root, true)
|
||||
if err != nil {
|
||||
res, err := d.config.Sync(ctx, root, true)
|
||||
switch {
|
||||
case errors.Is(err, ErrNotLoggedIn):
|
||||
continue
|
||||
case err != nil:
|
||||
log.Printf("%s: %v", root, err)
|
||||
} else if res.Pushed+res.Pulled > 0 {
|
||||
case res.Pushed+res.Pulled > 0:
|
||||
log.Printf("%s: pushed %d, pulled %d", root, res.Pushed, res.Pulled)
|
||||
}
|
||||
d.watch(root)
|
||||
@@ -178,14 +219,15 @@ func (d *daemon) watch(root string) {
|
||||
}
|
||||
rules := LoadRules(repo.Root)
|
||||
dirs := map[string]bool{repo.GitDir: true}
|
||||
for _, rule := range rules {
|
||||
for _, rule := range rules.synced() {
|
||||
target := filepath.Join(repo.Root, filepath.FromSlash(rule.Path))
|
||||
dirs[nearestDir(filepath.Dir(target), repo.Root)] = true
|
||||
_ = filepath.WalkDir(target, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil || !entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if entry.Name() == ".git" {
|
||||
rel, _ := filepath.Rel(repo.Root, path)
|
||||
if entry.Name() == ".git" || rules.ignored(filepath.ToSlash(rel)) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
dirs[path] = true
|
||||
@@ -224,25 +266,29 @@ func nearestDir(path, root string) string {
|
||||
return root
|
||||
}
|
||||
|
||||
func (d *daemon) listen(ctx context.Context) {
|
||||
func (d *daemon) listen(ctx context.Context, c Client) {
|
||||
backoff := minBackoff
|
||||
for ctx.Err() == nil {
|
||||
for {
|
||||
started := time.Now()
|
||||
err := d.stream(ctx)
|
||||
err := d.stream(ctx, c)
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
if time.Since(started) > maxBackoff {
|
||||
backoff = minBackoff
|
||||
}
|
||||
log.Printf("event stream closed, retrying in %s: %v", backoff, err)
|
||||
log.Printf("%s: event stream closed, retrying in %s: %v", c.URL, backoff, err)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(backoff):
|
||||
}
|
||||
backoff = min(backoff*2, maxBackoff)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *daemon) stream(ctx context.Context) error {
|
||||
res, err := d.client.request(ctx, http.MethodGet, "/api/events", nil)
|
||||
func (d *daemon) stream(ctx context.Context, c Client) error {
|
||||
res, err := c.request(ctx, http.MethodGet, "/api/events", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -257,11 +303,13 @@ func (d *daemon) stream(ctx context.Context) error {
|
||||
}
|
||||
d.mu.Lock()
|
||||
var roots []string
|
||||
if d.current.serverOf(event.Repo) == c.URL {
|
||||
for root, tree := range d.trees {
|
||||
if tree.key == event.Repo {
|
||||
roots = append(roots, root)
|
||||
}
|
||||
}
|
||||
}
|
||||
d.mu.Unlock()
|
||||
for _, root := range roots {
|
||||
d.schedule(root)
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestDaemonWatchesOnlyRulePaths(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { watcher.Close() })
|
||||
d := &daemon{client: m.client, watcher: watcher, owners: map[string]string{}, trees: map[string]tree{}}
|
||||
d := &daemon{config: m.config, watcher: watcher, owners: map[string]string{}, trees: map[string]tree{}}
|
||||
gitDir := filepath.Join(m.root, ".git")
|
||||
expectWatched := func(want ...string) {
|
||||
t.Helper()
|
||||
@@ -55,14 +55,43 @@ func TestDaemonWatchesOnlyRulePaths(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDaemonForgetsVanishedWorktrees(t *testing.T) {
|
||||
c := &Client{DataDir: t.TempDir()}
|
||||
if err := writeJSONFile(c.registryPath(), []string{filepath.Join(t.TempDir(), "gone")}); err != nil {
|
||||
func TestDaemonSkipsIgnoredPaths(t *testing.T) {
|
||||
m, _ := setup(t)
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d := &daemon{client: c, owners: map[string]string{}, trees: map[string]tree{}}
|
||||
t.Cleanup(func() { watcher.Close() })
|
||||
d := &daemon{config: m.config, watcher: watcher, owners: map[string]string{}, trees: map[string]tree{}}
|
||||
write(t, m.root, RulesFile, "shared vault\nignore vault/.obsidian/workspace*.json\nignore .DS_Store\nignore vault/.trash\n")
|
||||
write(t, m.root, "vault/.obsidian/app.json", "settings")
|
||||
write(t, m.root, "vault/.trash/old.md", "old")
|
||||
d.watch(m.root)
|
||||
if _, watched := d.owners[filepath.Join(m.root, "vault/.trash")]; watched {
|
||||
t.Fatal("ignored directory is watched")
|
||||
}
|
||||
for name, want := range map[string]bool{
|
||||
"vault/note.md": true,
|
||||
"vault/.obsidian/app.json": true,
|
||||
"vault/.obsidian/workspace.json": false,
|
||||
"vault/.obsidian/workspace-mobile.json": false,
|
||||
"vault/.DS_Store": false,
|
||||
"vault/.trash/old.md": false,
|
||||
} {
|
||||
if got := d.trees[m.root].relevant(m.root, filepath.Join(m.root, name)); got != want {
|
||||
t.Errorf("relevant(%s) = %v, want %v", name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDaemonForgetsVanishedWorktrees(t *testing.T) {
|
||||
cfg := &Config{DataDir: t.TempDir()}
|
||||
if err := writeJSONFile(cfg.registryPath(), []string{filepath.Join(t.TempDir(), "gone")}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d := &daemon{config: cfg, owners: map[string]string{}, trees: map[string]tree{}}
|
||||
d.syncAll()
|
||||
if roots := c.Worktrees(); len(roots) != 0 {
|
||||
if roots := cfg.Worktrees(); len(roots) != 0 {
|
||||
t.Fatalf("registry still lists %v", roots)
|
||||
}
|
||||
}
|
||||
@@ -75,11 +75,11 @@ 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 (cfg *Config) templateDir() string {
|
||||
return filepath.Join(cfg.DataDir, "git-template")
|
||||
}
|
||||
|
||||
func (c *Client) Install() error {
|
||||
func (cfg *Config) Install() error {
|
||||
binary, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -88,7 +88,7 @@ func (c *Client) Install() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.installHook(binary); err != nil {
|
||||
if err := cfg.installHook(binary); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, dir := range skillDirs {
|
||||
@@ -96,14 +96,17 @@ func (c *Client) Install() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return startService(home, binary, filepath.Join(c.DataDir, "daemon.log"))
|
||||
if err := startService(home, binary, filepath.Join(cfg.DataDir, "daemon.log")); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "lodge: the daemon could not be started as a service: %v\nrun `lodge daemon &` to keep this machine in sync\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) installHook(binary string) error {
|
||||
func (cfg *Config) installHook(binary string) error {
|
||||
template, _ := exec.Command("git", "config", "--global", "--get", "init.templateDir").Output()
|
||||
dir := strings.TrimSpace(string(template))
|
||||
if dir == "" {
|
||||
dir = c.templateDir()
|
||||
dir = cfg.templateDir()
|
||||
if err := command("git", "config", "--global", "init.templateDir", dir); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -146,7 +149,7 @@ func restartService() error {
|
||||
return command("systemctl", "--user", "restart", systemdUnit)
|
||||
}
|
||||
|
||||
func (c *Client) Uninstall() error {
|
||||
func (cfg *Config) Uninstall() error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -159,16 +162,20 @@ func (c *Client) Uninstall() error {
|
||||
_ = os.Remove(unitPath(home))
|
||||
}
|
||||
template, _ := exec.Command("git", "config", "--global", "--get", "init.templateDir").Output()
|
||||
if strings.TrimSpace(string(template)) == c.templateDir() {
|
||||
if strings.TrimSpace(string(template)) == cfg.templateDir() {
|
||||
_ = command("git", "config", "--global", "--unset", "init.templateDir")
|
||||
}
|
||||
for _, dir := range skillDirs {
|
||||
_ = os.RemoveAll(filepath.Join(home, dir))
|
||||
}
|
||||
return os.RemoveAll(c.templateDir())
|
||||
return os.RemoveAll(cfg.templateDir())
|
||||
}
|
||||
|
||||
func (c *Client) Update(ctx context.Context) (bool, error) {
|
||||
func (cfg *Config) Update(ctx context.Context) (bool, error) {
|
||||
c, err := cfg.client(cfg.URL)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
binary, err := os.Executable()
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"lodge/internal/gitrepo"
|
||||
"lodge/internal/protocol"
|
||||
)
|
||||
|
||||
func issueCode(t *testing.T, url string) string {
|
||||
t.Helper()
|
||||
var code protocol.CodeResponse
|
||||
admin := &Client{URL: url, Token: adminToken}
|
||||
if err := admin.call(context.Background(), http.MethodPost, "/api/codes", nil, &code); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return code.Code
|
||||
}
|
||||
|
||||
func (m *machine) remote(url, code string) {
|
||||
m.t.Helper()
|
||||
ctx := context.Background()
|
||||
if code != "" {
|
||||
if err := m.config.Connect(ctx, url, code); err != nil {
|
||||
m.t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := m.config.Bind(repoKey(m.t, m), url); err != nil {
|
||||
m.t.Fatal(err)
|
||||
}
|
||||
m.sync()
|
||||
}
|
||||
|
||||
func revisionsOn(t *testing.T, url, key string) []protocol.Revision {
|
||||
t.Helper()
|
||||
revisions, err := (&Client{URL: url, Token: adminToken}).Revisions(context.Background(), key, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return revisions
|
||||
}
|
||||
|
||||
func TestBoundRepositoryNeverReachesTheDefaultServer(t *testing.T) {
|
||||
personal, event, origin := newServer(t), newServer(t), newOrigin(t)
|
||||
mac := newMachine(t, origin, personal, adminToken)
|
||||
key := repoKey(t, mac)
|
||||
ctx := context.Background()
|
||||
|
||||
if err := mac.config.Hook(ctx, mac.root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stale, err := mac.config.reload()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mac.remote(event+"/", issueCode(t, event))
|
||||
write(t, mac.root, "docs/agents/plan.md", "event plan")
|
||||
if _, err := stale.Sync(ctx, mac.root, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
worktree := filepath.Join(t.TempDir(), "feature")
|
||||
git(t, mac.root, "worktree", "add", "-b", "feature", worktree)
|
||||
worktree, _ = filepath.EvalSymlinks(worktree)
|
||||
if err := mac.config.Hook(ctx, worktree); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if data, _ := os.ReadFile(filepath.Join(worktree, "docs/agents/plan.md")); string(data) != "event plan" {
|
||||
t.Fatalf("new worktree of a bound repository got %q", data)
|
||||
}
|
||||
|
||||
other := newMachine(t, newOrigin(t), personal, adminToken)
|
||||
other.config = mac.config
|
||||
write(t, other.root, "docs/agents/private.md", "personal")
|
||||
other.sync()
|
||||
|
||||
if revisions := revisionsOn(t, personal, key); len(revisions) != 0 {
|
||||
t.Fatalf("the default server received %d revisions of a bound repository", len(revisions))
|
||||
}
|
||||
if revisions := revisionsOn(t, event, key); len(revisions) != 1 {
|
||||
t.Fatalf("the event server has %d revisions, want 1", len(revisions))
|
||||
}
|
||||
if revisions := revisionsOn(t, event, repoKey(t, other)); len(revisions) != 0 {
|
||||
t.Fatal("an unbound repository reached the event server")
|
||||
}
|
||||
if revisions := revisionsOn(t, personal, repoKey(t, other)); len(revisions) != 1 {
|
||||
t.Fatal("an unbound repository did not reach the default server")
|
||||
}
|
||||
|
||||
if c, err := mac.config.Server(key); err != nil || c.URL != event {
|
||||
t.Fatalf("server of the bound repository = %v (%v), want %s", c, err, event)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMachineWithoutDefaultServer(t *testing.T) {
|
||||
event, origin := newServer(t), newOrigin(t)
|
||||
mac := newMachine(t, origin, event, adminToken)
|
||||
write(t, mac.root, "docs/agents/plan.md", "plan")
|
||||
mac.sync()
|
||||
|
||||
fresh := newMachine(t, origin, "", "")
|
||||
if _, err := fresh.config.Sync(context.Background(), fresh.root, true); !errors.Is(err, ErrNotLoggedIn) {
|
||||
t.Fatalf("sync without any server: %v", err)
|
||||
}
|
||||
if roots := fresh.config.Worktrees(); len(roots) != 0 {
|
||||
t.Fatalf("a repository without a server was registered: %v", roots)
|
||||
}
|
||||
if err := fresh.config.Connect(context.Background(), event, issueCode(t, event)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fresh.remote(event, "")
|
||||
fresh.expect("docs/agents/plan.md", "plan")
|
||||
if fresh.config.URL != "" {
|
||||
t.Fatalf("lodge remote set the default server to %q", fresh.config.URL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMovingBetweenServersKeepsLocalFiles(t *testing.T) {
|
||||
personal, event, origin := newServer(t), newServer(t), newOrigin(t)
|
||||
mac := newMachine(t, origin, personal, adminToken)
|
||||
dell := newMachine(t, origin, "", "")
|
||||
key := repoKey(t, mac)
|
||||
|
||||
write(t, mac.root, "docs/agents/mine.md", "mine")
|
||||
mac.sync()
|
||||
dell.remote(event, issueCode(t, event))
|
||||
write(t, dell.root, "docs/agents/theirs.md", "theirs")
|
||||
dell.sync()
|
||||
|
||||
mac.remote(event, issueCode(t, event))
|
||||
mac.expect("docs/agents/mine.md", "mine")
|
||||
mac.expect("docs/agents/theirs.md", "theirs")
|
||||
dell.sync()
|
||||
dell.expect("docs/agents/mine.md", "mine")
|
||||
|
||||
if err := mac.config.Bind(key, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mac.sync()
|
||||
mac.expect("docs/agents/mine.md", "mine")
|
||||
mac.expect("docs/agents/theirs.md", "theirs")
|
||||
state, err := mac.client.State(context.Background(), key, "")
|
||||
if err != nil || len(state.Spaces[protocol.SharedSpace]) != 2 {
|
||||
t.Fatalf("back on the default server: %v (%v)", state.Spaces, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateFromBeforeServersIsAdopted(t *testing.T) {
|
||||
mac, dell := setup(t)
|
||||
write(t, mac.root, "docs/agents/plan.md", "plan")
|
||||
mac.sync()
|
||||
dell.sync()
|
||||
repo := openRepo(t, mac)
|
||||
if err := os.Rename(mac.client.statePath(repo), worktreePath(mac.config.DataDir, repo)+".json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Remove(filepath.Join(dell.root, "docs/agents/plan.md")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dell.sync()
|
||||
mac.sync()
|
||||
mac.expect("docs/agents/plan.md", "")
|
||||
}
|
||||
|
||||
func TestDaemonFollowsServersAddedLater(t *testing.T) {
|
||||
event, origin := newServer(t), newOrigin(t)
|
||||
mac := newMachine(t, origin, event, adminToken)
|
||||
fresh := newMachine(t, origin, "", "")
|
||||
daemonConfig, err := fresh.config.reload()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan error)
|
||||
go func() { done <- daemonConfig.Daemon(ctx) }()
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
if err := <-done; err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
|
||||
fresh.remote(event, issueCode(t, event))
|
||||
write(t, mac.root, "docs/agents/first.md", "first")
|
||||
mac.sync()
|
||||
fresh.waitFor("docs/agents/first.md", "first")
|
||||
write(t, mac.root, "docs/agents/second.md", "second")
|
||||
mac.sync()
|
||||
fresh.waitFor("docs/agents/second.md", "second")
|
||||
|
||||
var machines []protocol.Machine
|
||||
if err := (&Client{URL: event, Token: adminToken}).call(context.Background(), http.MethodGet, "/api/machines", nil, &machines); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(machines) != 1 || !machines[0].Online {
|
||||
t.Fatalf("the daemon is not online on the server it was bound to: %+v", machines)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *machine) waitFor(path, content string) {
|
||||
m.t.Helper()
|
||||
deadline := time.Now().Add(15 * time.Second)
|
||||
for {
|
||||
if data, _ := os.ReadFile(filepath.Join(m.root, path)); string(data) == content {
|
||||
return
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
m.t.Fatalf("the daemon did not bring %s", path)
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func openRepo(t *testing.T, m *machine) *gitrepo.Repo {
|
||||
t.Helper()
|
||||
repo, err := gitrepo.Open(m.root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return repo
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"lodge/internal/gitrepo"
|
||||
@@ -15,6 +16,7 @@ const (
|
||||
RulesFile = ".lodge"
|
||||
sharedKeyword = "shared"
|
||||
branchKeyword = "branch"
|
||||
ignoreKeyword = "ignore"
|
||||
excludeBegin = "# lodge begin"
|
||||
excludeEnd = "# lodge end"
|
||||
DefaultRules = "shared docs/agents\nshared CLAUDE.local.md\nbranch .agent\n"
|
||||
@@ -23,6 +25,7 @@ const (
|
||||
type Rule struct {
|
||||
Path string `json:"path"`
|
||||
Shared bool `json:"shared"`
|
||||
Ignore bool `json:"ignore,omitempty"`
|
||||
}
|
||||
|
||||
type Rules []Rule
|
||||
@@ -31,12 +34,12 @@ func parseRules(text string) Rules {
|
||||
rules := Rules{{Path: RulesFile, Shared: true}}
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) != 2 || fields[0] != sharedKeyword && fields[0] != branchKeyword {
|
||||
if len(fields) != 2 || !slices.Contains([]string{sharedKeyword, branchKeyword, ignoreKeyword}, fields[0]) {
|
||||
continue
|
||||
}
|
||||
rulePath := path.Clean(strings.Trim(filepath.ToSlash(fields[1]), "/"))
|
||||
if protocol.SafePath(rulePath) {
|
||||
rules = append(rules, Rule{Path: rulePath, Shared: fields[0] == sharedKeyword})
|
||||
rules = append(rules, Rule{Path: rulePath, Shared: fields[0] == sharedKeyword, Ignore: fields[0] == ignoreKeyword})
|
||||
}
|
||||
}
|
||||
return rules
|
||||
@@ -50,9 +53,14 @@ func LoadRules(root string) Rules {
|
||||
return parseRules(string(data))
|
||||
}
|
||||
|
||||
func (rules Rules) synced() Rules {
|
||||
return slices.DeleteFunc(slices.Clone(rules), func(rule Rule) bool { return rule.Ignore })
|
||||
}
|
||||
|
||||
func (rules Rules) paths() []string {
|
||||
paths := make([]string, len(rules))
|
||||
for i, rule := range rules {
|
||||
synced := rules.synced()
|
||||
paths := make([]string, len(synced))
|
||||
for i, rule := range synced {
|
||||
paths[i] = rule.Path
|
||||
}
|
||||
return paths
|
||||
@@ -60,12 +68,29 @@ func (rules Rules) paths() []string {
|
||||
|
||||
func (rules Rules) covers(file string, shared bool) bool {
|
||||
best := Rule{}
|
||||
for _, rule := range rules {
|
||||
for _, rule := range rules.synced() {
|
||||
if within(file, rule.Path) && len(rule.Path) > len(best.Path) {
|
||||
best = rule
|
||||
}
|
||||
}
|
||||
return best.Path != "" && best.Shared == shared
|
||||
return best.Path != "" && best.Shared == shared && !rules.ignored(file)
|
||||
}
|
||||
|
||||
func (rules Rules) ignored(file string) bool {
|
||||
return file != RulesFile && slices.ContainsFunc(rules, func(rule Rule) bool { return rule.Ignore && rule.matches(file) })
|
||||
}
|
||||
|
||||
func (rule Rule) matches(file string) bool {
|
||||
for name := file; name != "." && name != "/"; name = path.Dir(name) {
|
||||
target := name
|
||||
if !strings.Contains(rule.Path, "/") {
|
||||
target = path.Base(name)
|
||||
}
|
||||
if matched, _ := path.Match(rule.Path, target); matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func within(path, dir string) bool {
|
||||
|
||||
@@ -15,7 +15,7 @@ Run `lodge status` inside the repository to see the rules in effect. The default
|
||||
- `.agent/` holds what belongs to the current branch: the plan for the feature, what was tried, why something was reverted. It is swapped on `git checkout` together with the branch.
|
||||
- `CLAUDE.local.md` is shared across branches.
|
||||
|
||||
The rules live in `.lodge` at the repository root, one per line, as `shared <path>` or `branch <path>`. Paths are relative to the root, no globs. The file itself is synced too.
|
||||
The rules live in `.lodge` at the repository root, one per line, as `shared <path>`, `branch <path>` or `ignore <pattern>`. Paths are relative to the root. `ignore` keeps matching files local to each machine, for editor state and similar noise: the pattern is a glob over the whole path, or over any single name when it has no `/`, as in `ignore vault/.obsidian/workspace*.json` or `ignore .DS_Store`. The file itself is synced too.
|
||||
|
||||
## Rules of conduct
|
||||
|
||||
@@ -23,6 +23,7 @@ The rules live in `.lodge` at the repository root, one per line, as `shared <pat
|
||||
- Files tracked by git are never touched by lodge, even under a synced path.
|
||||
- Files over 5 MB are not synced; `lodge status` lists them.
|
||||
- A deleted file is deleted on every machine. Its history stays on the server.
|
||||
- A repository may sync with another server than the rest of the machine; `lodge status` and `lodge remote` show which. Never run `lodge login`, `lodge remote` or `lodge update` unless asked: they change where files go.
|
||||
- Nothing needs to be run after writing a file: the daemon picks it up within seconds. Without a daemon (CI, cloud sandbox) run `lodge sync` at the start and at the end of the work.
|
||||
|
||||
## Reading history
|
||||
|
||||
+109
-21
@@ -1,6 +1,7 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@@ -9,10 +10,12 @@ import (
|
||||
"maps"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unicode/utf8"
|
||||
|
||||
"lodge/internal/gitrepo"
|
||||
"lodge/internal/protocol"
|
||||
@@ -42,8 +45,12 @@ type diskFiles struct {
|
||||
tracked map[string]bool
|
||||
}
|
||||
|
||||
func worktreePath(dataDir string, repo *gitrepo.Repo) string {
|
||||
return filepath.Join(dataDir, "repos", repo.Key, hashOf([]byte(repo.Root))[:16])
|
||||
}
|
||||
|
||||
func (c *Client) statePath(repo *gitrepo.Repo) string {
|
||||
return filepath.Join(c.DataDir, "repos", repo.Key, hashOf([]byte(repo.Root))[:16]+".json")
|
||||
return worktreePath(c.DataDir, repo) + "-" + hashOf([]byte(c.URL))[:16] + ".json"
|
||||
}
|
||||
|
||||
func (c *Client) loadState(repo *gitrepo.Repo) *worktreeState {
|
||||
@@ -60,8 +67,7 @@ func (c *Client) loadState(repo *gitrepo.Repo) *worktreeState {
|
||||
return st
|
||||
}
|
||||
|
||||
func (c *Client) lock(repo *gitrepo.Repo) (func(), error) {
|
||||
path := c.statePath(repo) + ".lock"
|
||||
func flock(path string) (func(), error) {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -76,31 +82,46 @@ func (c *Client) lock(repo *gitrepo.Repo) (func(), error) {
|
||||
return func() { file.Close() }, nil
|
||||
}
|
||||
|
||||
func (c *Client) Hook(ctx context.Context, dir string) error {
|
||||
func (cfg *Config) Hook(ctx context.Context, dir string) error {
|
||||
repo, err := gitrepo.Open(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
firstSight := !slices.Contains(c.Worktrees(), repo.Root)
|
||||
_, err = c.Sync(ctx, repo.Root, firstSight)
|
||||
firstSight := !slices.Contains(cfg.Worktrees(), repo.Root)
|
||||
_, err = cfg.Sync(ctx, repo.Root, firstSight)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) Sync(ctx context.Context, dir string, online bool) (Result, error) {
|
||||
var res Result
|
||||
func (cfg *Config) Sync(ctx context.Context, dir string, online bool) (Result, error) {
|
||||
repo, err := gitrepo.Open(dir)
|
||||
if err != nil {
|
||||
return res, err
|
||||
return Result{}, err
|
||||
}
|
||||
if err := c.register(repo.Root); err != nil {
|
||||
return res, err
|
||||
}
|
||||
unlock, err := c.lock(repo)
|
||||
worktree := worktreePath(cfg.DataDir, repo)
|
||||
unlock, err := flock(worktree + ".lock")
|
||||
if err != nil {
|
||||
return res, err
|
||||
return Result{}, err
|
||||
}
|
||||
defer unlock()
|
||||
current, err := cfg.reload()
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
c, err := current.Server(repo.Key)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if err := cfg.register(repo.Root); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if c.URL == current.URL {
|
||||
_ = os.Rename(worktree+".json", c.statePath(repo))
|
||||
}
|
||||
return c.sync(ctx, repo, online)
|
||||
}
|
||||
|
||||
func (c *Client) sync(ctx context.Context, repo *gitrepo.Repo, online bool) (Result, error) {
|
||||
var res Result
|
||||
st := c.loadState(repo)
|
||||
for range maxPasses {
|
||||
again, err := c.pass(ctx, repo, st, online, &res)
|
||||
@@ -114,20 +135,20 @@ func (c *Client) Sync(ctx context.Context, dir string, online bool) (Result, err
|
||||
|
||||
func (c *Client) scan(repo *gitrepo.Repo, rules Rules) diskFiles {
|
||||
found := diskFiles{shared: protocol.Manifest{}, branch: protocol.Manifest{}, tracked: repo.Tracked(rules.paths())}
|
||||
for _, rule := range rules {
|
||||
for _, rule := range rules.synced() {
|
||||
start := filepath.Join(repo.Root, filepath.FromSlash(rule.Path))
|
||||
_ = filepath.WalkDir(start, func(full string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if entry.IsDir() && entry.Name() == ".git" {
|
||||
rel, _ := filepath.Rel(repo.Root, full)
|
||||
rel = filepath.ToSlash(rel)
|
||||
if entry.IsDir() && (entry.Name() == ".git" || rules.ignored(rel)) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if !entry.Type().IsRegular() || strings.HasSuffix(full, tempSuffix) {
|
||||
return nil
|
||||
}
|
||||
rel, _ := filepath.Rel(repo.Root, full)
|
||||
rel = filepath.ToSlash(rel)
|
||||
if found.tracked[rel] || !rules.covers(rel, rule.Shared) {
|
||||
return nil
|
||||
}
|
||||
@@ -158,7 +179,9 @@ func (c *Client) scan(repo *gitrepo.Repo, rules Rules) diskFiles {
|
||||
return found
|
||||
}
|
||||
|
||||
func reconcile(space string, local, base, remote protocol.Manifest, relevant func(string) bool) (first, second []protocol.Change, pull, next protocol.Manifest) {
|
||||
type mergeFunc func(local, base, remote protocol.File) (protocol.File, bool)
|
||||
|
||||
func reconcile(space string, local, base, remote protocol.Manifest, relevant func(string) bool, merge mergeFunc) (first, second []protocol.Change, pull, next protocol.Manifest) {
|
||||
pull, next = protocol.Manifest{}, maps.Clone(base)
|
||||
if next == nil {
|
||||
next = protocol.Manifest{}
|
||||
@@ -174,6 +197,14 @@ func reconcile(space string, local, base, remote protocol.Manifest, relevant fun
|
||||
continue
|
||||
}
|
||||
l, b, r := local[path], base[path], remote[path]
|
||||
if l.Hash != b.Hash && r.Hash != b.Hash && l.Hash != r.Hash {
|
||||
if merged, ok := merge(l, b, r); ok {
|
||||
if merged.Hash != l.Hash {
|
||||
pull[path] = merged
|
||||
}
|
||||
l, b = merged, r
|
||||
}
|
||||
}
|
||||
mine := protocol.Change{Space: space, Path: path, Base: r.Hash, Hash: l.Hash}
|
||||
settled := r
|
||||
switch {
|
||||
@@ -197,6 +228,58 @@ func reconcile(space string, local, base, remote protocol.Manifest, relevant fun
|
||||
return first, second, pull, next
|
||||
}
|
||||
|
||||
func (c *Client) merge(ctx context.Context, local, base, remote protocol.File) (protocol.File, bool) {
|
||||
names := []string{"local", "base", "remote"}
|
||||
versions := make([][]byte, len(names))
|
||||
for i, file := range []protocol.File{local, base, remote} {
|
||||
if file.Hash == "" {
|
||||
return protocol.File{}, false
|
||||
}
|
||||
data, err := c.Blob(ctx, file.Hash)
|
||||
if err != nil || !isText(data) {
|
||||
return protocol.File{}, false
|
||||
}
|
||||
versions[i] = data
|
||||
}
|
||||
dir, err := os.MkdirTemp(c.DataDir, "merge-")
|
||||
if err != nil {
|
||||
return protocol.File{}, false
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
for i, name := range names {
|
||||
if err := os.WriteFile(filepath.Join(dir, name), versions[i], 0o600); err != nil {
|
||||
return protocol.File{}, false
|
||||
}
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "git", append([]string{"merge-file", "-p"}, names...)...)
|
||||
cmd.Dir = dir
|
||||
merged, err := cmd.Output()
|
||||
clean := err == nil
|
||||
if !clean {
|
||||
merged, clean = appended(versions[0], versions[1], versions[2])
|
||||
}
|
||||
if !clean || len(merged) > protocol.MaxBlobSize {
|
||||
return protocol.File{}, false
|
||||
}
|
||||
hash, err := c.cacheBlob(merged)
|
||||
return protocol.File{Hash: hash, Time: max(local.Time, remote.Time)}, err == nil
|
||||
}
|
||||
|
||||
func appended(local, base, remote []byte) ([]byte, bool) {
|
||||
if !bytes.HasPrefix(local, base) || !bytes.HasPrefix(remote, base) {
|
||||
return nil, false
|
||||
}
|
||||
tail, glue := local[len(base):], []byte{}
|
||||
if !bytes.HasSuffix(remote, []byte("\n")) && !bytes.HasPrefix(tail, []byte("\n")) {
|
||||
glue = []byte("\n")
|
||||
}
|
||||
return slices.Concat(remote, glue, tail), true
|
||||
}
|
||||
|
||||
func isText(data []byte) bool {
|
||||
return len(data) <= protocol.MaxBlobSize && utf8.Valid(data) && bytes.IndexByte(data, 0) == -1
|
||||
}
|
||||
|
||||
func (c *Client) pass(ctx context.Context, repo *gitrepo.Repo, st *worktreeState, online bool, res *Result) (bool, error) {
|
||||
rules := LoadRules(repo.Root)
|
||||
if err := writeExclude(repo, rules); err != nil {
|
||||
@@ -243,6 +326,9 @@ func (c *Client) pass(ctx context.Context, repo *gitrepo.Repo, st *worktreeState
|
||||
var first, second []protocol.Change
|
||||
pulls, bases := map[string]protocol.Manifest{}, map[string]protocol.Manifest{}
|
||||
spaces := map[string]bool{}
|
||||
merge := func(l, b, r protocol.File) (protocol.File, bool) {
|
||||
return c.merge(ctx, l, b, r)
|
||||
}
|
||||
for _, group := range []map[string]protocol.Manifest{locals, st.Base, remote.Spaces} {
|
||||
for space := range group {
|
||||
spaces[space] = true
|
||||
@@ -257,13 +343,15 @@ func (c *Client) pass(ctx context.Context, repo *gitrepo.Repo, st *worktreeState
|
||||
}
|
||||
continue
|
||||
}
|
||||
relevant := protocol.SafePath
|
||||
relevant := func(path string) bool {
|
||||
return protocol.SafePath(path) && !rules.ignored(path)
|
||||
}
|
||||
if active[space] != nil {
|
||||
relevant = func(path string) bool {
|
||||
return protocol.SafePath(path) && rules.covers(path, space == protocol.SharedSpace) && !found.tracked[path]
|
||||
}
|
||||
}
|
||||
mine, theirs, pull, next := reconcile(space, local, st.Base[space], remote.Spaces[space], relevant)
|
||||
mine, theirs, pull, next := reconcile(space, local, st.Base[space], remote.Spaces[space], relevant, merge)
|
||||
first, second, pulls[space], bases[space] = append(first, mine...), append(second, theirs...), pull, next
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ const adminToken = "admin"
|
||||
|
||||
type machine struct {
|
||||
t *testing.T
|
||||
config *Config
|
||||
client *Client
|
||||
root string
|
||||
}
|
||||
@@ -34,7 +35,7 @@ func git(t *testing.T, dir string, args ...string) string {
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func setup(t *testing.T) (*machine, *machine) {
|
||||
func newServer(t *testing.T) string {
|
||||
t.Helper()
|
||||
store, err := server.OpenStore(filepath.Join(t.TempDir(), "lodge.db"))
|
||||
if err != nil {
|
||||
@@ -43,22 +44,41 @@ func setup(t *testing.T) (*machine, *machine) {
|
||||
t.Cleanup(func() { store.Close() })
|
||||
api := httptest.NewServer(server.New(store, server.Config{AdminToken: adminToken}))
|
||||
t.Cleanup(api.Close)
|
||||
return api.URL
|
||||
}
|
||||
|
||||
func newOrigin(t *testing.T) string {
|
||||
t.Helper()
|
||||
origin := t.TempDir()
|
||||
git(t, origin, "init", "-b", "main")
|
||||
write(t, origin, "README.md", "readme")
|
||||
write(t, origin, "docs/agents/tracked.md", "in git")
|
||||
git(t, origin, "add", ".")
|
||||
git(t, origin, "commit", "-m", "root")
|
||||
git(t, origin, "commit", "-m", "root "+origin)
|
||||
return origin
|
||||
}
|
||||
|
||||
machines := [2]*machine{}
|
||||
for i := range machines {
|
||||
func newMachine(t *testing.T, origin, url, token string) *machine {
|
||||
t.Helper()
|
||||
cfg := &Config{URL: url, Token: token, DataDir: t.TempDir(), path: filepath.Join(t.TempDir(), "config.json")}
|
||||
if err := writeJSONFile(cfg.path, cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &machine{t: t, config: cfg, client: &Client{URL: url, Token: token, DataDir: cfg.DataDir}, root: clone(t, origin)}
|
||||
}
|
||||
|
||||
func clone(t *testing.T, origin string) string {
|
||||
t.Helper()
|
||||
root := filepath.Join(t.TempDir(), "clone")
|
||||
git(t, origin, "clone", origin, root)
|
||||
resolved, _ := filepath.EvalSymlinks(root)
|
||||
machines[i] = &machine{t: t, root: resolved, client: &Client{URL: api.URL, Token: adminToken, DataDir: t.TempDir()}}
|
||||
return resolved
|
||||
}
|
||||
return machines[0], machines[1]
|
||||
|
||||
func setup(t *testing.T) (*machine, *machine) {
|
||||
t.Helper()
|
||||
url, origin := newServer(t), newOrigin(t)
|
||||
return newMachine(t, origin, url, adminToken), newMachine(t, origin, url, adminToken)
|
||||
}
|
||||
|
||||
func write(t *testing.T, root, path, content string) {
|
||||
@@ -70,7 +90,7 @@ func write(t *testing.T, root, path, content string) {
|
||||
|
||||
func (m *machine) sync() Result {
|
||||
m.t.Helper()
|
||||
res, err := m.client.Sync(context.Background(), m.root, true)
|
||||
res, err := m.config.Sync(context.Background(), m.root, true)
|
||||
if err != nil {
|
||||
m.t.Fatal(err)
|
||||
}
|
||||
@@ -130,7 +150,7 @@ func TestBranchNotesFollowTheBranchAndSharedStay(t *testing.T) {
|
||||
mac.sync()
|
||||
|
||||
git(t, mac.root, "checkout", "main")
|
||||
if _, err := mac.client.Sync(context.Background(), mac.root, false); err != nil {
|
||||
if _, err := mac.config.Sync(context.Background(), mac.root, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mac.expect(".agent/plan.md", "main plan")
|
||||
@@ -183,6 +203,86 @@ func TestLaterEditWinsAndLoserStaysInHistory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentEditsMerge(t *testing.T) {
|
||||
for name, versions := range map[string][4]string{
|
||||
"different lines": {
|
||||
"# plan\n\n## todo\n- a\n\n## notes\n- x\n",
|
||||
"# plan\n\n## todo\n- a\n- from mac\n\n## notes\n- x\n",
|
||||
"# plan\n\n## todo\n- a\n\n## notes\n- x\n- from dell\n",
|
||||
"# plan\n\n## todo\n- a\n- from mac\n\n## notes\n- x\n- from dell\n",
|
||||
},
|
||||
"appends": {"# log\n- a\n", "# log\n- a\n- from mac\n", "# log\n- a\n- from dell\n", "# log\n- a\n- from dell\n- from mac\n"},
|
||||
"appends without newline": {"# log\n- a", "# log\n- a\n- from mac", "# log\n- a\n- from dell", "# log\n- a\n- from dell\n- from mac"},
|
||||
"appends to unfinished line": {"# log\n- a", "# log\n- a b", "# log\n- a\n- from dell", "# log\n- a\n- from dell\n b"},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
mac, dell := setup(t)
|
||||
path := "docs/agents/plan.md"
|
||||
write(t, mac.root, path, versions[0])
|
||||
mac.sync()
|
||||
dell.sync()
|
||||
|
||||
write(t, mac.root, path, versions[1])
|
||||
write(t, dell.root, path, versions[2])
|
||||
dell.sync()
|
||||
mac.sync()
|
||||
dell.sync()
|
||||
|
||||
mac.expect(path, versions[3])
|
||||
dell.expect(path, versions[3])
|
||||
state, err := mac.client.State(context.Background(), repoKey(t, mac), "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, _ := mac.client.Blob(context.Background(), state.Spaces[protocol.SharedSpace][path].Hash); string(got) != versions[3] {
|
||||
t.Fatalf("server has %q, want %q", got, versions[3])
|
||||
}
|
||||
for _, m := range []*machine{mac, dell, mac} {
|
||||
if res := m.sync(); res.Pushed != 0 || res.Pulled != 0 {
|
||||
t.Fatalf("merged file keeps syncing: %+v", res)
|
||||
}
|
||||
}
|
||||
revisions, err := mac.client.Revisions(context.Background(), repoKey(t, mac), path)
|
||||
if err != nil || len(revisions) != 3 {
|
||||
t.Fatalf("want base, dell and one merged revision, got %d (%v)", len(revisions), err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictingOrBinaryEditsFallBackToLaterWins(t *testing.T) {
|
||||
for name, versions := range map[string][3]string{
|
||||
"same line": {"1\n2\n3\n4\n5\n", "mac\n2\n3\n4\n5\n", "dell\n2\n3\n4\n5\n"},
|
||||
"nul byte": {"1\n2\n3\n4\n5\n\x00\n", "mac\n2\n3\n4\n5\n\x00\n", "1\n2\n3\n4\ndell\n\x00\n"},
|
||||
"invalid utf8": {"1\n2\n3\n4\n5\n\xff\n", "mac\n2\n3\n4\n5\n\xff\n", "1\n2\n3\n4\ndell\n\xff\n"},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
mac, dell := setup(t)
|
||||
path := "docs/agents/plan.md"
|
||||
write(t, mac.root, path, versions[0])
|
||||
mac.sync()
|
||||
dell.sync()
|
||||
|
||||
write(t, mac.root, path, versions[1])
|
||||
old := time.Now().Add(-time.Hour)
|
||||
if err := os.Chtimes(filepath.Join(mac.root, path), old, old); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
write(t, dell.root, path, versions[2])
|
||||
dell.sync()
|
||||
mac.sync()
|
||||
dell.sync()
|
||||
|
||||
mac.expect(path, versions[2])
|
||||
dell.expect(path, versions[2])
|
||||
revisions, err := mac.client.Revisions(context.Background(), repoKey(t, mac), path)
|
||||
if err != nil || len(revisions) != 4 {
|
||||
t.Fatalf("want base, dell, losing mac and restored dell revisions, got %d (%v)", len(revisions), err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRulesFileTravelsAndHostileServerPathsAreIgnored(t *testing.T) {
|
||||
mac, dell := setup(t)
|
||||
write(t, mac.root, RulesFile, "shared notes\n")
|
||||
@@ -207,6 +307,72 @@ func TestRulesFileTravelsAndHostileServerPathsAreIgnored(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIgnoredFilesNeverTravel(t *testing.T) {
|
||||
mac, dell := setup(t)
|
||||
rules := "shared vault\nignore vault/.obsidian/workspace*.json\nignore .DS_Store\nignore vault/cache\n"
|
||||
write(t, mac.root, RulesFile, rules)
|
||||
write(t, mac.root, "vault/note.md", "note")
|
||||
write(t, mac.root, "vault/.obsidian/app.json", "settings")
|
||||
write(t, mac.root, "vault/.obsidian/workspace.json", "mac layout")
|
||||
write(t, mac.root, "vault/.obsidian/workspace-mobile.json", "mac mobile")
|
||||
write(t, mac.root, "vault/deep/.DS_Store", "finder")
|
||||
write(t, mac.root, "vault/cache/index.bin", "cache")
|
||||
if res := mac.sync(); res.Pushed != 3 {
|
||||
t.Fatalf("pushed %d files, want the rules, the note and app.json", res.Pushed)
|
||||
}
|
||||
|
||||
write(t, dell.root, "vault/.obsidian/workspace.json", "dell layout")
|
||||
dell.sync()
|
||||
dell.expect("vault/note.md", "note")
|
||||
dell.expect("vault/.obsidian/app.json", "settings")
|
||||
dell.expect("vault/.obsidian/workspace.json", "dell layout")
|
||||
dell.expect("vault/.obsidian/workspace-mobile.json", "")
|
||||
dell.expect("vault/deep/.DS_Store", "")
|
||||
dell.expect("vault/cache/index.bin", "")
|
||||
|
||||
hash, _ := mac.client.cacheBlob([]byte("pushed by an old client"))
|
||||
if err := mac.client.uploadBlob(context.Background(), hash); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
push := protocol.Push{Changes: []protocol.Change{{Space: protocol.SharedSpace, Path: "vault/.obsidian/workspace.json", Hash: hash}}}
|
||||
if err := mac.client.call(context.Background(), http.MethodPost, "/api/repos/"+repoKey(t, mac)+"/revisions", push, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mac.sync()
|
||||
dell.sync()
|
||||
mac.expect("vault/.obsidian/workspace.json", "mac layout")
|
||||
dell.expect("vault/.obsidian/workspace.json", "dell layout")
|
||||
|
||||
exclude, _ := os.ReadFile(filepath.Join(dell.root, ".git/info/exclude"))
|
||||
if strings.Contains(string(exclude), "workspace") || strings.Contains(string(exclude), "DS_Store") {
|
||||
t.Fatalf("ignore rules leaked into exclude:\n%s", exclude)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIgnoreRuleMatching(t *testing.T) {
|
||||
rules := parseRules("shared vault\nbranch .agent\nignore vault/.obsidian/workspace*.json\nignore .DS_Store\nignore *.tmp\nignore .lodge\n")
|
||||
for file, want := range map[string]bool{
|
||||
"vault/note.md": true,
|
||||
"vault/.obsidian/workspace.json": false,
|
||||
"vault/.obsidian/workspace-mobile.json": false,
|
||||
"vault/.obsidian/workspaces.json": false,
|
||||
"vault/.obsidian/app.json": true,
|
||||
"vault/sub/.obsidian/workspace.json": true,
|
||||
"vault/.DS_Store": false,
|
||||
"vault/a/b/.DS_Store": false,
|
||||
"vault/.DS_Store/inside": false,
|
||||
"vault/draft.tmp": false,
|
||||
RulesFile: true,
|
||||
} {
|
||||
if got := rules.covers(file, true); got != want {
|
||||
t.Errorf("covers(%s) = %v, want %v", file, got, want)
|
||||
}
|
||||
}
|
||||
if rules.covers(".agent/.DS_Store", false) || !rules.covers(".agent/plan.md", false) {
|
||||
t.Error("ignore rules must apply to branch paths too")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletingRepositoryOnServerKeepsFilesOnMachines(t *testing.T) {
|
||||
mac, _ := setup(t)
|
||||
write(t, mac.root, "docs/agents/plan.md", "v1")
|
||||
|
||||
@@ -32,6 +32,7 @@ type Change struct {
|
||||
|
||||
type Push struct {
|
||||
Name string `json:"name"`
|
||||
Author string `json:"author,omitempty"`
|
||||
MainSHA string `json:"main_sha"`
|
||||
MainBranch string `json:"main_branch"`
|
||||
Changes []Change `json:"changes"`
|
||||
@@ -40,6 +41,7 @@ type Push struct {
|
||||
type Revision struct {
|
||||
Seq int64 `json:"seq"`
|
||||
Machine string `json:"machine"`
|
||||
Author string `json:"author"`
|
||||
Time int64 `json:"time"`
|
||||
MainSHA string `json:"main_sha"`
|
||||
MainBranch string `json:"main_branch"`
|
||||
@@ -58,6 +60,8 @@ type Machine struct {
|
||||
Name string `json:"name"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
LastPush int64 `json:"last_push"`
|
||||
LastSeen int64 `json:"last_seen"`
|
||||
Online bool `json:"online"`
|
||||
Revisions int64 `json:"revisions"`
|
||||
}
|
||||
|
||||
@@ -68,6 +72,7 @@ type Login struct {
|
||||
type Event struct {
|
||||
Repo string `json:"repo"`
|
||||
Seq int64 `json:"seq"`
|
||||
Author string `json:"author,omitempty"`
|
||||
}
|
||||
|
||||
type ClaimRequest struct {
|
||||
@@ -82,6 +87,7 @@ type ClaimResponse struct {
|
||||
type CodeResponse struct {
|
||||
Code string `json:"code"`
|
||||
Command string `json:"command"`
|
||||
RemoteCommand string `json:"remote_command"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
url="{{URL}}"
|
||||
code="{{CODE}}"
|
||||
mode="{{MODE}}"
|
||||
|
||||
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
arch=$(uname -m)
|
||||
case "$arch" in
|
||||
@@ -11,12 +15,26 @@ esac
|
||||
|
||||
bin="$HOME/.local/bin"
|
||||
mkdir -p "$bin"
|
||||
curl -fsSL "{{URL}}/dl/lodge-$os-$arch" -o "$bin/lodge.new"
|
||||
curl -fsSL "$url/dl/lodge-$os-$arch" -o "$bin/lodge.new"
|
||||
chmod +x "$bin/lodge.new"
|
||||
mv "$bin/lodge.new" "$bin/lodge"
|
||||
|
||||
"$bin/lodge" login "{{URL}}" "{{CODE}}"
|
||||
if [ "$mode" = login ]; then
|
||||
"$bin/lodge" login "$url" "$code"
|
||||
"$bin/lodge" install
|
||||
else
|
||||
plist="$HOME/Library/LaunchAgents/com.kotikot.lodge.plist"
|
||||
unit="$HOME/.config/systemd/user/lodge.service"
|
||||
restarted=yes
|
||||
if [ -f "$plist" ]; then
|
||||
launchctl kickstart -k "gui/$(id -u)/com.kotikot.lodge" || restarted=no
|
||||
elif [ -f "$unit" ]; then
|
||||
systemctl --user restart lodge.service || restarted=no
|
||||
fi
|
||||
[ "$restarted" = yes ] || echo "lodge: restart the running daemon so it runs the new binary" >&2
|
||||
(cd / && "$bin/lodge" remote "$url" "$code")
|
||||
[ -f "$plist" ] || [ -f "$unit" ] || "$bin/lodge" install
|
||||
fi
|
||||
|
||||
case ":$PATH:" in
|
||||
*":$bin:"*) ;;
|
||||
|
||||
@@ -50,14 +50,17 @@ type Server struct {
|
||||
|
||||
mu sync.Mutex
|
||||
subscribers map[chan protocol.Event]struct{}
|
||||
streams map[int64]int
|
||||
seen map[int64]int64
|
||||
}
|
||||
|
||||
type machineKey struct{}
|
||||
|
||||
func New(store *Store, config Config) http.Handler {
|
||||
s := &Server{config: config, store: store, subscribers: map[chan protocol.Event]struct{}{}}
|
||||
s := &Server{config: config, store: store, subscribers: map[chan protocol.Event]struct{}{}, streams: map[int64]int{}, seen: map[int64]int64{}}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /i/{code}", s.install)
|
||||
mux.HandleFunc("GET /i/{code}", s.install("login"))
|
||||
mux.HandleFunc("GET /i/{code}/remote", s.install("remote"))
|
||||
mux.Handle("GET /dl/", http.StripPrefix("/dl/", http.FileServer(http.Dir(config.ClientsDir))))
|
||||
mux.HandleFunc("POST /api/claim", s.claim)
|
||||
mux.HandleFunc("POST /api/session", s.login)
|
||||
@@ -136,6 +139,7 @@ func (s *Server) auth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
machineID = id
|
||||
s.track(id, 0)
|
||||
}
|
||||
if key := r.PathValue("key"); key != "" && !repoKeyPattern.MatchString(key) {
|
||||
http.Error(w, "bad repository key", http.StatusBadRequest)
|
||||
@@ -156,14 +160,16 @@ func (s *Server) publicURL(r *http.Request) string {
|
||||
return scheme + "://" + r.Host
|
||||
}
|
||||
|
||||
func (s *Server) install(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) install(mode string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
code := r.PathValue("code")
|
||||
if !codePattern.MatchString(code) || !s.store.CodeValid(r.Context(), code) {
|
||||
http.Error(w, "echo 'lodge: this install link has expired, create a new one' >&2; exit 1", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/x-shellscript")
|
||||
_, _ = strings.NewReplacer("{{URL}}", s.publicURL(r), "{{CODE}}", code).WriteString(w, installScript)
|
||||
_, _ = strings.NewReplacer("{{URL}}", s.publicURL(r), "{{CODE}}", code, "{{MODE}}", mode).WriteString(w, installScript)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) setSession(w http.ResponseWriter, r *http.Request, token string, expires time.Time) {
|
||||
@@ -206,12 +212,25 @@ func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) track(machineID int64, streams int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.seen[machineID] = time.Now().UnixMilli()
|
||||
s.streams[machineID] += streams
|
||||
}
|
||||
|
||||
func (s *Server) machines(w http.ResponseWriter, r *http.Request) {
|
||||
machines, err := s.store.Machines(r.Context())
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
for i, machine := range machines {
|
||||
machines[i].Online = s.streams[machine.ID] > 0
|
||||
machines[i].LastSeen = max(s.seen[machine.ID], machine.LastPush)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
writeJSON(w, http.StatusOK, machines)
|
||||
}
|
||||
|
||||
@@ -250,6 +269,7 @@ func (s *Server) createCode(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusCreated, protocol.CodeResponse{
|
||||
Code: code,
|
||||
Command: fmt.Sprintf("curl -fsSL %s/i/%s | sh", s.publicURL(r), code),
|
||||
RemoteCommand: fmt.Sprintf("curl -fsSL %s/i/%s/remote | sh", s.publicURL(r), code),
|
||||
ExpiresAt: expires.UnixMilli(),
|
||||
})
|
||||
}
|
||||
@@ -287,7 +307,19 @@ func (s *Server) state(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) revisions(w http.ResponseWriter, r *http.Request) {
|
||||
revisions, err := s.store.Revisions(r.Context(), r.PathValue("key"), r.URL.Query().Get("path"))
|
||||
key, query := r.PathValue("key"), r.URL.Query()
|
||||
var revisions []protocol.Revision
|
||||
var err error
|
||||
if query.Has("after") {
|
||||
after, parseErr := strconv.ParseInt(query.Get("after"), 10, 64)
|
||||
if parseErr != nil {
|
||||
http.Error(w, "bad after", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
revisions, err = s.store.RevisionsAfter(r.Context(), key, query.Get("path"), after)
|
||||
} else {
|
||||
revisions, err = s.store.Revisions(r.Context(), key, query.Get("path"))
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
@@ -319,8 +351,9 @@ func (s *Server) push(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
s.publish(protocol.Event{Repo: key, Seq: seq})
|
||||
writeJSON(w, http.StatusCreated, protocol.Event{Repo: key, Seq: seq})
|
||||
event := protocol.Event{Repo: key, Seq: seq, Author: push.Author}
|
||||
s.publish(event)
|
||||
writeJSON(w, http.StatusCreated, event)
|
||||
}
|
||||
|
||||
func (s *Server) blob(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -370,14 +403,17 @@ func (s *Server) events(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
machineID, _ := r.Context().Value(machineKey{}).(int64)
|
||||
subscriber := make(chan protocol.Event, 16)
|
||||
s.mu.Lock()
|
||||
s.subscribers[subscriber] = struct{}{}
|
||||
s.mu.Unlock()
|
||||
s.track(machineID, 1)
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
delete(s.subscribers, subscriber)
|
||||
s.mu.Unlock()
|
||||
s.track(machineID, -1)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
@@ -391,6 +427,7 @@ func (s *Server) events(w http.ResponseWriter, r *http.Request) {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.track(machineID, 0)
|
||||
fmt.Fprint(w, ": ping\n\n")
|
||||
case event := <-subscriber:
|
||||
data, _ := json.Marshal(event)
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"lodge/internal/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
adminToken = "admin"
|
||||
repoKey = "0123456789abcdef0123456789abcdef01234567"
|
||||
)
|
||||
|
||||
func newServer(t *testing.T) string {
|
||||
t.Helper()
|
||||
store, err := OpenStore(filepath.Join(t.TempDir(), "lodge.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { store.Close() })
|
||||
api := httptest.NewServer(New(store, Config{AdminToken: adminToken}))
|
||||
t.Cleanup(api.Close)
|
||||
return api.URL
|
||||
}
|
||||
|
||||
func call(t *testing.T, method, url, token string, in, out any) {
|
||||
t.Helper()
|
||||
var body bytes.Buffer
|
||||
if in != nil {
|
||||
_ = json.NewEncoder(&body).Encode(in)
|
||||
}
|
||||
req, _ := http.NewRequest(method, url, &body)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode >= http.StatusBadRequest {
|
||||
t.Fatalf("%s %s: %s", method, url, res.Status)
|
||||
}
|
||||
if out != nil {
|
||||
if err := json.NewDecoder(res.Body).Decode(out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func push(t *testing.T, url, author, path, base, content string) string {
|
||||
t.Helper()
|
||||
sum := sha256.Sum256([]byte(content))
|
||||
hash := hex.EncodeToString(sum[:])
|
||||
req, _ := http.NewRequest(http.MethodPut, url+"/api/blobs/"+hash, strings.NewReader(content))
|
||||
req.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res.Body.Close()
|
||||
change := protocol.Change{Space: protocol.SharedSpace, Path: path, Base: base, Hash: hash}
|
||||
call(t, http.MethodPost, url+"/api/repos/"+repoKey+"/revisions", adminToken, protocol.Push{Name: "vault", Author: author, Changes: []protocol.Change{change}}, nil)
|
||||
return hash
|
||||
}
|
||||
|
||||
func revisions(t *testing.T, url, query string) []protocol.Revision {
|
||||
t.Helper()
|
||||
var revisions []protocol.Revision
|
||||
call(t, http.MethodGet, url+"/api/repos/"+repoKey+"/revisions"+query, adminToken, nil, &revisions)
|
||||
return revisions
|
||||
}
|
||||
|
||||
func seqs(revisions []protocol.Revision) []int64 {
|
||||
seqs := make([]int64, len(revisions))
|
||||
for i, rev := range revisions {
|
||||
seqs[i] = rev.Seq
|
||||
}
|
||||
return seqs
|
||||
}
|
||||
|
||||
func TestRevisionsCarryAuthorAndPageAfterSeq(t *testing.T) {
|
||||
url := newServer(t)
|
||||
events, err := http.NewRequest(http.MethodGet, url+"/api/events", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
events.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
stream, err := http.DefaultClient.Do(events)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stream.Body.Close()
|
||||
|
||||
first := push(t, url, "", "a.md", "", "one")
|
||||
push(t, url, "alice", "a.md", first, "two")
|
||||
push(t, url, "bob", "b.md", "", "three")
|
||||
|
||||
if got := seqs(revisions(t, url, "")); len(got) != 3 || got[0] != 3 {
|
||||
t.Fatalf("default order = %v, want newest first", got)
|
||||
}
|
||||
after := revisions(t, url, "?after=1")
|
||||
if got := seqs(after); len(got) != 2 || got[0] != 2 || got[1] != 3 {
|
||||
t.Fatalf("after=1 = %v, want [2 3]", got)
|
||||
}
|
||||
if after[0].Author != "alice" || after[0].Machine != "web" || revisions(t, url, "?after=0")[0].Author != "" {
|
||||
t.Fatalf("authors = %+v", after)
|
||||
}
|
||||
if got := seqs(revisions(t, url, "?after=0&path=b.md")); len(got) != 1 || got[0] != 3 {
|
||||
t.Fatalf("after with path = %v", got)
|
||||
}
|
||||
if got := revisions(t, url, "?after=3"); len(got) != 0 {
|
||||
t.Fatalf("after the last revision = %v", got)
|
||||
}
|
||||
|
||||
var authors []string
|
||||
scanner := bufio.NewScanner(stream.Body)
|
||||
for len(authors) < 3 && scanner.Scan() {
|
||||
if data, ok := strings.CutPrefix(scanner.Text(), "data: "); ok {
|
||||
var event protocol.Event
|
||||
_ = json.Unmarshal([]byte(data), &event)
|
||||
authors = append(authors, event.Author)
|
||||
}
|
||||
}
|
||||
if strings.Join(authors, ",") != ",alice,bob" {
|
||||
t.Fatalf("event authors = %q", authors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOldDatabaseGainsAuthorColumn(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "lodge.db")
|
||||
db, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = db.Exec(`
|
||||
create table revisions (repo text not null, seq integer not null, machine_id integer, time integer not null, main_sha text not null, main_branch text not null, primary key (repo, seq));
|
||||
create table changes (repo text not null, seq integer not null, space text not null, path text not null, base text not null, hash text not null, primary key (repo, seq, space, path));
|
||||
insert into revisions values ('` + repoKey + `', 1, null, 1, '', 'main');
|
||||
insert into changes values ('` + repoKey + `', 1, 'shared', 'a.md', '', 'x');`)
|
||||
if err := errors.Join(err, db.Close()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for range 2 {
|
||||
store, err := OpenStore(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
revisions, err := store.Revisions(context.Background(), repoKey, "")
|
||||
store.Close()
|
||||
if err != nil || len(revisions) != 1 || revisions[0].Author != "" {
|
||||
t.Fatalf("old revisions = %+v (%v)", revisions, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMachinesShowWhoIsOnline(t *testing.T) {
|
||||
url := newServer(t)
|
||||
var code protocol.CodeResponse
|
||||
call(t, http.MethodPost, url+"/api/codes", adminToken, nil, &code)
|
||||
var claim protocol.ClaimResponse
|
||||
call(t, http.MethodPost, url+"/api/claim", "", protocol.ClaimRequest{Code: code.Code, Name: "dell"}, &claim)
|
||||
online := func() protocol.Machine {
|
||||
t.Helper()
|
||||
var machines []protocol.Machine
|
||||
call(t, http.MethodGet, url+"/api/machines", adminToken, nil, &machines)
|
||||
if len(machines) != 1 {
|
||||
t.Fatalf("machines = %+v", machines)
|
||||
}
|
||||
return machines[0]
|
||||
}
|
||||
if machine := online(); machine.Online || machine.LastSeen != 0 {
|
||||
t.Fatalf("a machine that never connected: %+v", machine)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url+"/api/events", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+claim.Token)
|
||||
stream, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if machine := online(); !machine.Online || machine.LastSeen == 0 {
|
||||
t.Fatalf("a machine listening for events: %+v", machine)
|
||||
}
|
||||
cancel()
|
||||
stream.Body.Close()
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for online().Online {
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("the machine stays online after its stream closed")
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallScriptModes(t *testing.T) {
|
||||
url := newServer(t)
|
||||
var code protocol.CodeResponse
|
||||
call(t, http.MethodPost, url+"/api/codes", adminToken, nil, &code)
|
||||
if code.RemoteCommand != "curl -fsSL "+url+"/i/"+code.Code+"/remote | sh" {
|
||||
t.Fatalf("remote command = %q", code.RemoteCommand)
|
||||
}
|
||||
for path, mode := range map[string]string{"/i/" + code.Code: "login", "/i/" + code.Code + "/remote": "remote"} {
|
||||
res, err := http.Get(url + path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
script, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK || !strings.Contains(string(script), `mode="`+mode+`"`) || !strings.Contains(string(script), `code="`+code.Code+`"`) {
|
||||
t.Fatalf("%s: %d\n%s", path, res.StatusCode, script)
|
||||
}
|
||||
check := exec.Command("sh", "-n")
|
||||
check.Stdin = bytes.NewReader(script)
|
||||
if out, err := check.CombinedOutput(); err != nil {
|
||||
t.Fatalf("%s is not valid sh: %v %s", path, err, out)
|
||||
}
|
||||
}
|
||||
res, err := http.Get(url + "/i/abcdef/remote")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res.Body.Close()
|
||||
if res.StatusCode != http.StatusNotFound {
|
||||
t.Fatalf("unknown code: %d", res.StatusCode)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,11 @@ const (
|
||||
codeLifetime = 10 * time.Minute
|
||||
sessionLifetime = 30 * 24 * time.Hour
|
||||
revisionLimit = 500
|
||||
revisionRows = `
|
||||
select r.seq, coalesce(m.name, iif(r.machine_id is null, 'web', 'revoked')), r.author, r.time, r.main_sha, r.main_branch, c.space, c.path, c.base, c.hash
|
||||
from revisions r
|
||||
join changes c on c.repo = r.repo and c.seq = r.seq
|
||||
left join machines m on m.id = r.machine_id`
|
||||
schema = `
|
||||
create table if not exists machines (
|
||||
id integer primary key,
|
||||
@@ -49,6 +54,7 @@ create table if not exists revisions (
|
||||
repo text not null,
|
||||
seq integer not null,
|
||||
machine_id integer,
|
||||
author text not null default '',
|
||||
time integer not null,
|
||||
main_sha text not null,
|
||||
main_branch text not null,
|
||||
@@ -86,9 +92,21 @@ func OpenStore(path string) (*Store, error) {
|
||||
if _, err := db.Exec(schema); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := addAuthor(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Store{db: db}, nil
|
||||
}
|
||||
|
||||
func addAuthor(db *sql.DB) error {
|
||||
var found int
|
||||
if err := db.QueryRow(`select count(*) from pragma_table_info('revisions') where name = 'author'`).Scan(&found); err != nil || found > 0 {
|
||||
return err
|
||||
}
|
||||
_, err := db.Exec(`alter table revisions add column author text not null default ''`)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) Close() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
@@ -292,14 +310,24 @@ func (s *Store) State(ctx context.Context, repo, at string) (protocol.State, err
|
||||
}
|
||||
|
||||
func (s *Store) Revisions(ctx context.Context, repo, path string) ([]protocol.Revision, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
select r.seq, coalesce(m.name, iif(r.machine_id is null, 'web', 'revoked')), r.time, r.main_sha, r.main_branch, c.space, c.path, c.base, c.hash
|
||||
from revisions r
|
||||
join changes c on c.repo = r.repo and c.seq = r.seq
|
||||
left join machines m on m.id = r.machine_id
|
||||
return s.revisions(ctx, revisionRows+`
|
||||
where r.repo = ?1 and (?2 = '' or c.path = ?2)
|
||||
order by r.seq desc, c.path
|
||||
limit ?3`, repo, path, revisionLimit)
|
||||
}
|
||||
|
||||
func (s *Store) RevisionsAfter(ctx context.Context, repo, path string, after int64) ([]protocol.Revision, error) {
|
||||
return s.revisions(ctx, revisionRows+`
|
||||
where r.repo = ?1 and (?2 = '' or c.path = ?2) and r.seq in (
|
||||
select distinct seq from changes
|
||||
where repo = ?1 and (?2 = '' or path = ?2) and seq > ?4
|
||||
order by seq limit ?3
|
||||
)
|
||||
order by r.seq, c.path`, repo, path, revisionLimit, after)
|
||||
}
|
||||
|
||||
func (s *Store) revisions(ctx context.Context, query string, args ...any) ([]protocol.Revision, error) {
|
||||
rows, err := s.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -308,7 +336,7 @@ func (s *Store) Revisions(ctx context.Context, repo, path string) ([]protocol.Re
|
||||
for rows.Next() {
|
||||
var rev protocol.Revision
|
||||
var change protocol.Change
|
||||
if err := rows.Scan(&rev.Seq, &rev.Machine, &rev.Time, &rev.MainSHA, &rev.MainBranch, &change.Space, &change.Path, &change.Base, &change.Hash); err != nil {
|
||||
if err := rows.Scan(&rev.Seq, &rev.Machine, &rev.Author, &rev.Time, &rev.MainSHA, &rev.MainBranch, &change.Space, &change.Path, &change.Base, &change.Hash); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if last := len(revisions) - 1; last >= 0 && revisions[last].Seq == rev.Seq {
|
||||
@@ -356,7 +384,7 @@ func (s *Store) Push(ctx context.Context, repo string, machineID int64, push pro
|
||||
return 0, err
|
||||
}
|
||||
machine := sql.NullInt64{Int64: machineID, Valid: machineID != 0}
|
||||
if _, err := tx.ExecContext(ctx, `insert into revisions (repo, seq, machine_id, time, main_sha, main_branch) values (?, ?, ?, ?, ?, ?)`, repo, seq, machine, now, push.MainSHA, push.MainBranch); err != nil {
|
||||
if _, err := tx.ExecContext(ctx, `insert into revisions (repo, seq, machine_id, author, time, main_sha, main_branch) values (?, ?, ?, ?, ?, ?, ?)`, repo, seq, machine, push.Author, now, push.MainSHA, push.MainBranch); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for _, change := range push.Changes {
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
.env
|
||||
Caddyfile
|
||||
/Caddyfile
|
||||
+1
-20
@@ -1,22 +1,3 @@
|
||||
# caddy
|
||||
|
||||
`site.caddy` routes `{$LODGE_DOMAIN}` to the project's services over the external `caddy` docker network. It works standalone and inside a shared Caddy that fronts several projects.
|
||||
|
||||
Standalone:
|
||||
|
||||
```sh
|
||||
docker network create caddy
|
||||
cp Caddyfile.example Caddyfile
|
||||
cp .env.example .env
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Shared Caddy: mount this directory as `/etc/caddy/projects.d/lodge:ro`, keep `import /etc/caddy/projects.d/*/site.caddy` in its Caddyfile and put `LODGE_DOMAIN` in its environment.
|
||||
|
||||
Rules that keep `site.caddy` portable:
|
||||
|
||||
- the domain comes only from the environment variable
|
||||
- no `tls` block, certificates are configured by the host Caddy (`acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}` in the global block)
|
||||
- snippets are prefixed with the project slug because Caddy snippets are global
|
||||
- mount the directory, not the file: a single-file bind mount goes stale when git replaces the inode
|
||||
- keep `admin localhost:2019`, `admin off` makes `caddy reload` impossible
|
||||
`site.caddy` routes `{$LODGE_DOMAIN}` to the project over the external `caddy` network. In a shared Caddy, mount this directory as `/etc/caddy/projects.d/lodge:ro` and set `LODGE_DOMAIN`. Keep it portable: domain only from the env, no `tls`, snippets prefixed with `lodge_`.
|
||||
@@ -1,5 +1,7 @@
|
||||
node_modules
|
||||
.svelte-kit
|
||||
build
|
||||
.env
|
||||
# .env goes into the build: $env/static/public (and static/private) are
|
||||
# inlined at build time from it. It stays in the builder stage, the runtime
|
||||
# image gets build/ only. Local overrides (.env.local and the like) stay out.
|
||||
.env.*
|
||||
+1
-11
@@ -1,8 +1,6 @@
|
||||
COMPOSE := cd .. && docker compose
|
||||
changed = ! git diff --quiet $(1) -- $(2) 2>/dev/null
|
||||
stamp = { git update-ref $(1) HEAD 2>/dev/null || true; }
|
||||
|
||||
.PHONY: fmt check build pre-deploy post-deploy dev
|
||||
.PHONY: fmt check build dev
|
||||
|
||||
fmt:
|
||||
bun run fix
|
||||
@@ -14,13 +12,5 @@ check:
|
||||
build:
|
||||
$(COMPOSE) run --rm frontend-dev sh -c "bun install && bun run build"
|
||||
|
||||
pre-deploy:
|
||||
@if $(call changed,refs/deploy/frontend,.) || ! test -d build; then \
|
||||
$(MAKE) build && $(call stamp,refs/deploy/frontend); \
|
||||
fi
|
||||
|
||||
post-deploy:
|
||||
@:
|
||||
|
||||
dev:
|
||||
bun run dev
|
||||
@@ -25,6 +25,7 @@
|
||||
"includes": ["**/*.svelte"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"a11y": { "useAnchorContent": "off" },
|
||||
"style": {
|
||||
"useFilenamingConvention": {
|
||||
"level": "error",
|
||||
|
||||
+117
-171
@@ -5,13 +5,13 @@
|
||||
"": {
|
||||
"name": "lodge",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.5.2",
|
||||
"@biomejs/biome": "^2.5.15",
|
||||
"@fontsource-variable/archivo": "^5.3.0",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
||||
"@internationalized/date": "^3.12.0",
|
||||
"@lucide/svelte": "^1.23.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.63.0",
|
||||
"@sveltejs/adapter-static": "^4.0.0",
|
||||
"@sveltejs/kit": "^3.0.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@types/bun": "^1.3.0",
|
||||
@@ -22,60 +22,50 @@
|
||||
"marked": "^18.0.13",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"shadcn-svelte": "^1.3.0",
|
||||
"svelte": "^5.56.1",
|
||||
"svelte-check": "^4.6.0",
|
||||
"svelte": "^5.57.1",
|
||||
"svelte-check": "^4.7.6",
|
||||
"svelte-sonner": "^1.1.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^6.0.3",
|
||||
"ultracite": "7.9.0",
|
||||
"ultracite": "^7.12.4",
|
||||
"vite": "^8.0.16",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@biomejs/biome": ["@biomejs/biome@2.5.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.2", "@biomejs/cli-darwin-x64": "2.5.2", "@biomejs/cli-linux-arm64": "2.5.2", "@biomejs/cli-linux-arm64-musl": "2.5.2", "@biomejs/cli-linux-x64": "2.5.2", "@biomejs/cli-linux-x64-musl": "2.5.2", "@biomejs/cli-win32-arm64": "2.5.2", "@biomejs/cli-win32-x64": "2.5.2" }, "bin": { "biome": "bin/biome" } }, "sha512-VQ3RCqr7JmDIX+w6stWYl+g/3bYofN3q2wDBHUKKc/c7i5QWrFKFBZYCYPWTE6agsUPMIZZe6/CMmVUfUAhkKA=="],
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-e7P3P7EkwFc/KiX2AHw4YDLIBOMfG9CPCAwy52k5Bp0dfhkozx9hf6wCmIr2QeXy2XeccJ3V/Sg+hDmzYEqxSg=="],
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ymzMvjC1Jg0b9K0D26ZdARqFQXs7MocfLC5FOCGfkC0Ss+ACUJkX5364ZM5nT4NLZanHRZNVrZEy+Ibwcvux/g=="],
|
||||
"@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-t7sseOmqND57uUWTwlawU6BYj+J06T/9EkydzBhkrgw/FK3QVhjU2wsJR0frljrKZ0/I8A/rYw7284QgqjQfIQ=="],
|
||||
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-w+ANG0ZvTu9IeEg9QnstoOnk6L0fpwJifW6aHR18+cb5Z39bkANItYjAfMrnvce5tmMK+IQ6nPX7/kQFdam5iw=="],
|
||||
"@biomejs/biome": ["@biomejs/biome@2.5.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.15", "@biomejs/cli-darwin-x64": "2.5.15", "@biomejs/cli-linux-arm64": "2.5.15", "@biomejs/cli-linux-arm64-musl": "2.5.15", "@biomejs/cli-linux-x64": "2.5.15", "@biomejs/cli-linux-x64-musl": "2.5.15", "@biomejs/cli-win32-arm64": "2.5.15", "@biomejs/cli-win32-x64": "2.5.15" }, "bin": { "biome": "bin/biome" } }, "sha512-WZTW4slm/pdkh92K6t/3aEN++44JD1PQ7squ7RCMLI1flHGl43DVOIGXvCjHiZgBMO1V4uYxoFNtqrIA4qGuIQ=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-M/lOZrewzTCRDINbjhQ1gYYru37KlD3kJBQwwKCG0ckz5E9IZwIoJ3X0wBwRXA+yBDIwWUuPBHS67HzJY4dTfA=="],
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BZVzFhJ/mUvTLMYbc9x6el0o2Uv5zP7bACYyhFb6fSc1giroXm7PL0a5KqMJ9F+/BnkYRjagD/ROSDVOZ6eapg=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-VArNLAzND063tF+XY0yPyM+DyahpzOMzOAvb7qs259nhjJWRjvjZdssuA+Rfl+l07+NOesKZ0Xu2yFrXyBMtzw=="],
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-V5Kw63V+fVGNFhFrizDxMbGXAzZCh8kYzJpy3GI6gTapWDg7bZK9oYC9CbVgQSEdTnupz0U5Efz8Ev+5vdBBXw=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-kbjFFKyZlzYnAuw7sRy5qDoFG6zrP40UK08oPQsWK0ct3NMnGSt+Bs1iviEEyEIP57N5MrykGXdO/wRiaR4lww=="],
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-XaG7P7eeSLYETD3K9grfB0mQmpLQZjygdbsxekWBJAg4am79fGAtRpfRyNnTvXB2fZFNnJZG3nXm7HBat0+VUw=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-4InchVpdVmdkkkgjQqKpgvyu+VPnoF/7RPSw5YATgEVpt2j72wcCAeV5TwaE9ZGJUZWZn7v2CwSAj6CrMJEx8A=="],
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-tGzZUTcJCV7tj5Adh/Gn6nR4MycqA3iohq2LEcrarAgRBkU0h0OKd2UzCRkOsOYOuUGaUJb4YmqRqyy2feGGSw=="],
|
||||
|
||||
"@cacheable/memory": ["@cacheable/memory@2.2.0", "", { "dependencies": { "@cacheable/utils": "^2.5.0", "@keyv/bigmap": "^1.3.1", "hookified": "^1.15.1", "keyv": "^5.6.0" } }, "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ=="],
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.15", "", { "os": "linux", "cpu": "x64" }, "sha512-xE4iEW/3LqlYj9GFgGrFsFSH51dEEpUbYWBfeOv0q87WkUDxK2o/HhreSb7qMJqck70RVM6Lg96hgedcAYcOkA=="],
|
||||
|
||||
"@cacheable/utils": ["@cacheable/utils@2.5.0", "", { "dependencies": { "hashery": "^1.5.1", "keyv": "^5.6.0" } }, "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA=="],
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.15", "", { "os": "linux", "cpu": "x64" }, "sha512-IlxUcyxilVGPsE008x13pWdkTbU3nQpP2i9b5UrbOYj6goBkKsRX1XB7yJLcV+O1H9LBchIIm4adOaVLZBX0ZQ=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-2kPKzhNlm8C+Ru3GcO0Me2ODkCBLrVOUNuyi84RJyWDVKAA4+Kjj3jMjL938lF6BRfvltC4vB2nPY9SvN51Ovg=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.15", "", { "os": "win32", "cpu": "x64" }, "sha512-yAzh4UqEImV6Hcy0zjUqNfJAsUhoD64FBQCdTG8Md/Z2wQeVP/ZmJ3XRYAs1g8J9fjTFyl2fCtc5FB5til47+g=="],
|
||||
|
||||
"@clack/core": ["@clack/core@1.5.1", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ=="],
|
||||
|
||||
"@clack/prompts": ["@clack/prompts@1.8.1", "", { "dependencies": { "@clack/core": "1.5.1", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.7.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.3", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
|
||||
@@ -86,16 +76,6 @@
|
||||
|
||||
"@fontsource-variable/jetbrains-mono": ["@fontsource-variable/jetbrains-mono@5.3.0", "", {}, "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
|
||||
|
||||
"@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
|
||||
|
||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||
|
||||
"@internationalized/date": ["@internationalized/date@3.12.4", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
@@ -108,12 +88,14 @@
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@keyv/bigmap": ["@keyv/bigmap@1.3.1", "", { "dependencies": { "hashery": "^1.4.0", "hookified": "^1.15.0" }, "peerDependencies": { "keyv": "^5.6.0" } }, "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ=="],
|
||||
|
||||
"@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="],
|
||||
|
||||
"@lucide/svelte": ["@lucide/svelte@1.47.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-tpuE7JIK9FzYDWsJx6PxHj9ROyCpnDxZfvHu5q5yHGKS2CFNjzSyf1o9/Ypu2A27GLvkPGqZcIMrF6idM+eQZQ=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.150.0", "", {}, "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw=="],
|
||||
|
||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||
@@ -150,13 +132,17 @@
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
|
||||
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
||||
|
||||
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.13", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ=="],
|
||||
|
||||
"@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="],
|
||||
"@sveltejs/adapter-static": ["@sveltejs/adapter-static@4.0.0", "", { "peerDependencies": { "@sveltejs/kit": "^3.0.0-next.0" } }, "sha512-aF1J23R1ebKqUxQ4/qLr/Lxu63NFmLAtg8UZl3HFLxaoA1D/nhTVGcD9lTqx65iUbjNR2iOL0TgOxSX82x6Paw=="],
|
||||
|
||||
"@sveltejs/kit": ["@sveltejs/kit@2.70.3", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg=="],
|
||||
"@sveltejs/kit": ["@sveltejs/kit@3.0.1", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1", "@standard-schema/spec": "^1.1.0", "cookie": "^2.0.1", "devalue": "^5.9.4", "esm-env": "^1.2.2", "magic-string": "^1.2.3", "mrmime": "^2.0.1", "sirv": "^3.0.2" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", "svelte": "^5.57.1", "typescript": "^6.0.0", "vite": "^8.0.12" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-8JMCCq7ZUJELi3hbhb0+c9Rs8qnbHW90vhOfM+t1FSF4fhIpG9/FOsH4IYgcITMhbdkSrrn+Kvd9G+q8A+UO6Q=="],
|
||||
|
||||
"@sveltejs/load-config": ["@sveltejs/load-config@0.2.3", "", {}, "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ=="],
|
||||
|
||||
@@ -196,69 +182,47 @@
|
||||
|
||||
"@types/bun": ["@types/bun@1.4.2", "", { "dependencies": { "bun-types": "1.4.2" } }, "sha512-GimotNn7+ZV0uVArItBbriZsR1oNf0+WTzPkdcFrzShI7k2norL0uzEaJT8T33dWr7O/c9ZDuAFQrctKCi72oQ=="],
|
||||
|
||||
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
||||
|
||||
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@26.6.2", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.70.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.70.0", "@typescript-eslint/types": "^8.70.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.70.0", "", { "dependencies": { "@typescript-eslint/types": "8.70.0", "@typescript-eslint/visitor-keys": "8.70.0" } }, "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ=="],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.70.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.70.0", "", {}, "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.70.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.70.0", "@typescript-eslint/tsconfig-utils": "8.70.0", "@typescript-eslint/types": "8.70.0", "@typescript-eslint/visitor-keys": "8.70.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.70.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.70.0", "@typescript-eslint/types": "8.70.0", "@typescript-eslint/typescript-estree": "8.70.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.70.0", "", { "dependencies": { "@typescript-eslint/types": "8.70.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ=="],
|
||||
|
||||
"acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
"ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
|
||||
|
||||
"ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
||||
"ansi-regex": ["ansi-regex@6.4.0", "", {}, "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"bits-ui": ["bits-ui@2.19.2", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-W9upilTmLTIWjuVAD6Ta7XoS6gxRflmCQnXamj+WN1eYxZecghqGENpuSMAKQmVENMLXhh85BZb04LCrBd2+Ng=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="],
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="],
|
||||
|
||||
"cacheable": ["cacheable@2.5.0", "", { "dependencies": { "@cacheable/memory": "^2.2.0", "@cacheable/utils": "^2.5.0", "hookified": "^1.15.0", "keyv": "^5.6.0", "qified": "^0.10.1" } }, "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g=="],
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="],
|
||||
|
||||
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
|
||||
|
||||
"cli-truncate": ["cli-truncate@6.1.1", "", { "dependencies": { "slice-ansi": "^9.0.0", "string-width": "^8.2.0" } }, "sha512-06p9vyLahLa4zkGcgsGxU6iEkSOiuI4fhCH6Emhe2lPAcoUv73n72DnODsnHA+5wwXGnV0n9M9/qOQJSjYhFhw=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"cn": ["cn@0.3.0", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-qRoU0ARrZD3Bbj1wKFftEKxPjSnzTEdFkf22NVhW+zU9D/wajhoP+qUZ5DChhhSOBCH1onV12hrmGqfu1+5Z2Q=="],
|
||||
|
||||
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
||||
|
||||
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
"cookie": ["cookie@2.0.1", "", {}, "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w=="],
|
||||
|
||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||
|
||||
@@ -272,35 +236,19 @@
|
||||
|
||||
"dompurify": ["dompurify@3.4.15", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw=="],
|
||||
|
||||
"empathic": ["empathic@2.1.0", "", {}, "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.25.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@10.11.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "11.1.5 || >11.1.6 <12", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
|
||||
|
||||
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||
|
||||
"espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
|
||||
|
||||
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
||||
|
||||
"esrap": ["esrap@2.3.7", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-n2nf7fZR3c9yXf0BPEuHuXqT+KW0SJVj4cN5FMEkpCZ3scLjOQWpiccyCxVzCC2q1wubTghuEGzngJY/7Ah0Ow=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
"execa": ["execa@10.1.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "figures": "^6.1.0", "get-stream": "^9.0.1", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.3.1", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "which-command": "^0.1.0", "yoctocolors": "^2.2.0" } }, "sha512-5HT/600HqXEtmkb9mK/PkTWw+bQSCzPnWiZ4/87wD3Hi0wFzCapOuNT9p4M08PDd+KLHdHvFhKjOWmNx/npRNQ=="],
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
||||
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
|
||||
|
||||
@@ -308,56 +256,50 @@
|
||||
|
||||
"fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="],
|
||||
|
||||
"fastq": ["fastq@1.20.3", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@11.1.5", "", { "dependencies": { "flat-cache": "^6.1.23" } }, "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q=="],
|
||||
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
|
||||
|
||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"flat-cache": ["flat-cache@6.1.23", "", { "dependencies": { "cacheable": "^2.5.0", "flatted": "^3.4.2", "hookified": "^1.15.0" } }, "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA=="],
|
||||
|
||||
"flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="],
|
||||
"find-workspaces": ["find-workspaces@0.3.1", "", { "dependencies": { "fast-glob": "^3.3.2", "pkg-types": "^1.0.3", "yaml": "^2.3.4" } }, "sha512-UDkGILGJSA1LN5Aa7McxCid4sqW3/e+UYsVwyxki3dDT0F8+ym0rAfnCkEfkL0rO7M+8/mvkim4t/s3IPHmg+w=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
|
||||
"get-east-asian-width": ["get-east-asian-width@1.7.0", "", {}, "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
|
||||
|
||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"hashery": ["hashery@1.5.1", "", { "dependencies": { "hookified": "^1.15.0" } }, "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ=="],
|
||||
|
||||
"hookified": ["hookified@1.15.1", "", {}, "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
"human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
"is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
||||
|
||||
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||
|
||||
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
|
||||
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
||||
|
||||
"keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
|
||||
@@ -384,19 +326,23 @@
|
||||
|
||||
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="],
|
||||
"log-update": ["log-update@8.0.0", "", { "dependencies": { "ansi-escapes": "^7.3.0", "cli-cursor": "^5.0.0", "slice-ansi": "^9.0.0", "string-width": "^8.2.0", "strip-ansi": "^7.2.0", "wrap-ansi": "^10.0.0" } }, "sha512-lddSgOt3bPASrylL54ZSpy8nBHns+vBVSoILlVOx+dei300pnLRN958rj/EdlVLKuWlSESU3qdnDZdAI7FXYGg=="],
|
||||
|
||||
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
"magic-string": ["magic-string@1.4.1", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow=="],
|
||||
|
||||
"magicast": ["magicast@0.5.5", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "source-map-js": "^1.2.1" } }, "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA=="],
|
||||
|
||||
"marked": ["marked@18.0.13", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-xTxVzZsBFwunP6HDmtBkabUQEYArnP7/rMDGmPj9SlrKlQ4i8MdYVow+nJL0eOqwpUqhzBoTBRADGN6uYwPyOw=="],
|
||||
|
||||
"minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="],
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||
|
||||
"mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
|
||||
|
||||
"mode-watcher": ["mode-watcher@1.1.0", "", { "dependencies": { "runed": "^0.25.0", "svelte-toolbelt": "^0.7.1" }, "peerDependencies": { "svelte": "^5.27.0" } }, "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g=="],
|
||||
|
||||
@@ -404,29 +350,21 @@
|
||||
|
||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||
|
||||
"npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
|
||||
|
||||
"nypm": ["nypm@0.6.10", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.3.1" }, "bin": { "nypm": "./dist/cli.mjs" } }, "sha512-W72Hrj1petq+b3Hk2aAC+9zswetlIFTnDW4s5djseZh2nYqBbyQLOtj472HwcbcWykPBUW1WpWpjOd4nK6gMRw=="],
|
||||
|
||||
"obug": ["obug@2.2.1", "", {}, "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
"parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
|
||||
"path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
@@ -434,38 +372,50 @@
|
||||
|
||||
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
|
||||
|
||||
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||
|
||||
"postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
"pretty-ms": ["pretty-ms@9.3.1", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"qified": ["qified@0.10.1", "", { "dependencies": { "hookified": "^2.1.1" } }, "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA=="],
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="],
|
||||
|
||||
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rolldown": ["rolldown@1.2.9", "", { "dependencies": { "@oxc-project/types": "=0.150.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.9", "@rolldown/binding-android-arm64": "1.2.9", "@rolldown/binding-darwin-arm64": "1.2.9", "@rolldown/binding-darwin-x64": "1.2.9", "@rolldown/binding-freebsd-x64": "1.2.9", "@rolldown/binding-linux-arm-gnueabihf": "1.2.9", "@rolldown/binding-linux-arm64-gnu": "1.2.9", "@rolldown/binding-linux-arm64-musl": "1.2.9", "@rolldown/binding-linux-ppc64-gnu": "1.2.9", "@rolldown/binding-linux-s390x-gnu": "1.2.9", "@rolldown/binding-linux-x64-gnu": "1.2.9", "@rolldown/binding-linux-x64-musl": "1.2.9", "@rolldown/binding-openharmony-arm64": "1.2.9", "@rolldown/binding-win32-arm64-msvc": "1.2.9", "@rolldown/binding-win32-x64-msvc": "1.2.9" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="],
|
||||
|
||||
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||
|
||||
"semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="],
|
||||
|
||||
"shadcn-svelte": ["shadcn-svelte@1.7.0", "", { "dependencies": { "cn": "^0.3.0", "commander": "^14.0.0", "node-fetch-native": "^1.6.4", "postcss": "^8.5.10" }, "peerDependencies": { "svelte": "^5.0.0" }, "bin": { "shadcn-svelte": "dist/index.mjs" } }, "sha512-AK3ZsYOL8PN/RxcEBCHGvQj2TrLfaaBsbw0QohyEb3nn/yPNDRC/+9iSjnTp/uKEe+tAi0tBxB/Yhzno0N7EEg=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
"slice-ansi": ["slice-ansi@9.0.1", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-aBY19bn/XA+hKOKX0qL7Z0UfdBsbQvc9hn993U8ALUjzxCvDcuZqLoRXjGJrUARWAlwMnsRVD9sw2AFNOYvalA=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||
|
||||
"svelte": ["svelte@5.57.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@jridgewell/sourcemap-codec": "^1.6.0", "@sveltejs/acorn-typescript": "^1.0.13", "@types/estree": "^1.0.9", "acorn": "^8.18.0", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.9.2", "esm-env": "^1.2.1", "esrap": "^2.3.6", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-Uqj49lWKB+iSSnneuwiYYJ7MZgkB+eXr0LXBhv4uDuAkXqnWmq65Sxflfvp0Lc6MdKjMUxGaeOKWJqz5SNiVIA=="],
|
||||
@@ -490,44 +440,44 @@
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
|
||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||
|
||||
"ultracite": ["ultracite@7.9.0", "", { "dependencies": { "@clack/prompts": "^1.5.1", "@typescript-eslint/utils": "^8.62.1", "commander": "^15.0.0", "cross-spawn": "^7.0.6", "deepmerge": "^4.3.1", "glob": "^13.0.6", "jsonc-parser": "^3.3.1", "nypm": "^0.6.6", "yaml": "^2.9.0", "zod": "^4.4.3" }, "peerDependencies": { "oxfmt": ">=0.1.0", "oxlint": "^1.0.0" }, "optionalPeers": ["oxfmt", "oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-Lkh6MXuX/vI89jeDIaEiOifI1BVXEJmk6e+igKRGAzdp5jKzeFa5d5A0tpJmHGCxXzd42jVwNpcs7jqm87IfNg=="],
|
||||
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
|
||||
|
||||
"ultracite": ["ultracite@7.12.4", "", { "dependencies": { "@clack/prompts": "^1.8.1", "cli-truncate": "^6.1.1", "commander": "^15.0.0", "deepmerge": "^4.3.1", "empathic": "^2.1.0", "execa": "^10.0.1", "fast-glob": "^3.3.3", "find-workspaces": "^0.3.1", "jsonc-parser": "^3.3.1", "log-update": "^8.0.0", "magicast": "^0.5.5", "npm-run-path": "^6.0.0", "nypm": "^0.6.10", "resolve.exports": "^2.0.3", "semver": "^7.8.5", "string-width": "^8.3.0", "which-command": "^0.1.0", "yaml": "^2.9.1", "zod": "^4.6.5" }, "peerDependencies": { "@biomejs/biome": "^2.5.0", "eslint": "^10.9.0", "oxfmt": ">=0.59.0", "oxlint": "^1.86.0", "prettier": "^3.0.0", "stylelint": "^17.0.0" }, "optionalPeers": ["@biomejs/biome", "eslint", "oxfmt", "oxlint", "prettier", "stylelint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-6jVEwaZBaytFm6XRoI3WGS2ZydrkrNvHTD0qTf7983ti2FlEWL1YcxnPaMaYuS6PkXqMhIK2XGr0cbsL4x5pQA=="],
|
||||
|
||||
"undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
|
||||
|
||||
"vite": ["vite@8.3.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ=="],
|
||||
|
||||
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
"which-command": ["which-command@0.1.0", "", { "bin": { "which-command": "cli.js" } }, "sha512-XZyoF5/5hZtXitIwzrU4NKK+Wtbb9aB9CezUEw2Q0wlYK8NUYQxC1rRXgNueYLtBAJwXIb+/tFVk4dozciNJMA=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
"wrap-ansi": ["wrap-ansi@10.0.2", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0" } }, "sha512-6OLRNZqRntVGBm26ghZ5eZhCuWWyOkRCSn8XsOIuq5stpCE/tjLbdozd93naILZig0lmD8ANfRwnhn0DLeXGlg=="],
|
||||
|
||||
"yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
"yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="],
|
||||
|
||||
"zimmerframe": ["zimmerframe@1.1.5", "", {}, "sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA=="],
|
||||
|
||||
"zod": ["zod@4.6.5", "", {}, "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q=="],
|
||||
|
||||
"@sveltejs/vite-plugin-svelte/magic-string": ["magic-string@1.4.1", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"@tailwindcss/node/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||
@@ -540,17 +490,13 @@
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"eslint/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"espree/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"mode-watcher/runed": ["runed@0.25.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg=="],
|
||||
|
||||
"mode-watcher/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
|
||||
|
||||
"qified/hookified": ["hookified@2.2.0", "", {}, "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA=="],
|
||||
"svelte/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"svelte-sonner/runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="],
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ services:
|
||||
caddy:
|
||||
aliases:
|
||||
- lodge-frontend
|
||||
# ports:
|
||||
# - "127.0.0.1:3000:3000"
|
||||
|
||||
networks:
|
||||
caddy:
|
||||
|
||||
+16
-12
@@ -1,26 +1,30 @@
|
||||
{
|
||||
"name": "lodge",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#lib": "./src/lib/index.js",
|
||||
"#lib/*": "./src/lib/*"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"dev": "vite dev",
|
||||
"fix": "ultracite fix",
|
||||
"lint": "ultracite check",
|
||||
"fix": "ultracite fix"
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.5.2",
|
||||
"@biomejs/biome": "^2.5.15",
|
||||
"@fontsource-variable/archivo": "^5.3.0",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
||||
"@internationalized/date": "^3.12.0",
|
||||
"@lucide/svelte": "^1.23.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.63.0",
|
||||
"@sveltejs/adapter-static": "^4.0.0",
|
||||
"@sveltejs/kit": "^3.0.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@types/bun": "^1.3.0",
|
||||
@@ -31,15 +35,15 @@
|
||||
"marked": "^18.0.13",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"shadcn-svelte": "^1.3.0",
|
||||
"svelte": "^5.56.1",
|
||||
"svelte-check": "^4.6.0",
|
||||
"svelte": "^5.57.1",
|
||||
"svelte-check": "^4.7.6",
|
||||
"svelte-sonner": "^1.1.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^6.0.3",
|
||||
"ultracite": "7.9.0",
|
||||
"ultracite": "^7.12.4",
|
||||
"vite": "^8.0.16"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { defineEnvVars } from "@sveltejs/kit/env";
|
||||
|
||||
export const variables = defineEnvVars({
|
||||
PUBLIC_API_BASE_URL: { public: true, schema: (input) => input ?? "/api" },
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { env } from "$env/dynamic/public";
|
||||
import { PUBLIC_API_BASE_URL } from "$app/env/public";
|
||||
|
||||
const BASE = env.PUBLIC_API_BASE_URL ?? "/api";
|
||||
const BASE = PUBLIC_API_BASE_URL;
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { ago, lithology, type Revision, shortSha } from "$lib/lodge";
|
||||
import { ago, lithology, type Revision, shortSha } from "#lib/lodge.js";
|
||||
|
||||
interface Props {
|
||||
at: number;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
// place files you want to import through the `#lib` alias in this folder.
|
||||
// biome-ignore-all lint/suspicious/noEmptySource: temporary
|
||||
@@ -1,4 +1,4 @@
|
||||
import { api } from "$lib/api/client";
|
||||
import { api } from "#lib/api/client.js";
|
||||
|
||||
export interface Change {
|
||||
base: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiError, api } from "#lib/api/client.js";
|
||||
import { goto } from "$app/navigation";
|
||||
import { ApiError, api } from "$lib/api/client";
|
||||
|
||||
export const session = $state({ signedIn: false });
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import "./layout.css";
|
||||
import { ModeWatcher, mode, toggleMode } from "mode-watcher";
|
||||
import { onMount } from "svelte";
|
||||
import { requireSession, session, signOut } from "#lib/session.svelte.js";
|
||||
import { page } from "$app/state";
|
||||
import { requireSession, session, signOut } from "$lib/session.svelte";
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { api } from "$lib/api/client";
|
||||
import { api } from "#lib/api/client.js";
|
||||
import {
|
||||
ago,
|
||||
exact,
|
||||
@@ -8,8 +8,8 @@
|
||||
type Machine,
|
||||
type Repo,
|
||||
type Revision,
|
||||
} from "$lib/lodge";
|
||||
import { describe } from "$lib/session.svelte";
|
||||
} from "#lib/lodge.js";
|
||||
import { describe } from "#lib/session.svelte.js";
|
||||
|
||||
const CORE_BEDS = 60;
|
||||
const KEY_LENGTH = 7;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { ApiError, api } from "#lib/api/client.js";
|
||||
import { describe, session } from "#lib/session.svelte.js";
|
||||
import { goto } from "$app/navigation";
|
||||
import { ApiError, api } from "$lib/api/client";
|
||||
import { describe, session } from "$lib/session.svelte";
|
||||
|
||||
let password = $state("");
|
||||
let busy = $state(false);
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { api } from "$lib/api/client";
|
||||
import { ago, type Code, exact, lithology, type Machine } from "$lib/lodge";
|
||||
import { describe } from "$lib/session.svelte";
|
||||
import { api } from "#lib/api/client.js";
|
||||
import {
|
||||
ago,
|
||||
type Code,
|
||||
exact,
|
||||
lithology,
|
||||
type Machine,
|
||||
} from "#lib/lodge.js";
|
||||
import { describe } from "#lib/session.svelte.js";
|
||||
|
||||
const SECOND = 1000;
|
||||
const MINUTE = 60;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/state";
|
||||
import { api } from "$lib/api/client";
|
||||
import Note from "$lib/components/Note.svelte";
|
||||
import StripLog from "$lib/components/StripLog.svelte";
|
||||
import { api } from "#lib/api/client.js";
|
||||
import Note from "#lib/components/Note.svelte";
|
||||
import StripLog from "#lib/components/StripLog.svelte";
|
||||
import {
|
||||
ago,
|
||||
blob,
|
||||
@@ -19,8 +17,10 @@
|
||||
shortSha,
|
||||
spaceName,
|
||||
text,
|
||||
} from "$lib/lodge";
|
||||
import { describe } from "$lib/session.svelte";
|
||||
} from "#lib/lodge.js";
|
||||
import { describe } from "#lib/session.svelte.js";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/state";
|
||||
|
||||
interface Opened {
|
||||
before: string | null;
|
||||
@@ -97,7 +97,7 @@
|
||||
);
|
||||
|
||||
const href = (patch: Record<string, string | number>) => {
|
||||
const next = new URLSearchParams(query);
|
||||
const next = new URLSearchParams(query.toString());
|
||||
for (const [param, value] of Object.entries(patch)) {
|
||||
if (value === "" || (param === "at" && value === latest)) {
|
||||
next.delete(param);
|
||||
@@ -112,9 +112,8 @@
|
||||
const select = (seq: number, scrubbing: boolean) => {
|
||||
confirming = "";
|
||||
goto(href({ at: seq }), {
|
||||
keepFocus: true,
|
||||
noScroll: true,
|
||||
replaceState: scrubbing,
|
||||
replace: scrubbing,
|
||||
reset: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -214,7 +213,7 @@
|
||||
});
|
||||
confirming = "";
|
||||
await load();
|
||||
await goto(href({ at: "" }), { noScroll: true });
|
||||
await goto(href({ at: "" }), { reset: false });
|
||||
} catch (error) {
|
||||
problem = describe(error);
|
||||
} finally {
|
||||
@@ -254,7 +253,9 @@
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
{#snippet slashed(value: string)}
|
||||
{#snippet slashed(
|
||||
value: string
|
||||
)}
|
||||
{#each value.split("/") as part, index (index)}
|
||||
{index ? "/" : ""}<wbr>{part}
|
||||
{/each}
|
||||
@@ -316,7 +317,11 @@
|
||||
<span class="kind">{changeKind(change)}</span>
|
||||
{#if change.hash}
|
||||
<a
|
||||
href={href({ path: change.path, space: change.space, view: "changes" })}
|
||||
href={href({
|
||||
path: change.path,
|
||||
space: change.space,
|
||||
view: "changes",
|
||||
})}
|
||||
>{@render slashed(change.path)}</a
|
||||
>
|
||||
{:else}
|
||||
@@ -336,15 +341,21 @@
|
||||
{group === SHARED ? "Shared notes" : `Branch ${spaceName(group)}`}
|
||||
</h3>
|
||||
<ul class="rows">
|
||||
{#each Object.entries(repoState?.spaces[group] ?? {}).sort( ([a], [b]) => a.localeCompare(b) ) as [ file, info ] (file)}
|
||||
{#each Object.entries(repoState?.spaces[group] ?? {}).sort(([a], [b]) =>
|
||||
a.localeCompare(b)
|
||||
) as [file, info] (file)}
|
||||
<li>
|
||||
<a
|
||||
aria-current={group === space && file === path ? "true" : undefined}
|
||||
aria-current={group === space && file === path
|
||||
? "true"
|
||||
: undefined}
|
||||
href={href({ path: file, space: group, view: "" })}
|
||||
>
|
||||
<span class="file">
|
||||
<span class="dir"
|
||||
>{@render slashed(file.slice(0, file.lastIndexOf("/") + 1))}</span
|
||||
>{@render slashed(
|
||||
file.slice(0, file.lastIndexOf("/") + 1)
|
||||
)}</span
|
||||
>{file.slice(file.lastIndexOf("/") + 1)}
|
||||
</span>
|
||||
<span aria-hidden="true" class="track">
|
||||
@@ -414,7 +425,11 @@
|
||||
|
||||
{#if entry}
|
||||
<div class="controls">
|
||||
{#each [["read", "Read"], ["changes", "Changes"], ["source", "Source"]] as [ id, title ] (id)}
|
||||
{#each [
|
||||
["read", "Read"],
|
||||
["changes", "Changes"],
|
||||
["source", "Source"],
|
||||
] as [id, title] (id)}
|
||||
{#if markdown || id !== "read"}
|
||||
<a
|
||||
aria-current={view === id ? "true" : undefined}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import adapter from "@sveltejs/adapter-static";
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
assets: "build",
|
||||
fallback: "index.html",
|
||||
pages: "build",
|
||||
precompress: false,
|
||||
strict: false,
|
||||
}),
|
||||
},
|
||||
preprocess: vitePreprocess(),
|
||||
};
|
||||
|
||||
export default config;
|
||||
+4
-15
@@ -1,20 +1,9 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"extends": "$app/tsconfig",
|
||||
"compilerOptions": {
|
||||
"rewriteRelativeImportExtensions": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// To make changes to top-level options such as include and exclude, we recommend extending
|
||||
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src", "vite.config.ts"]
|
||||
}
|
||||
+15
-1
@@ -1,4 +1,6 @@
|
||||
import adapter from "@sveltejs/adapter-static";
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
@@ -7,7 +9,19 @@ const allowedHosts = process.env.ALLOWED_HOSTS
|
||||
: undefined;
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), sveltekit()],
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
sveltekit({
|
||||
adapter: adapter({
|
||||
assets: "build",
|
||||
fallback: "index.html",
|
||||
pages: "build",
|
||||
precompress: false,
|
||||
strict: false,
|
||||
}),
|
||||
preprocess: [vitePreprocess()],
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
allowedHosts,
|
||||
proxy: {
|
||||
|
||||
Reference in new issue
Block a user