bots-garden/sidekickspublic Fork
⑂ b34befd
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/sidekicks.git
git clone ssh://git@rickub.com/bots-garden/sidekicks.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

🛟 Updated.

k33g committed 2026-10-29T13:08:37+01:00 Browse files
b34befd parent: b625b0f
modified .claude/skills/drawio-fancy-diagram/generator/examples/sidekicks_packages.py +29 -26
@@ -1,7 +1,9 @@
11 """The description of docs/diagrams/packages.{drawio,svg} (Sidekicks, ADR 0003).
22
33 python3 .claude/skills/drawio-fancy-diagram/generator/examples/sidekicks_packages.py docs/diagrams
4- rsvg-convert -w 1200 -h 780 -o docs/diagrams/packages.png docs/diagrams/packages.svg
4+ rsvg-convert -w 1200 -h 800 -o docs/diagrams/packages.png docs/diagrams/packages.svg
5+
6+Blue boxes exist; grey boxes are planned (their milestone is written in them).
57 """
68 import os
79 import sys
@@ -12,41 +14,42 @@ from flowgen import Diagram
1214 OUT = sys.argv[1] if len(sys.argv) > 1 else "docs/diagrams"
1315
1416 d = Diagram("packages", "Sidekicks — packages and dependencies",
15- "arrows are imports · the frontend only talks to internal/app · nothing outside internal/acp sees the ACP SDK", height=780)
17+ "arrows are imports · blue: exists (M1) · grey: planned · only main.go imports Wails", height=800)
1618
1719 # Row 1: the entry points.
18-d.box(40, 100, 250, 115, "user", "💻", "frontend/", ["Svelte 5 · panels · Markdown", "CodeMirror · xterm.js"])
19-d.box(475, 100, 250, 115, "decider", "🔀", "internal/app", ["Wails services bound to the UI", "typed events to the UI"], badge="★ only API of the UI")
20-d.box(910, 100, 250, 115, "data", "⚙️", "cmd/sidekicks", ["main: reads flags, wires", "the services, runs Wails"])
20+d.box(40, 100, 250, 115, "user", "💻", "frontend/", ["Svelte 5 · panels · state", "generated TS bindings"])
21+d.box(475, 100, 250, 115, "decider", "🔀", "internal/app", ["AgentService · ConfigService", "config ↔ agents glue"], badge="★ only API of the UI")
22+d.box(910, 100, 250, 115, "hook", "⚙️", "main.go (root)", ["flags · Wails app · window", "binds services and events"])
2123 d.edge([(290, 157), (475, 157)], "bindings · events", at=(382, 157), lw=130, both=True)
2224 d.edge([(910, 157), (725, 157)], "creates", at=(817, 157), lw=70)
2325
24-# Row 2: the domain packages, all used by internal/app.
25-d.box(40, 300, 250, 110, "agent", "💬", "internal/channels", ["channels, members, routing", "@mentions · /agent:command"])
26-d.box(330, 300, 250, 110, "agent", "🤖", "internal/agents", ["agent processes: lazy start,", "idle stop, limit, logs"])
27-d.box(620, 300, 250, 110, "agent", "📋", "internal/config", ["load · validate · watch", "save with comments"])
28-d.box(910, 300, 250, 110, "agent", "🧰", "internal/workspace", ["files + exec: local,", "container, sbx"])
26+# Row 2: the domain packages used by internal/app.
27+d.box(40, 300, 250, 110, "data", "💬", "internal/channels", ["planned · M3", "channels, routing, @ and /"])
28+d.box(330, 300, 250, 110, "agent", "🤖", "internal/agents", ["lazy start · LRU limit", "idle stop · states · logs"])
29+d.box(620, 300, 250, 110, "agent", "📋", "internal/config", ["load · validate · locate", "default file · save · watch"])
30+d.box(910, 300, 250, 110, "data", "🧰", "internal/workspace", ["planned · M4", "files + exec: local, container, sbx"])
2931 for cx in (165, 455, 745, 1035):
3032 d.edge([(600, 215), (600, 255), (cx, 255), (cx, 300)])
3133 d.edge([(290, 355), (330, 355)])
3234 d.edge([(580, 355), (620, 355)])
3335
34-# Row 3: what the domain packages build on.
35-d.box(40, 480, 250, 105, "agent", "📥", "internal/store", ["Pebble: channels, messages,", "session ids"])
36-d.box(330, 480, 250, 105, "agent", "🔧", "internal/acp", ["ACP v1 client wrapper", "SDK types stay inside"])
37-d.box(620, 480, 250, 105, "agent", "🛡️", "schema", ["config JSON Schema", "ValidateConfig()"])
38-d.edge([(165, 410), (165, 480)])
39-d.edge([(455, 410), (455, 480)])
40-d.edge([(745, 410), (745, 480)])
36+# Row 3: what they build on.
37+d.box(40, 490, 250, 105, "data", "📥", "internal/store", ["planned · M3", "Pebble: channels, messages"])
38+d.box(330, 490, 250, 105, "agent", "🔧", "internal/acp", ["ACP v1 client over stdio", "SDK types stay inside"])
39+d.box(620, 490, 250, 105, "agent", "🛡️", "schema", ["config JSON Schema", "ValidateConfig()"])
40+d.box(910, 490, 250, 105, "agent", "📋", "internal/events", ["event names + payloads", "Emitter interface"])
41+d.edge([(165, 410), (165, 490)])
42+d.edge([(455, 410), (455, 490)])
43+d.edge([(745, 410), (745, 490)])
44+d.edge([(560, 410), (560, 450), (1035, 450), (1035, 490)], "emits", at=(950, 450), lw=60)
4145
4246 # Row 4: third-party modules and external processes.
43-d.chip(40, 655, 250, 50, "cockroachdb/pebble/v2")
44-d.chip(330, 655, 250, 50, "coder/acp-go-sdk → mm -acp")
45-d.chip(620, 655, 250, 50, "jsonschema/v6 · yaml/v3")
46-d.chip(910, 655, 250, 50, "creack/pty · docker · sbx")
47-d.edge([(165, 585), (165, 655)])
48-d.edge([(455, 585), (455, 655)], "stdio", at=(500, 620), lw=50)
49-d.edge([(745, 585), (745, 655)])
50-d.edge([(1035, 410), (1035, 655)])
51-d.text(600, 750, "Third-party modules in grey. Wails v3 is imported by cmd/sidekicks and internal/app only.", 12.5, italic=True)
47+d.chip(40, 670, 250, 50, "cockroachdb/pebble/v2")
48+d.chip(330, 670, 250, 50, "coder/acp-go-sdk → mm -acp")
49+d.chip(620, 670, 250, 50, "jsonschema/v6 · yaml/v3")
50+d.chip(910, 670, 250, 50, "fsnotify (config watch)")
51+d.edge([(165, 595), (165, 670)], dashed=True)
52+d.edge([(455, 595), (455, 670)], "stdio", at=(500, 632), lw=50)
53+d.edge([(745, 595), (745, 670)])
54+d.text(600, 765, "internal/app also emits through internal/events; internal/config imports schema and fsnotify. Third-party modules in grey chips.", 12.5, italic=True)
5255 d.write(OUT)
@@ -1,7 +1,9 @@
1 """The description of docs/diagrams/packages.{drawio,svg} (Sidekicks, ADR 0003).1 """The description of docs/diagrams/packages.{drawio,svg} (Sidekicks, ADR 0003).
2 2
3 python3 .claude/skills/drawio-fancy-diagram/generator/examples/sidekicks_packages.py docs/diagrams3 python3 .claude/skills/drawio-fancy-diagram/generator/examples/sidekicks_packages.py docs/diagrams
4- rsvg-convert -w 1200 -h 780 -o docs/diagrams/packages.png docs/diagrams/packages.svg4+ rsvg-convert -w 1200 -h 800 -o docs/diagrams/packages.png docs/diagrams/packages.svg
5+
6+Blue boxes exist; grey boxes are planned (their milestone is written in them).
5 """7 """
6 import os8 import os
7 import sys9 import sys
@@ -12,41 +14,42 @@ from flowgen import Diagram
12 OUT = sys.argv[1] if len(sys.argv) > 1 else "docs/diagrams"14 OUT = sys.argv[1] if len(sys.argv) > 1 else "docs/diagrams"
13 15
14 d = Diagram("packages", "Sidekicks — packages and dependencies",16 d = Diagram("packages", "Sidekicks — packages and dependencies",
15- "arrows are imports · the frontend only talks to internal/app · nothing outside internal/acp sees the ACP SDK", height=780)17+ "arrows are imports · blue: exists (M1) · grey: planned · only main.go imports Wails", height=800)
16 18
17 # Row 1: the entry points.19 # Row 1: the entry points.
18-d.box(40, 100, 250, 115, "user", "💻", "frontend/", ["Svelte 5 · panels · Markdown", "CodeMirror · xterm.js"])20+d.box(40, 100, 250, 115, "user", "💻", "frontend/", ["Svelte 5 · panels · state", "generated TS bindings"])
19-d.box(475, 100, 250, 115, "decider", "🔀", "internal/app", ["Wails services bound to the UI", "typed events to the UI"], badge="★ only API of the UI")21+d.box(475, 100, 250, 115, "decider", "🔀", "internal/app", ["AgentService · ConfigService", "config ↔ agents glue"], badge="★ only API of the UI")
20-d.box(910, 100, 250, 115, "data", "⚙️", "cmd/sidekicks", ["main: reads flags, wires", "the services, runs Wails"])22+d.box(910, 100, 250, 115, "hook", "⚙️", "main.go (root)", ["flags · Wails app · window", "binds services and events"])
21 d.edge([(290, 157), (475, 157)], "bindings · events", at=(382, 157), lw=130, both=True)23 d.edge([(290, 157), (475, 157)], "bindings · events", at=(382, 157), lw=130, both=True)
22 d.edge([(910, 157), (725, 157)], "creates", at=(817, 157), lw=70)24 d.edge([(910, 157), (725, 157)], "creates", at=(817, 157), lw=70)
23 25
24-# Row 2: the domain packages, all used by internal/app.26+# Row 2: the domain packages used by internal/app.
25-d.box(40, 300, 250, 110, "agent", "💬", "internal/channels", ["channels, members, routing", "@mentions · /agent:command"])27+d.box(40, 300, 250, 110, "data", "💬", "internal/channels", ["planned · M3", "channels, routing, @ and /"])
26-d.box(330, 300, 250, 110, "agent", "🤖", "internal/agents", ["agent processes: lazy start,", "idle stop, limit, logs"])28+d.box(330, 300, 250, 110, "agent", "🤖", "internal/agents", ["lazy start · LRU limit", "idle stop · states · logs"])
27-d.box(620, 300, 250, 110, "agent", "📋", "internal/config", ["load · validate · watch", "save with comments"])29+d.box(620, 300, 250, 110, "agent", "📋", "internal/config", ["load · validate · locate", "default file · save · watch"])
28-d.box(910, 300, 250, 110, "agent", "🧰", "internal/workspace", ["files + exec: local,", "container, sbx"])30+d.box(910, 300, 250, 110, "data", "🧰", "internal/workspace", ["planned · M4", "files + exec: local, container, sbx"])
29 for cx in (165, 455, 745, 1035):31 for cx in (165, 455, 745, 1035):
30 d.edge([(600, 215), (600, 255), (cx, 255), (cx, 300)])32 d.edge([(600, 215), (600, 255), (cx, 255), (cx, 300)])
31 d.edge([(290, 355), (330, 355)])33 d.edge([(290, 355), (330, 355)])
32 d.edge([(580, 355), (620, 355)])34 d.edge([(580, 355), (620, 355)])
33 35
34-# Row 3: what the domain packages build on.36+# Row 3: what they build on.
35-d.box(40, 480, 250, 105, "agent", "📥", "internal/store", ["Pebble: channels, messages,", "session ids"])37+d.box(40, 490, 250, 105, "data", "📥", "internal/store", ["planned · M3", "Pebble: channels, messages"])
36-d.box(330, 480, 250, 105, "agent", "🔧", "internal/acp", ["ACP v1 client wrapper", "SDK types stay inside"])38+d.box(330, 490, 250, 105, "agent", "🔧", "internal/acp", ["ACP v1 client over stdio", "SDK types stay inside"])
37-d.box(620, 480, 250, 105, "agent", "🛡️", "schema", ["config JSON Schema", "ValidateConfig()"])39+d.box(620, 490, 250, 105, "agent", "🛡️", "schema", ["config JSON Schema", "ValidateConfig()"])
38-d.edge([(165, 410), (165, 480)])40+d.box(910, 490, 250, 105, "agent", "📋", "internal/events", ["event names + payloads", "Emitter interface"])
39-d.edge([(455, 410), (455, 480)])41+d.edge([(165, 410), (165, 490)])
40-d.edge([(745, 410), (745, 480)])42+d.edge([(455, 410), (455, 490)])
43+d.edge([(745, 410), (745, 490)])
44+d.edge([(560, 410), (560, 450), (1035, 450), (1035, 490)], "emits", at=(950, 450), lw=60)
41 45
42 # Row 4: third-party modules and external processes.46 # Row 4: third-party modules and external processes.
43-d.chip(40, 655, 250, 50, "cockroachdb/pebble/v2")47+d.chip(40, 670, 250, 50, "cockroachdb/pebble/v2")
44-d.chip(330, 655, 250, 50, "coder/acp-go-sdk → mm -acp")48+d.chip(330, 670, 250, 50, "coder/acp-go-sdk → mm -acp")
45-d.chip(620, 655, 250, 50, "jsonschema/v6 · yaml/v3")49+d.chip(620, 670, 250, 50, "jsonschema/v6 · yaml/v3")
46-d.chip(910, 655, 250, 50, "creack/pty · docker · sbx")50+d.chip(910, 670, 250, 50, "fsnotify (config watch)")
47-d.edge([(165, 585), (165, 655)])51+d.edge([(165, 595), (165, 670)], dashed=True)
48-d.edge([(455, 585), (455, 655)], "stdio", at=(500, 620), lw=50)52+d.edge([(455, 595), (455, 670)], "stdio", at=(500, 632), lw=50)
49-d.edge([(745, 585), (745, 655)])53+d.edge([(745, 595), (745, 670)])
50-d.edge([(1035, 410), (1035, 655)])54+d.text(600, 765, "internal/app also emits through internal/events; internal/config imports schema and fsnotify. Third-party modules in grey chips.", 12.5, italic=True)
51-d.text(600, 750, "Third-party modules in grey. Wails v3 is imported by cmd/sidekicks and internal/app only.", 12.5, italic=True)
52 d.write(OUT)55 d.write(OUT)
added .github/workflows/ci.yml +53 -0
new file mode 100644
@@ -0,0 +1,53 @@
1+# Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+#
3+# Continuous integration: `make check` (lint + every test that needs no real
4+# agent) and a check that the committed TypeScript bindings are up to date.
5+# Rickub runs it as an ordinary GitHub Actions workflow, on every push and
6+# every merge request. No workflow_dispatch: Rickub's dispatch API fires every
7+# dispatchable workflow of a ref (see mini-me's release.yml).
8+name: CI
9+
10+on:
11+ push:
12+ pull_request:
13+
14+concurrency:
15+ group: ci-${{ github.ref }}
16+ cancel-in-progress: true
17+
18+jobs:
19+ check:
20+ runs-on: ubuntu-latest
21+ steps:
22+ - name: Checkout
23+ uses: actions/checkout@v4
24+
25+ - name: Set up Go
26+ uses: actions/setup-go@v5
27+ with:
28+ go-version-file: go.mod
29+ cache: true
30+
31+ - name: Set up Node
32+ uses: actions/setup-node@v4
33+ with:
34+ node-version: "22"
35+
36+ - name: Install pnpm (version from frontend/package.json)
37+ run: npm install -g "$(node -p "require('./frontend/package.json').packageManager")"
38+
39+ - name: Install the Wails build dependencies (GTK 4, WebKitGTK 6)
40+ run: |
41+ sudo apt-get update -qq
42+ sudo apt-get install -y -qq pkg-config libgtk-4-dev libwebkitgtk-6.0-dev
43+
44+ - name: Install the Wails CLI
45+ run: |
46+ go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0
47+ echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
48+
49+ - name: Bindings are up to date
50+ run: make bindings-check
51+
52+ - name: Lint and tests
53+ run: make check
new file mode 100644
@@ -0,0 +1,53 @@
1+# Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+#
3+# Continuous integration: `make check` (lint + every test that needs no real
4+# agent) and a check that the committed TypeScript bindings are up to date.
5+# Rickub runs it as an ordinary GitHub Actions workflow, on every push and
6+# every merge request. No workflow_dispatch: Rickub's dispatch API fires every
7+# dispatchable workflow of a ref (see mini-me's release.yml).
8+name: CI
9+
10+on:
11+ push:
12+ pull_request:
13+
14+concurrency:
15+ group: ci-${{ github.ref }}
16+ cancel-in-progress: true
17+
18+jobs:
19+ check:
20+ runs-on: ubuntu-latest
21+ steps:
22+ - name: Checkout
23+ uses: actions/checkout@v4
24+
25+ - name: Set up Go
26+ uses: actions/setup-go@v5
27+ with:
28+ go-version-file: go.mod
29+ cache: true
30+
31+ - name: Set up Node
32+ uses: actions/setup-node@v4
33+ with:
34+ node-version: "22"
35+
36+ - name: Install pnpm (version from frontend/package.json)
37+ run: npm install -g "$(node -p "require('./frontend/package.json').packageManager")"
38+
39+ - name: Install the Wails build dependencies (GTK 4, WebKitGTK 6)
40+ run: |
41+ sudo apt-get update -qq
42+ sudo apt-get install -y -qq pkg-config libgtk-4-dev libwebkitgtk-6.0-dev
43+
44+ - name: Install the Wails CLI
45+ run: |
46+ go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0
47+ echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
48+
49+ - name: Bindings are up to date
50+ run: make bindings-check
51+
52+ - name: Lint and tests
53+ run: make check
modified .gitignore +11 -0
@@ -2,3 +2,14 @@
22 .run/
33 # macOS
44 .DS_Store
5+# Wails / Task
6+.task
7+bin/
8+frontend/dist
9+frontend/node_modules
10+build/linux/appimage/build
11+build/windows/nsis/MicrosoftEdgeWebview2Setup.exe
12+# pnpm content-addressable store (created next to the repository when it sits on another file system)
13+.pnpm-store/
14+# Local scratch files (machine-specific configurations…)
15+tmp/
@@ -2,3 +2,14 @@
2 .run/2 .run/
3 # macOS3 # macOS
4 .DS_Store4 .DS_Store
5+# Wails / Task
6+.task
7+bin/
8+frontend/dist
9+frontend/node_modules
10+build/linux/appimage/build
11+build/windows/nsis/MicrosoftEdgeWebview2Setup.exe
12+# pnpm content-addressable store (created next to the repository when it sits on another file system)
13+.pnpm-store/
14+# Local scratch files (machine-specific configurations…)
15+tmp/
added .golangci.yml +33 -0
new file mode 100644
@@ -0,0 +1,33 @@
1+# Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+#
3+# golangci-lint v2, pinned in tools/go.mod and run with `make lint`.
4+version: "2"
5+
6+linters:
7+ default: standard # errcheck, govet, ineffassign, staticcheck, unused
8+ enable:
9+ - errorlint # errors compared with errors.Is / errors.As
10+ - gocyclo # functions a person can still follow
11+ - misspell
12+ - revive # doc comments and naming of exported identifiers
13+ - unconvert
14+ - unparam
15+ settings:
16+ gocyclo:
17+ min-complexity: 15
18+ misspell:
19+ # No locale: British and American spellings are both accepted (the ACP
20+ # SDK itself says "Cancelled"); only real misspellings are reported.
21+ ignore-rules:
22+ # French words of the default configuration file (internal/config/default.go)
23+ - assistante
24+ - exemple
25+ - programme
26+ - marrage
27+ exclusions:
28+ paths:
29+ - spikes # throwaway prototypes, separate modules
30+
31+formatters:
32+ enable:
33+ - gofmt
new file mode 100644
@@ -0,0 +1,33 @@
1+# Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+#
3+# golangci-lint v2, pinned in tools/go.mod and run with `make lint`.
4+version: "2"
5+
6+linters:
7+ default: standard # errcheck, govet, ineffassign, staticcheck, unused
8+ enable:
9+ - errorlint # errors compared with errors.Is / errors.As
10+ - gocyclo # functions a person can still follow
11+ - misspell
12+ - revive # doc comments and naming of exported identifiers
13+ - unconvert
14+ - unparam
15+ settings:
16+ gocyclo:
17+ min-complexity: 15
18+ misspell:
19+ # No locale: British and American spellings are both accepted (the ACP
20+ # SDK itself says "Cancelled"); only real misspellings are reported.
21+ ignore-rules:
22+ # French words of the default configuration file (internal/config/default.go)
23+ - assistante
24+ - exemple
25+ - programme
26+ - marrage
27+ exclusions:
28+ paths:
29+ - spikes # throwaway prototypes, separate modules
30+
31+formatters:
32+ enable:
33+ - gofmt
modified .memory/handoffs/2026-10-29-milestone-initialize.md +1 -1
@@ -10,7 +10,7 @@ Initialize is implemented on branch `feature/initialize` (not committed by the a
1010
1111 ## Next steps
1212
13-1. Owner: merge `feature/initialize`, then create the M1 branch.
13+1. Done: `feature/initialize` merged; M1 continues in `handoffs/2026-10-29-milestone-m1.md`.
1414 2. Owner: decide whether to run the qlty gate (needs a sandbox with the dev-toolkit kit) before M1.
1515 3. Start M1 with #8 (project from `wails3 init -t svelte`, merged into the existing root `go.mod`; keep the Makefile as entry point; licence headers), then #9 (loader on top of `schema`), #10 (`internal/acp` wrapping coder/acp-go-sdk; `realagents` build tag), #11, #12, #13.
1616
@@ -10,7 +10,7 @@ Initialize is implemented on branch `feature/initialize` (not committed by the a
10 10
11 ## Next steps11 ## Next steps
12 12
13-1. Owner: merge `feature/initialize`, then create the M1 branch.13+1. Done: `feature/initialize` merged; M1 continues in `handoffs/2026-10-29-milestone-m1.md`.
14 2. Owner: decide whether to run the qlty gate (needs a sandbox with the dev-toolkit kit) before M1.14 2. Owner: decide whether to run the qlty gate (needs a sandbox with the dev-toolkit kit) before M1.
15 3. Start M1 with #8 (project from `wails3 init -t svelte`, merged into the existing root `go.mod`; keep the Makefile as entry point; licence headers), then #9 (loader on top of `schema`), #10 (`internal/acp` wrapping coder/acp-go-sdk; `realagents` build tag), #11, #12, #13.15 3. Start M1 with #8 (project from `wails3 init -t svelte`, merged into the existing root `go.mod`; keep the Makefile as entry point; licence headers), then #9 (loader on top of `schema`), #10 (`internal/acp` wrapping coder/acp-go-sdk; `realagents` build tag), #11, #12, #13.
16 16
added .memory/handoffs/2026-10-29-milestone-m1.md +32 -0
new file mode 100644
@@ -0,0 +1,32 @@
1+# Handoff — 2026-10-29 — milestone M1 · Fondations
2+
3+## State
4+
5+M1 is implemented on `feature/m1-foundations` (not committed by the agent). `make check` passes in the sandbox; `make test-real` passes with Alice. The app was exercised under Xvfb on Linux. Issues #8–#11, #13 closed; #12 open with one task: the owner's macOS check.
6+
7+## In flight
8+
9+- Owner: follow `docs/fr/how-to/tester-la-milestone-m1-sous-macos.md`, then close #12 and milestone "M1 · Fondations" (or ask the agent).
10+- First push: check that `.github/workflows/ci.yml` runs on rickub (ubuntu-latest must provide `libwebkitgtk-6.0-dev`; if not, the apt step needs another image or package set).
11+
12+## Next steps
13+
14+1. M2 · Conversation avec un agent: #14 (sessions + prompt turn + streaming in `internal/acp`, the `client` handler must route `SessionUpdate`/`RequestPermission` to sessions), #15 (conversation view + composer), #16 (Markdown/Shiki/DOMPurify — reuse `spikes/wails-ui/frontend/src/lib/markdown.ts`), #17 (tool calls + permissions, see ADR 0001 fact 4), #18 (Settings YAML editor, `config.Save`), #19 (open another config file; `config.Locate` already takes `lastOpened`).
15+2. #38: qlty kit.
16+
17+## Open questions / blockers
18+
19+- qlty unavailable (by design of the quality skill: do not install by hand).
20+- `make` targets need `wails3` and `pnpm` on PATH (`$(go env GOPATH)/bin`, `npm i -g pnpm@12.11.2`).
21+
22+## Watch out for
23+
24+- **Never nest heredocs** in a Bash tool call (`<<'EOF'` containing `<<EOF`): the outer one ends at the inner EOF and the rest runs as shell. Use the Write tool for any file containing a heredoc.
25+- `rm` with a glob after `cd` is blocked by a safety check: copy only what is needed instead.
26+- Timestamps in `.tickets/`: use `date -u` (UTC), not the local hour.
27+- `frontend/node_modules` is shared with the Mac: only `pnpm install` (never npm), thanks to `pnpm-workspace.yaml` both platforms' binaries are present.
28+- macOS detection: never rely on `System.IsMac()` alone at startup (Wails environment arrives asynchronously); use `isMac()` from `frontend/src/lib/theme.ts`.
29+- Svelte 5: a module export named `state` conflicts with the `$state` rune — the shared state is `appState`.
30+- `go test ./...` needs `frontend/dist` (embed in main.go): `make test-go` builds it if missing.
31+- Bindings are generated with `-ts -i` (interfaces); `wails3 generate bindings -clean=true -ts -i` is stable (no diff on re-run).
32+- The SDK logs "connection closed" via slog: silenced in `internal/acp`.
new file mode 100644
@@ -0,0 +1,32 @@
1+# Handoff — 2026-10-29 — milestone M1 · Fondations
2+
3+## State
4+
5+M1 is implemented on `feature/m1-foundations` (not committed by the agent). `make check` passes in the sandbox; `make test-real` passes with Alice. The app was exercised under Xvfb on Linux. Issues #8–#11, #13 closed; #12 open with one task: the owner's macOS check.
6+
7+## In flight
8+
9+- Owner: follow `docs/fr/how-to/tester-la-milestone-m1-sous-macos.md`, then close #12 and milestone "M1 · Fondations" (or ask the agent).
10+- First push: check that `.github/workflows/ci.yml` runs on rickub (ubuntu-latest must provide `libwebkitgtk-6.0-dev`; if not, the apt step needs another image or package set).
11+
12+## Next steps
13+
14+1. M2 · Conversation avec un agent: #14 (sessions + prompt turn + streaming in `internal/acp`, the `client` handler must route `SessionUpdate`/`RequestPermission` to sessions), #15 (conversation view + composer), #16 (Markdown/Shiki/DOMPurify — reuse `spikes/wails-ui/frontend/src/lib/markdown.ts`), #17 (tool calls + permissions, see ADR 0001 fact 4), #18 (Settings YAML editor, `config.Save`), #19 (open another config file; `config.Locate` already takes `lastOpened`).
15+2. #38: qlty kit.
16+
17+## Open questions / blockers
18+
19+- qlty unavailable (by design of the quality skill: do not install by hand).
20+- `make` targets need `wails3` and `pnpm` on PATH (`$(go env GOPATH)/bin`, `npm i -g pnpm@12.11.2`).
21+
22+## Watch out for
23+
24+- **Never nest heredocs** in a Bash tool call (`<<'EOF'` containing `<<EOF`): the outer one ends at the inner EOF and the rest runs as shell. Use the Write tool for any file containing a heredoc.
25+- `rm` with a glob after `cd` is blocked by a safety check: copy only what is needed instead.
26+- Timestamps in `.tickets/`: use `date -u` (UTC), not the local hour.
27+- `frontend/node_modules` is shared with the Mac: only `pnpm install` (never npm), thanks to `pnpm-workspace.yaml` both platforms' binaries are present.
28+- macOS detection: never rely on `System.IsMac()` alone at startup (Wails environment arrives asynchronously); use `isMac()` from `frontend/src/lib/theme.ts`.
29+- Svelte 5: a module export named `state` conflicts with the `$state` rune — the shared state is `appState`.
30+- `go test ./...` needs `frontend/dist` (embed in main.go): `make test-go` builds it if missing.
31+- Bindings are generated with `-ts -i` (interfaces); `wails3 generate bindings -clean=true -ts -i` is stable (no diff on re-run).
32+- The SDK logs "connection closed" via slog: silenced in `internal/acp`.
modified .memory/history.md +21 -0
@@ -34,3 +34,24 @@
3434
3535 - **Goal**: after the owner's successful macOS check, close #3 and the milestone.
3636 - **Changes**: #3 last task closed + result comment, issue closed; milestone `Initialize` → closed; ADR 0002 status "verified on macOS"; prototype README updated; `.memory/` summary and handoff updated. No commit by the agent (the owner commits and merges).
37+
38+## 2026-10-29 — Milestone M1 · Fondations (#8–#13)
39+
40+- **Goal**: implement M1 in one go (owner's choice), with CI on rickub Actions, pnpm + supportedArchitectures, golangci-lint as go tool, and a ticket for a qlty kit.
41+- **Changes**: ticket #38 (qlty kit); root Wails v3 project (`main.go`, `Taskfile.yml`, `build/`, `frontend/`); `internal/{events,config,acp,agents,app}`; frontend shell (paneforge, panel registry, Agents/Channels/Conversation/Settings/Journal panels, status bar, theme, macOS inset); `Makefile` rewrite; `tools/go.mod` + `.golangci.yml`; `scripts/check-license-headers.sh` (+tests); `.github/workflows/ci.yml`; README; schema fix (`stopIdleAgentsAfter: 0` as a number); ADR 0003 Amendment 1; package diagram; docs EN/FR (tutorial first launch, how-to develop / run tests / test M1 on macOS, references commands + agents-and-events, updated configuration/architecture/memory); tickets #8–#11, #13 closed, #12 open.
42+- **Decisions**: main at the root instead of `cmd/sidekicks` (Wails Taskfiles build `.`); Wails imported only by main.go (internal/app testable without cgo); LRU eviction of an idle agent when the limit is reached, refuse only if all busy; crash never auto-restarts; config channels exposed read-only until M3; bindings committed + `bindings-check` in CI; go.yaml.in/yaml/v3 instead of gopkg.in/yaml.v3; misspell without locale (SDK uses "Cancelled"), French words ignored by explicit rule.
43+- **Tests**: Go (events, config 20+, acp with a self-exec fake agent, agents with fake clock, app) all pass with `-race`; vitest 8; shell 14 + 5; real Alice handshake (`make test-real`) OK; app run under Xvfb: config load, start Alice from the panel, journal, invalid config + live fix, quit stops agents.
44+- **Quality**: golangci-lint 0 issues, svelte-check 0/0, licence headers ok. qlty not available (see #38).
45+- **Docs**: see Changes; all links checked, no FR→EN link.
46+
47+## 2026-10-29 — macOS feedback on M1: traffic lights over "CHANNELS" again
48+
49+- **Cause**: `System.IsMac()` reads `window._wails.environment`, filled asynchronously by the Wails runtime; at page load it can still be empty, so the `macos` class (and `--titlebar-inset`) was set or not depending on timing.
50+- **Fix**: `isMac(System.IsMac(), navigator.userAgent)` in `frontend/src/lib/theme.ts` (WKWebView's user agent contains "Macintosh"), 3 vitest cases; comment on #12. Not yet re-checked on the Mac.
51+- **Docs**: M1 macOS guide (EN/FR) — Settings shows the full path; `open '~/…'` in quotes does not expand `~`; added `open` and restore commands.
52+
53+## 2026-10-29 — Default configuration with the test agents
54+
55+- **Goal**: make the default configuration work with the test agents (owner's request), and give the owner a ready file in `./tmp`.
56+- **Changes**: `scripts/install-test-config.sh` + `make install-test-config` (writes examples/test-agents.yaml at the default location with absolute repo paths and `MM_BIN`/`PATH` in each agent's env — an app opened from the Finder gets a minimal PATH and finds neither mm nor jq; keeps an existing file as `.bak`); Go test `internal/config/testconfig_test.go`; docs (commands reference, M1 guide, develop guide, EN/FR). `tmp/config.yaml` generated for the Mac (MM_BIN=/usr/local/bin/mm, PATH with /opt/homebrew/bin); `tmp/` git-ignored.
57+- **Tests**: Go test passes; app started without --config and with `env -i PATH=/usr/bin:/bin` → Alice starts.
@@ -34,3 +34,24 @@
34 34
35 - **Goal**: after the owner's successful macOS check, close #3 and the milestone.35 - **Goal**: after the owner's successful macOS check, close #3 and the milestone.
36 - **Changes**: #3 last task closed + result comment, issue closed; milestone `Initialize` → closed; ADR 0002 status "verified on macOS"; prototype README updated; `.memory/` summary and handoff updated. No commit by the agent (the owner commits and merges).36 - **Changes**: #3 last task closed + result comment, issue closed; milestone `Initialize` → closed; ADR 0002 status "verified on macOS"; prototype README updated; `.memory/` summary and handoff updated. No commit by the agent (the owner commits and merges).
37+
38+## 2026-10-29 — Milestone M1 · Fondations (#8–#13)
39+
40+- **Goal**: implement M1 in one go (owner's choice), with CI on rickub Actions, pnpm + supportedArchitectures, golangci-lint as go tool, and a ticket for a qlty kit.
41+- **Changes**: ticket #38 (qlty kit); root Wails v3 project (`main.go`, `Taskfile.yml`, `build/`, `frontend/`); `internal/{events,config,acp,agents,app}`; frontend shell (paneforge, panel registry, Agents/Channels/Conversation/Settings/Journal panels, status bar, theme, macOS inset); `Makefile` rewrite; `tools/go.mod` + `.golangci.yml`; `scripts/check-license-headers.sh` (+tests); `.github/workflows/ci.yml`; README; schema fix (`stopIdleAgentsAfter: 0` as a number); ADR 0003 Amendment 1; package diagram; docs EN/FR (tutorial first launch, how-to develop / run tests / test M1 on macOS, references commands + agents-and-events, updated configuration/architecture/memory); tickets #8–#11, #13 closed, #12 open.
42+- **Decisions**: main at the root instead of `cmd/sidekicks` (Wails Taskfiles build `.`); Wails imported only by main.go (internal/app testable without cgo); LRU eviction of an idle agent when the limit is reached, refuse only if all busy; crash never auto-restarts; config channels exposed read-only until M3; bindings committed + `bindings-check` in CI; go.yaml.in/yaml/v3 instead of gopkg.in/yaml.v3; misspell without locale (SDK uses "Cancelled"), French words ignored by explicit rule.
43+- **Tests**: Go (events, config 20+, acp with a self-exec fake agent, agents with fake clock, app) all pass with `-race`; vitest 8; shell 14 + 5; real Alice handshake (`make test-real`) OK; app run under Xvfb: config load, start Alice from the panel, journal, invalid config + live fix, quit stops agents.
44+- **Quality**: golangci-lint 0 issues, svelte-check 0/0, licence headers ok. qlty not available (see #38).
45+- **Docs**: see Changes; all links checked, no FR→EN link.
46+
47+## 2026-10-29 — macOS feedback on M1: traffic lights over "CHANNELS" again
48+
49+- **Cause**: `System.IsMac()` reads `window._wails.environment`, filled asynchronously by the Wails runtime; at page load it can still be empty, so the `macos` class (and `--titlebar-inset`) was set or not depending on timing.
50+- **Fix**: `isMac(System.IsMac(), navigator.userAgent)` in `frontend/src/lib/theme.ts` (WKWebView's user agent contains "Macintosh"), 3 vitest cases; comment on #12. Not yet re-checked on the Mac.
51+- **Docs**: M1 macOS guide (EN/FR) — Settings shows the full path; `open '~/…'` in quotes does not expand `~`; added `open` and restore commands.
52+
53+## 2026-10-29 — Default configuration with the test agents
54+
55+- **Goal**: make the default configuration work with the test agents (owner's request), and give the owner a ready file in `./tmp`.
56+- **Changes**: `scripts/install-test-config.sh` + `make install-test-config` (writes examples/test-agents.yaml at the default location with absolute repo paths and `MM_BIN`/`PATH` in each agent's env — an app opened from the Finder gets a minimal PATH and finds neither mm nor jq; keeps an existing file as `.bak`); Go test `internal/config/testconfig_test.go`; docs (commands reference, M1 guide, develop guide, EN/FR). `tmp/config.yaml` generated for the Mac (MM_BIN=/usr/local/bin/mm, PATH with /opt/homebrew/bin); `tmp/` git-ignored.
57+- **Tests**: Go test passes; app started without --config and with `env -i PATH=/usr/bin:/bin` → Alice starts.
modified .memory/summary.md +20 -15
@@ -4,23 +4,25 @@
44
55 A desktop application (Go + Wails) to talk with AI agents in channels, for non-technical ("citizen") users. Agent-agnostic: every agent is reached over the Agent Client Protocol (ACP, JSON-RPC on stdio). Full brief: `.tickets/issues/0001-draft.yaml`; backlog of 36 issues, 9 epics, 6 milestones in `.tickets/` (IssueSpec format, see `.claude/skills/issuespec`).
66
7-## State (2026-10-29)
7+## State (2026-10-29, end of M1)
88
9-Milestone **Initialize** is closed (issues #2–#7 closed; the UI prototype was verified on macOS by the owner). There is **no application code yet**; M1 (issues #8–#13) starts it.
9+Milestone **Initialize** is closed. Milestone **M1 · Fondations** is implemented on branch `feature/m1-foundations` (issues #8–#11, #13 closed; #12 open until the owner checks the shell on macOS with `docs/fr/how-to/tester-la-milestone-m1-sous-macos.md`). The application opens, loads/validates/watches its configuration, lists the agents, starts/stops them over ACP (lazy start, LRU limit, idle stop, crash → error), shows states and stderr logs. **No conversation yet** (M2: #14–#19).
1010
1111 What exists:
1212
13-- `scripts/agents.sh` — the only way to start test agents: `run <agent>` (stdio pass-through, max 2 simultaneous, unloads the model at exit), `ps`, `unload`, `model`, `list`.
14-- `schema/` — Go package (`git.rickub.com/bots-garden/sidekicks/schema`): configuration JSON Schema + `ValidateConfig`.
15-- `examples/test-agents.yaml` — a valid configuration with the 4 test agents (through `scripts/agents.sh`).
16-- `spikes/acp-trace` (ACP trace client), `spikes/wails-ui` (Wails v3 + Svelte prototype) — throwaway, separate Go modules.
17-- `docs/adr/0001..0003`, `docs/design/` (6 wireframes, draw.io), `docs/diagrams/packages.drawio`, `docs/en|fr/` (Diátaxis).
13+- `main.go` (root, only file importing Wails) · `internal/app` (AgentService, ConfigService, Core) · `internal/agents` (Manager) · `internal/acp` (ACP v1 client) · `internal/config` · `internal/events` · `schema`.
14+- `frontend/`: Svelte 5 + paneforge shell, panel registry (`src/lib/panels/index.ts`), shared state (`src/lib/state.svelte.ts`, `appState`), generated bindings committed in `frontend/bindings`.
15+- Tooling: `Makefile` (entry point), golangci-lint in `tools/go.mod`, `.golangci.yml`, `scripts/check-license-headers.sh`, CI `.github/workflows/ci.yml` (not yet seen running on rickub).
16+- `scripts/agents.sh`, `examples/test-agents.yaml`, `spikes/` (Initialize), docs EN/FR, ADRs, wireframes.
1817
1918 ## Decisions in force
2019
2120 - ACP **v1**, via `github.com/coder/acp-go-sdk` v0.13.5, wrapped in `internal/acp` (ADR 0001). One ACP session per (channel, agent).
2221 - **Wails v3.0.0**, Svelte 5 + TS, paneforge, markdown-it + Shiki (JS engine) + DOMPurify, CodeMirror 6, xterm.js + creack/pty (ADR 0002). Markdown rendered at most once per animation frame. macOS hidden-inset title bar: content near the top-left is padded by `--titlebar-inset` (38px under `:root.macos`, = `InvisibleTitleBarHeight`).
23-- Packages and data model: ADR 0003. YAML = what exists; Pebble v2 = what happens, one DB per config file; channels live only in Pebble (YAML-declared ones are created once). Messages written at end of turn.
22+- Packages and data model: ADR 0003 + Amendment 1 (main at the root, only main.go imports Wails, `internal/events` with an `Emitter` interface, LRU eviction of idle agents at the limit).
23+- Frontend dependencies with **pnpm 12.11.2** and `frontend/pnpm-workspace.yaml` `supportedArchitectures` (darwin+linux): the repository folder is shared between the Mac and the Linux sandbox. Never use npm in `frontend/`.
24+- Go deps: wails v3.0.0, coder/acp-go-sdk v0.13.5, go.yaml.in/yaml/v3, santhosh-tekuri/jsonschema/v6, fsnotify; go.mod says `go 1.26.0`.
25+- Packages and data model (initial): ADR 0003. YAML = what exists; Pebble v2 = what happens, one DB per config file; channels live only in Pebble (YAML-declared ones are created once). Messages written at end of turn.
2426 - Config format v1: `docs/en/reference/configuration.md`; unknown keys are errors; cross-entry rules belong to the loader (#9).
2527 - Licence: custom source-available (`LICENSE`). Every hand-written source file starts with the one-line header `Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE` (comment syntax of the language); not on generated files nor on the owner's root scripts (`git.sh`, `new.*.sh`, `tag-step.sh`).
2628 - ADRs and specs in English; docs bilingual EN + FR; tickets in French.
@@ -29,12 +31,13 @@ What exists:
2931
3032 | | |
3133 | --- | --- |
32-| All tests | `make test` (Go `go test ./...` + `scripts/agents_test.sh`; no network, no real agent) |
33-| Test agents | `scripts/agents.sh run alice` · `ps` · `unload --all` (needs `mm` on PATH or `MM_BIN`, llmman on 127.0.0.1:17434 or host.docker.internal:17434) |
34-| ACP trace | `cd spikes/acp-trace && go run . -agent alice -prompt "…" -out /tmp/t.jsonl` |
35-| UI prototype | `cd spikes/wails-ui && wails3 dev` (macOS) ; Linux build needs `pkg-config libgtk-4-dev libwebkitgtk-6.0-dev` |
36-| Wireframes | `python3 docs/design/wireframes.py` then `rsvg-convert` each SVG |
37-| Package diagram | `python3 .claude/skills/drawio-fancy-diagram/generator/examples/sidekicks_packages.py docs/diagrams` then `rsvg-convert -w 1200 -h 780` |
34+| Everything the CI runs | `make check` (lint + tests; needs `wails3`, `pnpm`, GTK/WebKit dev packages on Linux) |
35+| Tests only | `make test` (Go + vitest + shell) · real agents: `make test-real` |
36+| Run | `SIDEKICKS_CONFIG="$PWD/examples/test-agents.yaml" make dev` · build `make build` · macOS app `make package` |
37+| Bindings | `make bindings` after changing a service/event; CI runs `make bindings-check` |
38+| Test agents | `scripts/agents.sh run alice` · `ps` · `unload --all` (`MM_BIN`, llmman on 127.0.0.1 or host.docker.internal:17434) |
39+| Headless UI check (sandbox) | `Xvfb :97`, `GDK_BACKEND=x11 WEBKIT_DISABLE_DMABUF_RENDERER=1 dbus-launch bin/sidekicks --config …`, `xdotool`, `import -window root` |
40+| Wireframes / package diagram | `python3 docs/design/wireframes.py` · `python3 .claude/skills/drawio-fancy-diagram/generator/examples/sidekicks_packages.py docs/diagrams` + `rsvg-convert -w 1200 -h 800` |
3841
3942 ## Facts about the test agents (mm v1.0.3)
4043
@@ -45,5 +48,7 @@ What exists:
4548 ## Not yet established
4649
4750 - How to exec a shell / read files inside an `sbx` sandbox (issue #28).
48-- Quality gate: `qlty` was not installed in the 2026-10-29 sandbox, so no qlty measurement exists yet (`.quality/` absent).
51+- Quality gate: `qlty` is not in this sandbox → no qlty measurement yet (issue #38: build a sandbox kit with qlty). golangci-lint (go tool) is the stopgap.
52+- Whether the CI workflow runs as-is on rickub (first push of `feature/m1-foundations`).
53+- The M1 shell on macOS (issue #12).
4954 - Whether `.claude/skills/*` (kit files) should be committed with the project is the owner's call.
@@ -4,23 +4,25 @@
4 4
5 A desktop application (Go + Wails) to talk with AI agents in channels, for non-technical ("citizen") users. Agent-agnostic: every agent is reached over the Agent Client Protocol (ACP, JSON-RPC on stdio). Full brief: `.tickets/issues/0001-draft.yaml`; backlog of 36 issues, 9 epics, 6 milestones in `.tickets/` (IssueSpec format, see `.claude/skills/issuespec`).5 A desktop application (Go + Wails) to talk with AI agents in channels, for non-technical ("citizen") users. Agent-agnostic: every agent is reached over the Agent Client Protocol (ACP, JSON-RPC on stdio). Full brief: `.tickets/issues/0001-draft.yaml`; backlog of 36 issues, 9 epics, 6 milestones in `.tickets/` (IssueSpec format, see `.claude/skills/issuespec`).
6 6
7-## State (2026-10-29)7+## State (2026-10-29, end of M1)
8 8
9-Milestone **Initialize** is closed (issues #2–#7 closed; the UI prototype was verified on macOS by the owner). There is **no application code yet**; M1 (issues #8–#13) starts it.9+Milestone **Initialize** is closed. Milestone **M1 · Fondations** is implemented on branch `feature/m1-foundations` (issues #8–#11, #13 closed; #12 open until the owner checks the shell on macOS with `docs/fr/how-to/tester-la-milestone-m1-sous-macos.md`). The application opens, loads/validates/watches its configuration, lists the agents, starts/stops them over ACP (lazy start, LRU limit, idle stop, crash → error), shows states and stderr logs. **No conversation yet** (M2: #14–#19).
10 10
11 What exists:11 What exists:
12 12
13-- `scripts/agents.sh` — the only way to start test agents: `run <agent>` (stdio pass-through, max 2 simultaneous, unloads the model at exit), `ps`, `unload`, `model`, `list`.13+- `main.go` (root, only file importing Wails) · `internal/app` (AgentService, ConfigService, Core) · `internal/agents` (Manager) · `internal/acp` (ACP v1 client) · `internal/config` · `internal/events` · `schema`.
14-- `schema/` — Go package (`git.rickub.com/bots-garden/sidekicks/schema`): configuration JSON Schema + `ValidateConfig`.14+- `frontend/`: Svelte 5 + paneforge shell, panel registry (`src/lib/panels/index.ts`), shared state (`src/lib/state.svelte.ts`, `appState`), generated bindings committed in `frontend/bindings`.
15-- `examples/test-agents.yaml` — a valid configuration with the 4 test agents (through `scripts/agents.sh`).15+- Tooling: `Makefile` (entry point), golangci-lint in `tools/go.mod`, `.golangci.yml`, `scripts/check-license-headers.sh`, CI `.github/workflows/ci.yml` (not yet seen running on rickub).
16-- `spikes/acp-trace` (ACP trace client), `spikes/wails-ui` (Wails v3 + Svelte prototype) — throwaway, separate Go modules.16+- `scripts/agents.sh`, `examples/test-agents.yaml`, `spikes/` (Initialize), docs EN/FR, ADRs, wireframes.
17-- `docs/adr/0001..0003`, `docs/design/` (6 wireframes, draw.io), `docs/diagrams/packages.drawio`, `docs/en|fr/` (Diátaxis).
18 17
19 ## Decisions in force18 ## Decisions in force
20 19
21 - ACP **v1**, via `github.com/coder/acp-go-sdk` v0.13.5, wrapped in `internal/acp` (ADR 0001). One ACP session per (channel, agent).20 - ACP **v1**, via `github.com/coder/acp-go-sdk` v0.13.5, wrapped in `internal/acp` (ADR 0001). One ACP session per (channel, agent).
22 - **Wails v3.0.0**, Svelte 5 + TS, paneforge, markdown-it + Shiki (JS engine) + DOMPurify, CodeMirror 6, xterm.js + creack/pty (ADR 0002). Markdown rendered at most once per animation frame. macOS hidden-inset title bar: content near the top-left is padded by `--titlebar-inset` (38px under `:root.macos`, = `InvisibleTitleBarHeight`).21 - **Wails v3.0.0**, Svelte 5 + TS, paneforge, markdown-it + Shiki (JS engine) + DOMPurify, CodeMirror 6, xterm.js + creack/pty (ADR 0002). Markdown rendered at most once per animation frame. macOS hidden-inset title bar: content near the top-left is padded by `--titlebar-inset` (38px under `:root.macos`, = `InvisibleTitleBarHeight`).
23-- Packages and data model: ADR 0003. YAML = what exists; Pebble v2 = what happens, one DB per config file; channels live only in Pebble (YAML-declared ones are created once). Messages written at end of turn.22+- Packages and data model: ADR 0003 + Amendment 1 (main at the root, only main.go imports Wails, `internal/events` with an `Emitter` interface, LRU eviction of idle agents at the limit).
23+- Frontend dependencies with **pnpm 12.11.2** and `frontend/pnpm-workspace.yaml` `supportedArchitectures` (darwin+linux): the repository folder is shared between the Mac and the Linux sandbox. Never use npm in `frontend/`.
24+- Go deps: wails v3.0.0, coder/acp-go-sdk v0.13.5, go.yaml.in/yaml/v3, santhosh-tekuri/jsonschema/v6, fsnotify; go.mod says `go 1.26.0`.
25+- Packages and data model (initial): ADR 0003. YAML = what exists; Pebble v2 = what happens, one DB per config file; channels live only in Pebble (YAML-declared ones are created once). Messages written at end of turn.
24 - Config format v1: `docs/en/reference/configuration.md`; unknown keys are errors; cross-entry rules belong to the loader (#9).26 - Config format v1: `docs/en/reference/configuration.md`; unknown keys are errors; cross-entry rules belong to the loader (#9).
25 - Licence: custom source-available (`LICENSE`). Every hand-written source file starts with the one-line header `Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE` (comment syntax of the language); not on generated files nor on the owner's root scripts (`git.sh`, `new.*.sh`, `tag-step.sh`).27 - Licence: custom source-available (`LICENSE`). Every hand-written source file starts with the one-line header `Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE` (comment syntax of the language); not on generated files nor on the owner's root scripts (`git.sh`, `new.*.sh`, `tag-step.sh`).
26 - ADRs and specs in English; docs bilingual EN + FR; tickets in French.28 - ADRs and specs in English; docs bilingual EN + FR; tickets in French.
@@ -29,12 +31,13 @@ What exists:
29 31
30 | | |32 | | |
31 | --- | --- |33 | --- | --- |
32-| All tests | `make test` (Go `go test ./...` + `scripts/agents_test.sh`; no network, no real agent) |34+| Everything the CI runs | `make check` (lint + tests; needs `wails3`, `pnpm`, GTK/WebKit dev packages on Linux) |
33-| Test agents | `scripts/agents.sh run alice` · `ps` · `unload --all` (needs `mm` on PATH or `MM_BIN`, llmman on 127.0.0.1:17434 or host.docker.internal:17434) |35+| Tests only | `make test` (Go + vitest + shell) · real agents: `make test-real` |
34-| ACP trace | `cd spikes/acp-trace && go run . -agent alice -prompt "…" -out /tmp/t.jsonl` |36+| Run | `SIDEKICKS_CONFIG="$PWD/examples/test-agents.yaml" make dev` · build `make build` · macOS app `make package` |
35-| UI prototype | `cd spikes/wails-ui && wails3 dev` (macOS) ; Linux build needs `pkg-config libgtk-4-dev libwebkitgtk-6.0-dev` |37+| Bindings | `make bindings` after changing a service/event; CI runs `make bindings-check` |
36-| Wireframes | `python3 docs/design/wireframes.py` then `rsvg-convert` each SVG |38+| Test agents | `scripts/agents.sh run alice` · `ps` · `unload --all` (`MM_BIN`, llmman on 127.0.0.1 or host.docker.internal:17434) |
37-| Package diagram | `python3 .claude/skills/drawio-fancy-diagram/generator/examples/sidekicks_packages.py docs/diagrams` then `rsvg-convert -w 1200 -h 780` |39+| Headless UI check (sandbox) | `Xvfb :97`, `GDK_BACKEND=x11 WEBKIT_DISABLE_DMABUF_RENDERER=1 dbus-launch bin/sidekicks --config …`, `xdotool`, `import -window root` |
40+| Wireframes / package diagram | `python3 docs/design/wireframes.py` · `python3 .claude/skills/drawio-fancy-diagram/generator/examples/sidekicks_packages.py docs/diagrams` + `rsvg-convert -w 1200 -h 800` |
38 41
39 ## Facts about the test agents (mm v1.0.3)42 ## Facts about the test agents (mm v1.0.3)
40 43
@@ -45,5 +48,7 @@ What exists:
45 ## Not yet established48 ## Not yet established
46 49
47 - How to exec a shell / read files inside an `sbx` sandbox (issue #28).50 - How to exec a shell / read files inside an `sbx` sandbox (issue #28).
48-- Quality gate: `qlty` was not installed in the 2026-10-29 sandbox, so no qlty measurement exists yet (`.quality/` absent).51+- Quality gate: `qlty` is not in this sandbox → no qlty measurement yet (issue #38: build a sandbox kit with qlty). golangci-lint (go tool) is the stopgap.
52+- Whether the CI workflow runs as-is on rickub (first push of `feature/m1-foundations`).
53+- The M1 shell on macOS (issue #12).
49 - Whether `.claude/skills/*` (kit files) should be committed with the project is the owner's call.54 - Whether `.claude/skills/*` (kit files) should be committed with the project is the owner's call.
modified .tickets/issues/0008-initialiser-le-projet-go-wails.yaml +40 -10
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 8
33 title: "Initialiser le projet Go + Wails"
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-29T07:20:06.000Z
9-updatedAt: 2026-10-29T08:51:00.000Z
9+updatedAt: 2026-10-29T10:19:30.000Z
1010 labels:
1111 - "kind::chore"
1212 - "area::infra"
@@ -38,68 +38,84 @@ body: |
3838 tasks:
3939 - id: 1
4040 title: "Générer le projet Wails avec le template choisi"
41- state: open
41+ state: closed
4242 priority: high
4343 author:
4444 name: k33g
4545 email: ph.charriere@gmail.com
4646 createdAt: 2026-10-29T07:20:06.000Z
47+ updatedAt: 2026-10-29T10:19:30.000Z
48+ closedAt: 2026-10-29T10:19:30.000Z
4749 - id: 2
4850 title: "Arborescence cmd/ internal/ frontend/"
49- state: open
51+ state: closed
5052 priority: medium
5153 author:
5254 name: k33g
5355 email: ph.charriere@gmail.com
5456 createdAt: 2026-10-29T07:20:06.000Z
57+ updatedAt: 2026-10-29T10:19:30.000Z
58+ closedAt: 2026-10-29T10:19:30.000Z
5559 - id: 3
5660 title: "Taskfile : dev, build, test, lint"
57- state: open
61+ state: closed
5862 priority: medium
5963 author:
6064 name: k33g
6165 email: ph.charriere@gmail.com
6266 createdAt: 2026-10-29T07:20:06.000Z
67+ updatedAt: 2026-10-29T10:19:30.000Z
68+ closedAt: 2026-10-29T10:19:30.000Z
6369 - id: 4
6470 title: "Lint Go et frontend"
65- state: open
71+ state: closed
6672 priority: medium
6773 author:
6874 name: k33g
6975 email: ph.charriere@gmail.com
7076 createdAt: 2026-10-29T07:20:06.000Z
77+ updatedAt: 2026-10-29T10:19:30.000Z
78+ closedAt: 2026-10-29T10:19:30.000Z
7179 - id: 5
7280 title: "CI ou hook pre-push"
73- state: open
81+ state: closed
7482 priority: medium
7583 author:
7684 name: k33g
7785 email: ph.charriere@gmail.com
7886 createdAt: 2026-10-29T07:20:06.000Z
87+ updatedAt: 2026-10-29T10:19:30.000Z
88+ closedAt: 2026-10-29T10:19:30.000Z
7989 - id: 6
8090 title: "README développeur"
81- state: open
91+ state: closed
8292 priority: medium
8393 author:
8494 name: k33g
8595 email: ph.charriere@gmail.com
8696 createdAt: 2026-10-29T07:20:06.000Z
97+ updatedAt: 2026-10-29T10:19:30.000Z
98+ closedAt: 2026-10-29T10:19:30.000Z
8799 - id: 7
88100 title: "Ajouter l'en-tête de licence aux fichiers sources et aux templates"
89- state: open
101+ state: closed
90102 priority: high
91103 author:
92104 name: k33g
93105 email: ph.charriere@gmail.com
94106 createdAt: 2026-10-29T08:00:00.000Z
107+ updatedAt: 2026-10-29T10:19:30.000Z
108+ closedAt: 2026-10-29T10:19:30.000Z
95109 - id: 8
96110 title: "Vérifier la présence de l'en-tête dans task lint"
97- state: open
111+ state: closed
98112 priority: medium
99113 author:
100114 name: k33g
101115 email: ph.charriere@gmail.com
102116 createdAt: 2026-10-29T08:00:00.000Z
117+ updatedAt: 2026-10-29T10:19:30.000Z
118+ closedAt: 2026-10-29T10:19:30.000Z
103119 comments:
104120 - id: 1
105121 author:
@@ -138,3 +154,17 @@ comments:
138154 - `go.mod` existe déjà à la racine (`git.rickub.com/bots-garden/sidekicks`, go 1.26) avec le package `schema`. `wails3 init` génère son propre go.mod : partir du template Svelte dans un dossier temporaire et reporter `main.go`, `frontend/`, `build/`, `Taskfile.yml` à la racine.
139155 - Le `Makefile` est le point d'entrée unique (`make test`) ; ses cibles délèguent à la Taskfile de Wails (ADR 0002).
140156 - Build Linux : `pkg-config`, `libgtk-4-dev`, `libwebkitgtk-6.0-dev`. Le prototype [spikes/wails-ui](spikes/wails-ui/README.md) sert de modèle.
157+ - id: 3
158+ author:
159+ name: k33g
160+ email: ph.charriere@gmail.com
161+ createdAt: 2026-10-29T10:19:30.000Z
162+ body: |
163+ ## Résultat — milestone M1
164+
165+ - Projet Wails v3 (template Svelte) à la racine : `main.go`, `Taskfile.yml` et `build/` générés par Wails, `frontend/` en Svelte 5 + TypeScript. **Écart à l'ADR 0003** : le `main` est à la racine et non dans `cmd/sidekicks`, parce que les Taskfiles générées compilent `.` sur toutes les plateformes (voir l'amendement 1 de [l'ADR 0003](docs/adr/0003-architecture.md)).
166+ - **Taskfile** : le `Makefile` reste le point d'entrée unique (`install`, `bindings`, `frontend`, `dev`, `build`, `package`, `check`, `test*`, `lint*`, `bindings-check`) et délègue la construction à la Taskfile de Wails via `wails3`.
167+ - **Lint** : golangci-lint v2.14.0 épinglé dans `tools/go.mod` (`go tool`), configuré dans `.golangci.yml` ; `svelte-check` ; `scripts/check-license-headers.sh` (avec tests). qlty viendra avec #38.
168+ - **CI** : `.github/workflows/ci.yml` (format GitHub Actions, comme mini-me) : `make bindings-check` puis `make check` à chaque push et MR. **À confirmer côté forge** : premier passage sur rickub.
169+ - **pnpm 12.11.2** avec `frontend/pnpm-workspace.yaml` (`supportedArchitectures`) : un seul `node_modules` pour macOS et Linux.
170+ - README développeur à la racine ; guides EN/FR « Développer l'application » et référence « Commandes ».
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 82 id: 8
3 title: "Initialiser le projet Go + Wails"3 title: "Initialiser le projet Go + Wails"
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-29T07:20:06.000Z8 createdAt: 2026-10-29T07:20:06.000Z
9-updatedAt: 2026-10-29T08:51:00.000Z9+updatedAt: 2026-10-29T10:19:30.000Z
10 labels:10 labels:
11 - "kind::chore"11 - "kind::chore"
12 - "area::infra"12 - "area::infra"
@@ -38,68 +38,84 @@ body: |
38 tasks:38 tasks:
39 - id: 139 - id: 1
40 title: "Générer le projet Wails avec le template choisi"40 title: "Générer le projet Wails avec le template choisi"
41- state: open41+ state: closed
42 priority: high42 priority: high
43 author:43 author:
44 name: k33g44 name: k33g
45 email: ph.charriere@gmail.com45 email: ph.charriere@gmail.com
46 createdAt: 2026-10-29T07:20:06.000Z46 createdAt: 2026-10-29T07:20:06.000Z
47+ updatedAt: 2026-10-29T10:19:30.000Z
48+ closedAt: 2026-10-29T10:19:30.000Z
47 - id: 249 - id: 2
48 title: "Arborescence cmd/ internal/ frontend/"50 title: "Arborescence cmd/ internal/ frontend/"
49- state: open51+ state: closed
50 priority: medium52 priority: medium
51 author:53 author:
52 name: k33g54 name: k33g
53 email: ph.charriere@gmail.com55 email: ph.charriere@gmail.com
54 createdAt: 2026-10-29T07:20:06.000Z56 createdAt: 2026-10-29T07:20:06.000Z
57+ updatedAt: 2026-10-29T10:19:30.000Z
58+ closedAt: 2026-10-29T10:19:30.000Z
55 - id: 359 - id: 3
56 title: "Taskfile : dev, build, test, lint"60 title: "Taskfile : dev, build, test, lint"
57- state: open61+ state: closed
58 priority: medium62 priority: medium
59 author:63 author:
60 name: k33g64 name: k33g
61 email: ph.charriere@gmail.com65 email: ph.charriere@gmail.com
62 createdAt: 2026-10-29T07:20:06.000Z66 createdAt: 2026-10-29T07:20:06.000Z
67+ updatedAt: 2026-10-29T10:19:30.000Z
68+ closedAt: 2026-10-29T10:19:30.000Z
63 - id: 469 - id: 4
64 title: "Lint Go et frontend"70 title: "Lint Go et frontend"
65- state: open71+ state: closed
66 priority: medium72 priority: medium
67 author:73 author:
68 name: k33g74 name: k33g
69 email: ph.charriere@gmail.com75 email: ph.charriere@gmail.com
70 createdAt: 2026-10-29T07:20:06.000Z76 createdAt: 2026-10-29T07:20:06.000Z
77+ updatedAt: 2026-10-29T10:19:30.000Z
78+ closedAt: 2026-10-29T10:19:30.000Z
71 - id: 579 - id: 5
72 title: "CI ou hook pre-push"80 title: "CI ou hook pre-push"
73- state: open81+ state: closed
74 priority: medium82 priority: medium
75 author:83 author:
76 name: k33g84 name: k33g
77 email: ph.charriere@gmail.com85 email: ph.charriere@gmail.com
78 createdAt: 2026-10-29T07:20:06.000Z86 createdAt: 2026-10-29T07:20:06.000Z
87+ updatedAt: 2026-10-29T10:19:30.000Z
88+ closedAt: 2026-10-29T10:19:30.000Z
79 - id: 689 - id: 6
80 title: "README développeur"90 title: "README développeur"
81- state: open91+ state: closed
82 priority: medium92 priority: medium
83 author:93 author:
84 name: k33g94 name: k33g
85 email: ph.charriere@gmail.com95 email: ph.charriere@gmail.com
86 createdAt: 2026-10-29T07:20:06.000Z96 createdAt: 2026-10-29T07:20:06.000Z
97+ updatedAt: 2026-10-29T10:19:30.000Z
98+ closedAt: 2026-10-29T10:19:30.000Z
87 - id: 799 - id: 7
88 title: "Ajouter l'en-tête de licence aux fichiers sources et aux templates"100 title: "Ajouter l'en-tête de licence aux fichiers sources et aux templates"
89- state: open101+ state: closed
90 priority: high102 priority: high
91 author:103 author:
92 name: k33g104 name: k33g
93 email: ph.charriere@gmail.com105 email: ph.charriere@gmail.com
94 createdAt: 2026-10-29T08:00:00.000Z106 createdAt: 2026-10-29T08:00:00.000Z
107+ updatedAt: 2026-10-29T10:19:30.000Z
108+ closedAt: 2026-10-29T10:19:30.000Z
95 - id: 8109 - id: 8
96 title: "Vérifier la présence de l'en-tête dans task lint"110 title: "Vérifier la présence de l'en-tête dans task lint"
97- state: open111+ state: closed
98 priority: medium112 priority: medium
99 author:113 author:
100 name: k33g114 name: k33g
101 email: ph.charriere@gmail.com115 email: ph.charriere@gmail.com
102 createdAt: 2026-10-29T08:00:00.000Z116 createdAt: 2026-10-29T08:00:00.000Z
117+ updatedAt: 2026-10-29T10:19:30.000Z
118+ closedAt: 2026-10-29T10:19:30.000Z
103 comments:119 comments:
104 - id: 1120 - id: 1
105 author:121 author:
@@ -138,3 +154,17 @@ comments:
138 - `go.mod` existe déjà à la racine (`git.rickub.com/bots-garden/sidekicks`, go 1.26) avec le package `schema`. `wails3 init` génère son propre go.mod : partir du template Svelte dans un dossier temporaire et reporter `main.go`, `frontend/`, `build/`, `Taskfile.yml` à la racine.154 - `go.mod` existe déjà à la racine (`git.rickub.com/bots-garden/sidekicks`, go 1.26) avec le package `schema`. `wails3 init` génère son propre go.mod : partir du template Svelte dans un dossier temporaire et reporter `main.go`, `frontend/`, `build/`, `Taskfile.yml` à la racine.
139 - Le `Makefile` est le point d'entrée unique (`make test`) ; ses cibles délèguent à la Taskfile de Wails (ADR 0002).155 - Le `Makefile` est le point d'entrée unique (`make test`) ; ses cibles délèguent à la Taskfile de Wails (ADR 0002).
140 - Build Linux : `pkg-config`, `libgtk-4-dev`, `libwebkitgtk-6.0-dev`. Le prototype [spikes/wails-ui](spikes/wails-ui/README.md) sert de modèle.156 - Build Linux : `pkg-config`, `libgtk-4-dev`, `libwebkitgtk-6.0-dev`. Le prototype [spikes/wails-ui](spikes/wails-ui/README.md) sert de modèle.
157+ - id: 3
158+ author:
159+ name: k33g
160+ email: ph.charriere@gmail.com
161+ createdAt: 2026-10-29T10:19:30.000Z
162+ body: |
163+ ## Résultat — milestone M1
164+
165+ - Projet Wails v3 (template Svelte) à la racine : `main.go`, `Taskfile.yml` et `build/` générés par Wails, `frontend/` en Svelte 5 + TypeScript. **Écart à l'ADR 0003** : le `main` est à la racine et non dans `cmd/sidekicks`, parce que les Taskfiles générées compilent `.` sur toutes les plateformes (voir l'amendement 1 de [l'ADR 0003](docs/adr/0003-architecture.md)).
166+ - **Taskfile** : le `Makefile` reste le point d'entrée unique (`install`, `bindings`, `frontend`, `dev`, `build`, `package`, `check`, `test*`, `lint*`, `bindings-check`) et délègue la construction à la Taskfile de Wails via `wails3`.
167+ - **Lint** : golangci-lint v2.14.0 épinglé dans `tools/go.mod` (`go tool`), configuré dans `.golangci.yml` ; `svelte-check` ; `scripts/check-license-headers.sh` (avec tests). qlty viendra avec #38.
168+ - **CI** : `.github/workflows/ci.yml` (format GitHub Actions, comme mini-me) : `make bindings-check` puis `make check` à chaque push et MR. **À confirmer côté forge** : premier passage sur rickub.
169+ - **pnpm 12.11.2** avec `frontend/pnpm-workspace.yaml` (`supportedArchitectures`) : un seul `node_modules` pour macOS et Linux.
170+ - README développeur à la racine ; guides EN/FR « Développer l'application » et référence « Commandes ».
modified .tickets/issues/0009-charger-valider-et-sauvegarder-la-configuration.yaml +38 -9
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 9
33 title: "Charger, valider et sauvegarder la configuration YAML"
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-29T07:20:07.000Z
9-updatedAt: 2026-10-29T08:51:00.000Z
9+updatedAt: 2026-10-29T10:19:30.000Z
1010 labels:
1111 - "kind::feature"
1212 - "area::config"
@@ -35,60 +35,74 @@ body: |
3535 tasks:
3636 - id: 1
3737 title: "Structs Go et parsing (gopkg.in/yaml.v3)"
38- state: open
38+ state: closed
3939 priority: high
4040 author:
4141 name: k33g
4242 email: ph.charriere@gmail.com
4343 createdAt: 2026-10-29T07:20:07.000Z
44+ updatedAt: 2026-10-29T10:19:30.000Z
45+ closedAt: 2026-10-29T10:19:30.000Z
4446 - id: 2
4547 title: "Validation avec messages lisibles et numéros de ligne"
46- state: open
48+ state: closed
4749 priority: high
4850 author:
4951 name: k33g
5052 email: ph.charriere@gmail.com
5153 createdAt: 2026-10-29T07:20:07.000Z
54+ updatedAt: 2026-10-29T10:19:30.000Z
55+ closedAt: 2026-10-29T10:19:30.000Z
5256 - id: 3
5357 title: "Création d'un fichier par défaut commenté"
54- state: open
58+ state: closed
5559 priority: medium
5660 author:
5761 name: k33g
5862 email: ph.charriere@gmail.com
5963 createdAt: 2026-10-29T07:20:07.000Z
64+ updatedAt: 2026-10-29T10:19:30.000Z
65+ closedAt: 2026-10-29T10:19:30.000Z
6066 - id: 4
6167 title: "Résolution de l'emplacement (défaut, flag, env)"
62- state: open
68+ state: closed
6369 priority: medium
6470 author:
6571 name: k33g
6672 email: ph.charriere@gmail.com
6773 createdAt: 2026-10-29T07:20:07.000Z
74+ updatedAt: 2026-10-29T10:19:30.000Z
75+ closedAt: 2026-10-29T10:19:30.000Z
6876 - id: 5
6977 title: "Watch du fichier et événement de rechargement"
70- state: open
78+ state: closed
7179 priority: medium
7280 author:
7381 name: k33g
7482 email: ph.charriere@gmail.com
7583 createdAt: 2026-10-29T07:20:07.000Z
84+ updatedAt: 2026-10-29T10:19:30.000Z
85+ closedAt: 2026-10-29T10:19:30.000Z
7686 - id: 6
7787 title: "Sauvegarde préservant les commentaires"
78- state: open
88+ state: closed
7989 priority: medium
8090 author:
8191 name: k33g
8292 email: ph.charriere@gmail.com
8393 createdAt: 2026-10-29T07:20:07.000Z
94+ updatedAt: 2026-10-29T10:19:30.000Z
95+ closedAt: 2026-10-29T10:19:30.000Z
8496 - id: 7
8597 title: "Tests unitaires"
86- state: open
98+ state: closed
8799 priority: medium
88100 author:
89101 name: k33g
90102 email: ph.charriere@gmail.com
91103 createdAt: 2026-10-29T07:20:07.000Z
104+ updatedAt: 2026-10-29T10:19:30.000Z
105+ closedAt: 2026-10-29T10:19:30.000Z
92106 comments:
93107 - id: 1
94108 author:
@@ -97,3 +111,18 @@ comments:
97111 createdAt: 2026-10-29T08:51:00.000Z
98112 body: |
99113 Le package `schema` (#4) existe : `schema.ValidateConfig(yaml)` valide la forme du fichier. Il reste au chargeur : les règles inter-entrées (noms uniques, membres de channel existants, `command` trouvable), la traduction des `*jsonschema.ValidationError` en messages humains avec numéro de ligne (exemples dans la référence de configuration), et la résolution des chemins. Bibliothèques déjà choisies : `go.yaml.in/yaml/v3` (préserve les commentaires via l'API Node) et `santhosh-tekuri/jsonschema/v6`.
114+ - id: 2
115+ author:
116+ name: k33g
117+ email: ph.charriere@gmail.com
118+ createdAt: 2026-10-29T10:19:30.000Z
119+ body: |
120+ ## Résultat
121+
122+ Package `internal/config` : `Load`/`Parse` (valeurs par défaut, chemins résolus par rapport au fichier, `~`), `Locate` (flag → `SIDEKICKS_CONFIG` → dernier ouvert → défaut), `EnsureDefault` (fichier commenté pour le citizen user), `Save` (valide puis écrit atomiquement), `AppendAgent` (préserve les commentaires via l'API Node), `Watch` (fsnotify sur le dossier, regroupe les rafales).
123+
124+ - Problèmes localisés et expliqués, en FR ou EN selon `ui.language` : « ligne 4 — agents[0].comand : clé inconnue « comand » ; vouliez-vous dire « command » ? ».
125+ - Règles inter-entrées : noms d'agents et de channels uniques, membres existants ; commande introuvable = simple avertissement.
126+ - YAML : `go.yaml.in/yaml/v3` (successeur maintenu de `gopkg.in/yaml.v3`).
127+ - **Bug trouvé** dans le schéma d'Initialize : `stopIdleAgentsAfter: 0` (nombre) était refusé alors que la référence l'autorise — corrigé, avec un cas de test.
128+ - 20+ tests, dont le rechargement après un enregistrement atomique.
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 92 id: 9
3 title: "Charger, valider et sauvegarder la configuration YAML"3 title: "Charger, valider et sauvegarder la configuration YAML"
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-29T07:20:07.000Z8 createdAt: 2026-10-29T07:20:07.000Z
9-updatedAt: 2026-10-29T08:51:00.000Z9+updatedAt: 2026-10-29T10:19:30.000Z
10 labels:10 labels:
11 - "kind::feature"11 - "kind::feature"
12 - "area::config"12 - "area::config"
@@ -35,60 +35,74 @@ body: |
35 tasks:35 tasks:
36 - id: 136 - id: 1
37 title: "Structs Go et parsing (gopkg.in/yaml.v3)"37 title: "Structs Go et parsing (gopkg.in/yaml.v3)"
38- state: open38+ state: closed
39 priority: high39 priority: high
40 author:40 author:
41 name: k33g41 name: k33g
42 email: ph.charriere@gmail.com42 email: ph.charriere@gmail.com
43 createdAt: 2026-10-29T07:20:07.000Z43 createdAt: 2026-10-29T07:20:07.000Z
44+ updatedAt: 2026-10-29T10:19:30.000Z
45+ closedAt: 2026-10-29T10:19:30.000Z
44 - id: 246 - id: 2
45 title: "Validation avec messages lisibles et numéros de ligne"47 title: "Validation avec messages lisibles et numéros de ligne"
46- state: open48+ state: closed
47 priority: high49 priority: high
48 author:50 author:
49 name: k33g51 name: k33g
50 email: ph.charriere@gmail.com52 email: ph.charriere@gmail.com
51 createdAt: 2026-10-29T07:20:07.000Z53 createdAt: 2026-10-29T07:20:07.000Z
54+ updatedAt: 2026-10-29T10:19:30.000Z
55+ closedAt: 2026-10-29T10:19:30.000Z
52 - id: 356 - id: 3
53 title: "Création d'un fichier par défaut commenté"57 title: "Création d'un fichier par défaut commenté"
54- state: open58+ state: closed
55 priority: medium59 priority: medium
56 author:60 author:
57 name: k33g61 name: k33g
58 email: ph.charriere@gmail.com62 email: ph.charriere@gmail.com
59 createdAt: 2026-10-29T07:20:07.000Z63 createdAt: 2026-10-29T07:20:07.000Z
64+ updatedAt: 2026-10-29T10:19:30.000Z
65+ closedAt: 2026-10-29T10:19:30.000Z
60 - id: 466 - id: 4
61 title: "Résolution de l'emplacement (défaut, flag, env)"67 title: "Résolution de l'emplacement (défaut, flag, env)"
62- state: open68+ state: closed
63 priority: medium69 priority: medium
64 author:70 author:
65 name: k33g71 name: k33g
66 email: ph.charriere@gmail.com72 email: ph.charriere@gmail.com
67 createdAt: 2026-10-29T07:20:07.000Z73 createdAt: 2026-10-29T07:20:07.000Z
74+ updatedAt: 2026-10-29T10:19:30.000Z
75+ closedAt: 2026-10-29T10:19:30.000Z
68 - id: 576 - id: 5
69 title: "Watch du fichier et événement de rechargement"77 title: "Watch du fichier et événement de rechargement"
70- state: open78+ state: closed
71 priority: medium79 priority: medium
72 author:80 author:
73 name: k33g81 name: k33g
74 email: ph.charriere@gmail.com82 email: ph.charriere@gmail.com
75 createdAt: 2026-10-29T07:20:07.000Z83 createdAt: 2026-10-29T07:20:07.000Z
84+ updatedAt: 2026-10-29T10:19:30.000Z
85+ closedAt: 2026-10-29T10:19:30.000Z
76 - id: 686 - id: 6
77 title: "Sauvegarde préservant les commentaires"87 title: "Sauvegarde préservant les commentaires"
78- state: open88+ state: closed
79 priority: medium89 priority: medium
80 author:90 author:
81 name: k33g91 name: k33g
82 email: ph.charriere@gmail.com92 email: ph.charriere@gmail.com
83 createdAt: 2026-10-29T07:20:07.000Z93 createdAt: 2026-10-29T07:20:07.000Z
94+ updatedAt: 2026-10-29T10:19:30.000Z
95+ closedAt: 2026-10-29T10:19:30.000Z
84 - id: 796 - id: 7
85 title: "Tests unitaires"97 title: "Tests unitaires"
86- state: open98+ state: closed
87 priority: medium99 priority: medium
88 author:100 author:
89 name: k33g101 name: k33g
90 email: ph.charriere@gmail.com102 email: ph.charriere@gmail.com
91 createdAt: 2026-10-29T07:20:07.000Z103 createdAt: 2026-10-29T07:20:07.000Z
104+ updatedAt: 2026-10-29T10:19:30.000Z
105+ closedAt: 2026-10-29T10:19:30.000Z
92 comments:106 comments:
93 - id: 1107 - id: 1
94 author:108 author:
@@ -97,3 +111,18 @@ comments:
97 createdAt: 2026-10-29T08:51:00.000Z111 createdAt: 2026-10-29T08:51:00.000Z
98 body: |112 body: |
99 Le package `schema` (#4) existe : `schema.ValidateConfig(yaml)` valide la forme du fichier. Il reste au chargeur : les règles inter-entrées (noms uniques, membres de channel existants, `command` trouvable), la traduction des `*jsonschema.ValidationError` en messages humains avec numéro de ligne (exemples dans la référence de configuration), et la résolution des chemins. Bibliothèques déjà choisies : `go.yaml.in/yaml/v3` (préserve les commentaires via l'API Node) et `santhosh-tekuri/jsonschema/v6`.113 Le package `schema` (#4) existe : `schema.ValidateConfig(yaml)` valide la forme du fichier. Il reste au chargeur : les règles inter-entrées (noms uniques, membres de channel existants, `command` trouvable), la traduction des `*jsonschema.ValidationError` en messages humains avec numéro de ligne (exemples dans la référence de configuration), et la résolution des chemins. Bibliothèques déjà choisies : `go.yaml.in/yaml/v3` (préserve les commentaires via l'API Node) et `santhosh-tekuri/jsonschema/v6`.
114+ - id: 2
115+ author:
116+ name: k33g
117+ email: ph.charriere@gmail.com
118+ createdAt: 2026-10-29T10:19:30.000Z
119+ body: |
120+ ## Résultat
121+
122+ Package `internal/config` : `Load`/`Parse` (valeurs par défaut, chemins résolus par rapport au fichier, `~`), `Locate` (flag → `SIDEKICKS_CONFIG` → dernier ouvert → défaut), `EnsureDefault` (fichier commenté pour le citizen user), `Save` (valide puis écrit atomiquement), `AppendAgent` (préserve les commentaires via l'API Node), `Watch` (fsnotify sur le dossier, regroupe les rafales).
123+
124+ - Problèmes localisés et expliqués, en FR ou EN selon `ui.language` : « ligne 4 — agents[0].comand : clé inconnue « comand » ; vouliez-vous dire « command » ? ».
125+ - Règles inter-entrées : noms d'agents et de channels uniques, membres existants ; commande introuvable = simple avertissement.
126+ - YAML : `go.yaml.in/yaml/v3` (successeur maintenu de `gopkg.in/yaml.v3`).
127+ - **Bug trouvé** dans le schéma d'Initialize : `stopIdleAgentsAfter: 0` (nombre) était refusé alors que la référence l'autorise — corrigé, avec un cas de test.
128+ - 20+ tests, dont le rechargement après un enregistrement atomique.
modified .tickets/issues/0010-client-acp-transport-stdio-et-handshake-initiali.yaml +37 -10
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 10
33 title: "Client ACP : transport stdio et handshake initialize"
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-29T07:20:08.000Z
9-updatedAt: 2026-10-29T08:51:00.000Z
9+updatedAt: 2026-10-29T10:19:30.000Z
1010 labels:
1111 - "kind::feature"
1212 - "area::acp"
@@ -40,68 +40,84 @@ body: |
4040 tasks:
4141 - id: 1
4242 title: "Spawn du processus et pipes stdio"
43- state: open
43+ state: closed
4444 priority: high
4545 author:
4646 name: k33g
4747 email: ph.charriere@gmail.com
4848 createdAt: 2026-10-29T07:20:08.000Z
49+ updatedAt: 2026-10-29T10:19:30.000Z
50+ closedAt: 2026-10-29T10:19:30.000Z
4951 - id: 2
5052 title: "Codec JSON-RPC (corrélation des ids, notifications, requêtes entrantes)"
51- state: open
53+ state: closed
5254 priority: high
5355 author:
5456 name: k33g
5557 email: ph.charriere@gmail.com
5658 createdAt: 2026-10-29T07:20:08.000Z
59+ updatedAt: 2026-10-29T10:19:30.000Z
60+ closedAt: 2026-10-29T10:19:30.000Z
5761 - id: 3
5862 title: "Méthode initialize et stockage des capacités"
59- state: open
63+ state: closed
6064 priority: high
6165 author:
6266 name: k33g
6367 email: ph.charriere@gmail.com
6468 createdAt: 2026-10-29T07:20:08.000Z
69+ updatedAt: 2026-10-29T10:19:30.000Z
70+ closedAt: 2026-10-29T10:19:30.000Z
6571 - id: 4
6672 title: "Capture de stderr dans un log par agent"
67- state: open
73+ state: closed
6874 priority: medium
6975 author:
7076 name: k33g
7177 email: ph.charriere@gmail.com
7278 createdAt: 2026-10-29T07:20:08.000Z
79+ updatedAt: 2026-10-29T10:19:30.000Z
80+ closedAt: 2026-10-29T10:19:30.000Z
7381 - id: 5
7482 title: "Arrêt propre et gestion des crashs"
75- state: open
83+ state: closed
7684 priority: medium
7785 author:
7886 name: k33g
7987 email: ph.charriere@gmail.com
8088 createdAt: 2026-10-29T07:20:08.000Z
89+ updatedAt: 2026-10-29T10:19:30.000Z
90+ closedAt: 2026-10-29T10:19:30.000Z
8191 - id: 6
8292 title: "Faux agent pour les tests unitaires"
83- state: open
93+ state: closed
8494 priority: medium
8595 author:
8696 name: k33g
8797 email: ph.charriere@gmail.com
8898 createdAt: 2026-10-29T07:20:08.000Z
99+ updatedAt: 2026-10-29T10:19:30.000Z
100+ closedAt: 2026-10-29T10:19:30.000Z
89101 - id: 7
90102 title: "Test d'intégration avec Alice"
91- state: open
103+ state: closed
92104 priority: medium
93105 author:
94106 name: k33g
95107 email: ph.charriere@gmail.com
96108 createdAt: 2026-10-29T07:20:08.000Z
109+ updatedAt: 2026-10-29T10:19:30.000Z
110+ closedAt: 2026-10-29T10:19:30.000Z
97111 - id: 8
98112 title: "Build tag realagents pour les tests d'intégration (repris de #7)"
99- state: open
113+ state: closed
100114 priority: high
101115 author:
102116 name: k33g
103117 email: ph.charriere@gmail.com
104118 createdAt: 2026-10-29T08:51:00.000Z
119+ updatedAt: 2026-10-29T10:19:30.000Z
120+ closedAt: 2026-10-29T10:19:30.000Z
105121 comments:
106122 - id: 1
107123 author:
@@ -110,3 +126,14 @@ comments:
110126 createdAt: 2026-10-29T08:51:00.000Z
111127 body: |
112128 Repris de #7 : les tests d'intégration avec de vrais agents utilisent le build tag `realagents` et lancent les agents via `scripts/agents.sh run <agent>` (limite + déchargement). Le SDK retenu est `github.com/coder/acp-go-sdk` v0.13.5 ([ADR 0001](docs/adr/0001-acp.md)) ; [spikes/acp-trace](spikes/acp-trace/main.go) montre son usage côté client.
129+ - id: 2
130+ author:
131+ name: k33g
132+ email: ph.charriere@gmail.com
133+ createdAt: 2026-10-29T10:19:30.000Z
134+ body: |
135+ ## Résultat
136+
137+ Package `internal/acp` : `Start(ctx, Spec, Options)` lance l'agent dans son propre groupe de processus, fait `initialize` (ACP v1, refuse une autre version), capture stderr (callback + 20 dernières lignes, ajoutées au message d'erreur d'un démarrage raté), `Close` ferme stdin puis tue le groupe après 5 s, `Done`/`Err` signalent un crash. Les types du SDK `coder/acp-go-sdk` restent dans le package.
138+
139+ Tests : un faux agent (le binaire de test relancé en mode agent) couvre handshake, fermeture, agent récalcitrant, crash, mauvaise version, sortie prématurée, commande absente, délai d'initialize — tous avec `-race`. Test réel `realagents` avec Alice via `scripts/agents.sh` : OK (`make test-real`).
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 102 id: 10
3 title: "Client ACP : transport stdio et handshake initialize"3 title: "Client ACP : transport stdio et handshake initialize"
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-29T07:20:08.000Z8 createdAt: 2026-10-29T07:20:08.000Z
9-updatedAt: 2026-10-29T08:51:00.000Z9+updatedAt: 2026-10-29T10:19:30.000Z
10 labels:10 labels:
11 - "kind::feature"11 - "kind::feature"
12 - "area::acp"12 - "area::acp"
@@ -40,68 +40,84 @@ body: |
40 tasks:40 tasks:
41 - id: 141 - id: 1
42 title: "Spawn du processus et pipes stdio"42 title: "Spawn du processus et pipes stdio"
43- state: open43+ state: closed
44 priority: high44 priority: high
45 author:45 author:
46 name: k33g46 name: k33g
47 email: ph.charriere@gmail.com47 email: ph.charriere@gmail.com
48 createdAt: 2026-10-29T07:20:08.000Z48 createdAt: 2026-10-29T07:20:08.000Z
49+ updatedAt: 2026-10-29T10:19:30.000Z
50+ closedAt: 2026-10-29T10:19:30.000Z
49 - id: 251 - id: 2
50 title: "Codec JSON-RPC (corrélation des ids, notifications, requêtes entrantes)"52 title: "Codec JSON-RPC (corrélation des ids, notifications, requêtes entrantes)"
51- state: open53+ state: closed
52 priority: high54 priority: high
53 author:55 author:
54 name: k33g56 name: k33g
55 email: ph.charriere@gmail.com57 email: ph.charriere@gmail.com
56 createdAt: 2026-10-29T07:20:08.000Z58 createdAt: 2026-10-29T07:20:08.000Z
59+ updatedAt: 2026-10-29T10:19:30.000Z
60+ closedAt: 2026-10-29T10:19:30.000Z
57 - id: 361 - id: 3
58 title: "Méthode initialize et stockage des capacités"62 title: "Méthode initialize et stockage des capacités"
59- state: open63+ state: closed
60 priority: high64 priority: high
61 author:65 author:
62 name: k33g66 name: k33g
63 email: ph.charriere@gmail.com67 email: ph.charriere@gmail.com
64 createdAt: 2026-10-29T07:20:08.000Z68 createdAt: 2026-10-29T07:20:08.000Z
69+ updatedAt: 2026-10-29T10:19:30.000Z
70+ closedAt: 2026-10-29T10:19:30.000Z
65 - id: 471 - id: 4
66 title: "Capture de stderr dans un log par agent"72 title: "Capture de stderr dans un log par agent"
67- state: open73+ state: closed
68 priority: medium74 priority: medium
69 author:75 author:
70 name: k33g76 name: k33g
71 email: ph.charriere@gmail.com77 email: ph.charriere@gmail.com
72 createdAt: 2026-10-29T07:20:08.000Z78 createdAt: 2026-10-29T07:20:08.000Z
79+ updatedAt: 2026-10-29T10:19:30.000Z
80+ closedAt: 2026-10-29T10:19:30.000Z
73 - id: 581 - id: 5
74 title: "Arrêt propre et gestion des crashs"82 title: "Arrêt propre et gestion des crashs"
75- state: open83+ state: closed
76 priority: medium84 priority: medium
77 author:85 author:
78 name: k33g86 name: k33g
79 email: ph.charriere@gmail.com87 email: ph.charriere@gmail.com
80 createdAt: 2026-10-29T07:20:08.000Z88 createdAt: 2026-10-29T07:20:08.000Z
89+ updatedAt: 2026-10-29T10:19:30.000Z
90+ closedAt: 2026-10-29T10:19:30.000Z
81 - id: 691 - id: 6
82 title: "Faux agent pour les tests unitaires"92 title: "Faux agent pour les tests unitaires"
83- state: open93+ state: closed
84 priority: medium94 priority: medium
85 author:95 author:
86 name: k33g96 name: k33g
87 email: ph.charriere@gmail.com97 email: ph.charriere@gmail.com
88 createdAt: 2026-10-29T07:20:08.000Z98 createdAt: 2026-10-29T07:20:08.000Z
99+ updatedAt: 2026-10-29T10:19:30.000Z
100+ closedAt: 2026-10-29T10:19:30.000Z
89 - id: 7101 - id: 7
90 title: "Test d'intégration avec Alice"102 title: "Test d'intégration avec Alice"
91- state: open103+ state: closed
92 priority: medium104 priority: medium
93 author:105 author:
94 name: k33g106 name: k33g
95 email: ph.charriere@gmail.com107 email: ph.charriere@gmail.com
96 createdAt: 2026-10-29T07:20:08.000Z108 createdAt: 2026-10-29T07:20:08.000Z
109+ updatedAt: 2026-10-29T10:19:30.000Z
110+ closedAt: 2026-10-29T10:19:30.000Z
97 - id: 8111 - id: 8
98 title: "Build tag realagents pour les tests d'intégration (repris de #7)"112 title: "Build tag realagents pour les tests d'intégration (repris de #7)"
99- state: open113+ state: closed
100 priority: high114 priority: high
101 author:115 author:
102 name: k33g116 name: k33g
103 email: ph.charriere@gmail.com117 email: ph.charriere@gmail.com
104 createdAt: 2026-10-29T08:51:00.000Z118 createdAt: 2026-10-29T08:51:00.000Z
119+ updatedAt: 2026-10-29T10:19:30.000Z
120+ closedAt: 2026-10-29T10:19:30.000Z
105 comments:121 comments:
106 - id: 1122 - id: 1
107 author:123 author:
@@ -110,3 +126,14 @@ comments:
110 createdAt: 2026-10-29T08:51:00.000Z126 createdAt: 2026-10-29T08:51:00.000Z
111 body: |127 body: |
112 Repris de #7 : les tests d'intégration avec de vrais agents utilisent le build tag `realagents` et lancent les agents via `scripts/agents.sh run <agent>` (limite + déchargement). Le SDK retenu est `github.com/coder/acp-go-sdk` v0.13.5 ([ADR 0001](docs/adr/0001-acp.md)) ; [spikes/acp-trace](spikes/acp-trace/main.go) montre son usage côté client.128 Repris de #7 : les tests d'intégration avec de vrais agents utilisent le build tag `realagents` et lancent les agents via `scripts/agents.sh run <agent>` (limite + déchargement). Le SDK retenu est `github.com/coder/acp-go-sdk` v0.13.5 ([ADR 0001](docs/adr/0001-acp.md)) ; [spikes/acp-trace](spikes/acp-trace/main.go) montre son usage côté client.
129+ - id: 2
130+ author:
131+ name: k33g
132+ email: ph.charriere@gmail.com
133+ createdAt: 2026-10-29T10:19:30.000Z
134+ body: |
135+ ## Résultat
136+
137+ Package `internal/acp` : `Start(ctx, Spec, Options)` lance l'agent dans son propre groupe de processus, fait `initialize` (ACP v1, refuse une autre version), capture stderr (callback + 20 dernières lignes, ajoutées au message d'erreur d'un démarrage raté), `Close` ferme stdin puis tue le groupe après 5 s, `Done`/`Err` signalent un crash. Les types du SDK `coder/acp-go-sdk` restent dans le package.
138+
139+ Tests : un faux agent (le binaire de test relancé en mode agent) couvre handshake, fermeture, agent récalcitrant, crash, mauvaise version, sortie prématurée, commande absente, délai d'initialize — tous avec `-race`. Test réel `realagents` avec Alice via `scripts/agents.sh` : OK (`make test-real`).
modified .tickets/issues/0011-gerer-le-cycle-de-vie-des-processus-agents.yaml +32 -9
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 11
33 title: "Gérer le cycle de vie des processus agents"
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-29T07:20:09.000Z
9-updatedAt: 2026-10-29T07:20:09.000Z
9+updatedAt: 2026-10-29T10:19:30.000Z
1010 labels:
1111 - "kind::feature"
1212 - "area::acp"
@@ -33,50 +33,73 @@ body: |
3333 tasks:
3434 - id: 1
3535 title: "AgentManager : démarrage paresseux"
36- state: open
36+ state: closed
3737 priority: high
3838 author:
3939 name: k33g
4040 email: ph.charriere@gmail.com
4141 createdAt: 2026-10-29T07:20:09.000Z
42+ updatedAt: 2026-10-29T10:19:30.000Z
43+ closedAt: 2026-10-29T10:19:30.000Z
4244 - id: 2
4345 title: "Arrêt après inactivité"
44- state: open
46+ state: closed
4547 priority: medium
4648 author:
4749 name: k33g
4850 email: ph.charriere@gmail.com
4951 createdAt: 2026-10-29T07:20:09.000Z
52+ updatedAt: 2026-10-29T10:19:30.000Z
53+ closedAt: 2026-10-29T10:19:30.000Z
5054 - id: 3
5155 title: "Limite d'agents simultanés"
52- state: open
56+ state: closed
5357 priority: high
5458 author:
5559 name: k33g
5660 email: ph.charriere@gmail.com
5761 createdAt: 2026-10-29T07:20:09.000Z
62+ updatedAt: 2026-10-29T10:19:30.000Z
63+ closedAt: 2026-10-29T10:19:30.000Z
5864 - id: 4
5965 title: "Machine à états et événements"
60- state: open
66+ state: closed
6167 priority: medium
6268 author:
6369 name: k33g
6470 email: ph.charriere@gmail.com
6571 createdAt: 2026-10-29T07:20:09.000Z
72+ updatedAt: 2026-10-29T10:19:30.000Z
73+ closedAt: 2026-10-29T10:19:30.000Z
6674 - id: 5
6775 title: "Gestion du crash et proposition de redémarrage"
68- state: open
76+ state: closed
6977 priority: medium
7078 author:
7179 name: k33g
7280 email: ph.charriere@gmail.com
7381 createdAt: 2026-10-29T07:20:09.000Z
82+ updatedAt: 2026-10-29T10:19:30.000Z
83+ closedAt: 2026-10-29T10:19:30.000Z
7484 - id: 6
7585 title: "Tests"
76- state: open
86+ state: closed
7787 priority: medium
7888 author:
7989 name: k33g
8090 email: ph.charriere@gmail.com
8191 createdAt: 2026-10-29T07:20:09.000Z
82-comments: []
92+ updatedAt: 2026-10-29T10:19:30.000Z
93+ closedAt: 2026-10-29T10:19:30.000Z
94+comments:
95+ - id: 1
96+ author:
97+ name: k33g
98+ email: ph.charriere@gmail.com
99+ createdAt: 2026-10-29T10:19:30.000Z
100+ body: |
101+ ## Résultat
102+
103+ Package `internal/agents` : `Manager` avec démarrage paresseux (un seul démarrage pour plusieurs demandes simultanées), `Acquire`/release (états busy/ready), limite `maxRunningAgents` — **l'agent inoccupé utilisé le moins récemment est arrêté pour faire de la place** ; si tous sont occupés, le démarrage est refusé —, arrêt après inactivité (`StopIdle`, balayage toutes les 30 s), crash → état `error` avec la raison, jamais de redémarrage automatique (`Restart` le propose), `Reconfigure` (agents ajoutés/retirés, `outdated` si la définition change), journal de 200 lignes et événements `agent:state`/`agent:log`. 13 tests avec horloge simulée, `-race`.
104+
105+ Vérifié dans l'application (Linux) : ▶ sur Alice démarre le vrai mm via `agents.sh`, fermer Sidekicks arrête les agents et décharge les modèles.
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 112 id: 11
3 title: "Gérer le cycle de vie des processus agents"3 title: "Gérer le cycle de vie des processus agents"
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-29T07:20:09.000Z8 createdAt: 2026-10-29T07:20:09.000Z
9-updatedAt: 2026-10-29T07:20:09.000Z9+updatedAt: 2026-10-29T10:19:30.000Z
10 labels:10 labels:
11 - "kind::feature"11 - "kind::feature"
12 - "area::acp"12 - "area::acp"
@@ -33,50 +33,73 @@ body: |
33 tasks:33 tasks:
34 - id: 134 - id: 1
35 title: "AgentManager : démarrage paresseux"35 title: "AgentManager : démarrage paresseux"
36- state: open36+ state: closed
37 priority: high37 priority: high
38 author:38 author:
39 name: k33g39 name: k33g
40 email: ph.charriere@gmail.com40 email: ph.charriere@gmail.com
41 createdAt: 2026-10-29T07:20:09.000Z41 createdAt: 2026-10-29T07:20:09.000Z
42+ updatedAt: 2026-10-29T10:19:30.000Z
43+ closedAt: 2026-10-29T10:19:30.000Z
42 - id: 244 - id: 2
43 title: "Arrêt après inactivité"45 title: "Arrêt après inactivité"
44- state: open46+ state: closed
45 priority: medium47 priority: medium
46 author:48 author:
47 name: k33g49 name: k33g
48 email: ph.charriere@gmail.com50 email: ph.charriere@gmail.com
49 createdAt: 2026-10-29T07:20:09.000Z51 createdAt: 2026-10-29T07:20:09.000Z
52+ updatedAt: 2026-10-29T10:19:30.000Z
53+ closedAt: 2026-10-29T10:19:30.000Z
50 - id: 354 - id: 3
51 title: "Limite d'agents simultanés"55 title: "Limite d'agents simultanés"
52- state: open56+ state: closed
53 priority: high57 priority: high
54 author:58 author:
55 name: k33g59 name: k33g
56 email: ph.charriere@gmail.com60 email: ph.charriere@gmail.com
57 createdAt: 2026-10-29T07:20:09.000Z61 createdAt: 2026-10-29T07:20:09.000Z
62+ updatedAt: 2026-10-29T10:19:30.000Z
63+ closedAt: 2026-10-29T10:19:30.000Z
58 - id: 464 - id: 4
59 title: "Machine à états et événements"65 title: "Machine à états et événements"
60- state: open66+ state: closed
61 priority: medium67 priority: medium
62 author:68 author:
63 name: k33g69 name: k33g
64 email: ph.charriere@gmail.com70 email: ph.charriere@gmail.com
65 createdAt: 2026-10-29T07:20:09.000Z71 createdAt: 2026-10-29T07:20:09.000Z
72+ updatedAt: 2026-10-29T10:19:30.000Z
73+ closedAt: 2026-10-29T10:19:30.000Z
66 - id: 574 - id: 5
67 title: "Gestion du crash et proposition de redémarrage"75 title: "Gestion du crash et proposition de redémarrage"
68- state: open76+ state: closed
69 priority: medium77 priority: medium
70 author:78 author:
71 name: k33g79 name: k33g
72 email: ph.charriere@gmail.com80 email: ph.charriere@gmail.com
73 createdAt: 2026-10-29T07:20:09.000Z81 createdAt: 2026-10-29T07:20:09.000Z
82+ updatedAt: 2026-10-29T10:19:30.000Z
83+ closedAt: 2026-10-29T10:19:30.000Z
74 - id: 684 - id: 6
75 title: "Tests"85 title: "Tests"
76- state: open86+ state: closed
77 priority: medium87 priority: medium
78 author:88 author:
79 name: k33g89 name: k33g
80 email: ph.charriere@gmail.com90 email: ph.charriere@gmail.com
81 createdAt: 2026-10-29T07:20:09.000Z91 createdAt: 2026-10-29T07:20:09.000Z
82-comments: []92+ updatedAt: 2026-10-29T10:19:30.000Z
93+ closedAt: 2026-10-29T10:19:30.000Z
94+comments:
95+ - id: 1
96+ author:
97+ name: k33g
98+ email: ph.charriere@gmail.com
99+ createdAt: 2026-10-29T10:19:30.000Z
100+ body: |
101+ ## Résultat
102+
103+ Package `internal/agents` : `Manager` avec démarrage paresseux (un seul démarrage pour plusieurs demandes simultanées), `Acquire`/release (états busy/ready), limite `maxRunningAgents` — **l'agent inoccupé utilisé le moins récemment est arrêté pour faire de la place** ; si tous sont occupés, le démarrage est refusé —, arrêt après inactivité (`StopIdle`, balayage toutes les 30 s), crash → état `error` avec la raison, jamais de redémarrage automatique (`Restart` le propose), `Reconfigure` (agents ajoutés/retirés, `outdated` si la définition change), journal de 200 lignes et événements `agent:state`/`agent:log`. 13 tests avec horloge simulée, `-race`.
104+
105+ Vérifié dans l'application (Linux) : ▶ sur Alice démarre le vrai mm via `agents.sh`, fermer Sidekicks arrête les agents et décharge les modèles.
modified .tickets/issues/0012-shell-ui-a-panels-inspire-de-zed.yaml +47 -7
@@ -6,7 +6,7 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-29T07:20:10.000Z
9-updatedAt: 2026-10-29T07:20:10.000Z
9+updatedAt: 2026-10-29T11:58:59.000Z
1010 labels:
1111 - "kind::feature"
1212 - "area::ui"
@@ -37,42 +37,82 @@ body: |
3737 tasks:
3838 - id: 1
3939 title: "Layout docks + centre"
40- state: open
40+ state: closed
4141 priority: high
4242 author:
4343 name: k33g
4444 email: ph.charriere@gmail.com
4545 createdAt: 2026-10-29T07:20:10.000Z
46+ updatedAt: 2026-10-29T10:19:30.000Z
47+ closedAt: 2026-10-29T10:19:30.000Z
4648 - id: 2
4749 title: "Redimensionnement et repli"
48- state: open
50+ state: closed
4951 priority: medium
5052 author:
5153 name: k33g
5254 email: ph.charriere@gmail.com
5355 createdAt: 2026-10-29T07:20:10.000Z
56+ updatedAt: 2026-10-29T10:19:30.000Z
57+ closedAt: 2026-10-29T10:19:30.000Z
5458 - id: 3
5559 title: "Persistance de la disposition"
56- state: open
60+ state: closed
5761 priority: medium
5862 author:
5963 name: k33g
6064 email: ph.charriere@gmail.com
6165 createdAt: 2026-10-29T07:20:10.000Z
66+ updatedAt: 2026-10-29T10:19:30.000Z
67+ closedAt: 2026-10-29T10:19:30.000Z
6268 - id: 4
6369 title: "Registre de panels"
64- state: open
70+ state: closed
6571 priority: medium
6672 author:
6773 name: k33g
6874 email: ph.charriere@gmail.com
6975 createdAt: 2026-10-29T07:20:10.000Z
76+ updatedAt: 2026-10-29T10:19:30.000Z
77+ closedAt: 2026-10-29T10:19:30.000Z
7078 - id: 5
7179 title: "Thèmes clair et sombre"
72- state: open
80+ state: closed
7381 priority: medium
7482 author:
7583 name: k33g
7684 email: ph.charriere@gmail.com
7785 createdAt: 2026-10-29T07:20:10.000Z
78-comments: []
86+ updatedAt: 2026-10-29T10:19:30.000Z
87+ closedAt: 2026-10-29T10:19:30.000Z
88+ - id: 6
89+ title: "Vérifier le shell sur macOS (guide de test M1, partie 2)"
90+ state: open
91+ priority: high
92+ author:
93+ name: k33g
94+ email: ph.charriere@gmail.com
95+ createdAt: 2026-10-29T10:19:30.000Z
96+comments:
97+ - id: 1
98+ author:
99+ name: k33g
100+ email: ph.charriere@gmail.com
101+ createdAt: 2026-10-29T10:19:30.000Z
102+ body: |
103+ ## Résultat — à vérifier sur macOS
104+
105+ Shell UI en Svelte 5 + paneforge : docks gauche (Channels, Agents), centre (Conversation, état vide en attendant M2), droite (Settings : fichier, source, problèmes), bas (Journal) ; docks repliables depuis la barre d'état, tailles mémorisées (`autoSaveId`), thème clair/sombre suivant le système ou forcé par `ui.theme`, marge `--titlebar-inset` sous macOS. **Registre de panels** : une fonctionnalité déclare son panel dans `frontend/src/lib/panels/index.ts`, le shell ne change pas.
106+
107+ Vérifié sous Linux (Xvfb) : chargement de la config de test, démarrage d'Alice depuis le panel, journal, configuration invalide puis corrigée à chaud. Reste la vérification sur Mac : [guide de test M1](docs/fr/how-to/tester-la-milestone-m1-sous-macos.md).
108+ - id: 2
109+ author:
110+ name: k33g
111+ email: ph.charriere@gmail.com
112+ createdAt: 2026-10-29T11:58:59.000Z
113+ body: |
114+ Retour du test sur Mac : « CHANNELS » était de nouveau sous les boutons de fenêtre au lancement avec la configuration par défaut.
115+
116+ Cause : `System.IsMac()` lit `window._wails.environment`, que le runtime Wails remplit de façon asynchrone ; au chargement de la page il peut encore être vide (le runtime le dit lui-même dans `drag.js` et utilise une heuristique de secours). Selon le moment, la classe `macos` — et donc la marge `--titlebar-inset` — n'était pas posée. Correctif : `isMac(System.IsMac(), navigator.userAgent)` dans `frontend/src/lib/theme.ts` (le user agent de WKWebView contient « Macintosh »), avec 3 tests. À revérifier sur Mac (point 1 du guide).
117+
118+ Guide corrigé : Settings affiche le chemin complet ; `open '~/…'` entre guillemets ne développe pas `~` — commandes `open` et de remise en place ajoutées.
@@ -6,7 +6,7 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-29T07:20:10.000Z8 createdAt: 2026-10-29T07:20:10.000Z
9-updatedAt: 2026-10-29T07:20:10.000Z9+updatedAt: 2026-10-29T11:58:59.000Z
10 labels:10 labels:
11 - "kind::feature"11 - "kind::feature"
12 - "area::ui"12 - "area::ui"
@@ -37,42 +37,82 @@ body: |
37 tasks:37 tasks:
38 - id: 138 - id: 1
39 title: "Layout docks + centre"39 title: "Layout docks + centre"
40- state: open40+ state: closed
41 priority: high41 priority: high
42 author:42 author:
43 name: k33g43 name: k33g
44 email: ph.charriere@gmail.com44 email: ph.charriere@gmail.com
45 createdAt: 2026-10-29T07:20:10.000Z45 createdAt: 2026-10-29T07:20:10.000Z
46+ updatedAt: 2026-10-29T10:19:30.000Z
47+ closedAt: 2026-10-29T10:19:30.000Z
46 - id: 248 - id: 2
47 title: "Redimensionnement et repli"49 title: "Redimensionnement et repli"
48- state: open50+ state: closed
49 priority: medium51 priority: medium
50 author:52 author:
51 name: k33g53 name: k33g
52 email: ph.charriere@gmail.com54 email: ph.charriere@gmail.com
53 createdAt: 2026-10-29T07:20:10.000Z55 createdAt: 2026-10-29T07:20:10.000Z
56+ updatedAt: 2026-10-29T10:19:30.000Z
57+ closedAt: 2026-10-29T10:19:30.000Z
54 - id: 358 - id: 3
55 title: "Persistance de la disposition"59 title: "Persistance de la disposition"
56- state: open60+ state: closed
57 priority: medium61 priority: medium
58 author:62 author:
59 name: k33g63 name: k33g
60 email: ph.charriere@gmail.com64 email: ph.charriere@gmail.com
61 createdAt: 2026-10-29T07:20:10.000Z65 createdAt: 2026-10-29T07:20:10.000Z
66+ updatedAt: 2026-10-29T10:19:30.000Z
67+ closedAt: 2026-10-29T10:19:30.000Z
62 - id: 468 - id: 4
63 title: "Registre de panels"69 title: "Registre de panels"
64- state: open70+ state: closed
65 priority: medium71 priority: medium
66 author:72 author:
67 name: k33g73 name: k33g
68 email: ph.charriere@gmail.com74 email: ph.charriere@gmail.com
69 createdAt: 2026-10-29T07:20:10.000Z75 createdAt: 2026-10-29T07:20:10.000Z
76+ updatedAt: 2026-10-29T10:19:30.000Z
77+ closedAt: 2026-10-29T10:19:30.000Z
70 - id: 578 - id: 5
71 title: "Thèmes clair et sombre"79 title: "Thèmes clair et sombre"
72- state: open80+ state: closed
73 priority: medium81 priority: medium
74 author:82 author:
75 name: k33g83 name: k33g
76 email: ph.charriere@gmail.com84 email: ph.charriere@gmail.com
77 createdAt: 2026-10-29T07:20:10.000Z85 createdAt: 2026-10-29T07:20:10.000Z
78-comments: []86+ updatedAt: 2026-10-29T10:19:30.000Z
87+ closedAt: 2026-10-29T10:19:30.000Z
88+ - id: 6
89+ title: "Vérifier le shell sur macOS (guide de test M1, partie 2)"
90+ state: open
91+ priority: high
92+ author:
93+ name: k33g
94+ email: ph.charriere@gmail.com
95+ createdAt: 2026-10-29T10:19:30.000Z
96+comments:
97+ - id: 1
98+ author:
99+ name: k33g
100+ email: ph.charriere@gmail.com
101+ createdAt: 2026-10-29T10:19:30.000Z
102+ body: |
103+ ## Résultat — à vérifier sur macOS
104+
105+ Shell UI en Svelte 5 + paneforge : docks gauche (Channels, Agents), centre (Conversation, état vide en attendant M2), droite (Settings : fichier, source, problèmes), bas (Journal) ; docks repliables depuis la barre d'état, tailles mémorisées (`autoSaveId`), thème clair/sombre suivant le système ou forcé par `ui.theme`, marge `--titlebar-inset` sous macOS. **Registre de panels** : une fonctionnalité déclare son panel dans `frontend/src/lib/panels/index.ts`, le shell ne change pas.
106+
107+ Vérifié sous Linux (Xvfb) : chargement de la config de test, démarrage d'Alice depuis le panel, journal, configuration invalide puis corrigée à chaud. Reste la vérification sur Mac : [guide de test M1](docs/fr/how-to/tester-la-milestone-m1-sous-macos.md).
108+ - id: 2
109+ author:
110+ name: k33g
111+ email: ph.charriere@gmail.com
112+ createdAt: 2026-10-29T11:58:59.000Z
113+ body: |
114+ Retour du test sur Mac : « CHANNELS » était de nouveau sous les boutons de fenêtre au lancement avec la configuration par défaut.
115+
116+ Cause : `System.IsMac()` lit `window._wails.environment`, que le runtime Wails remplit de façon asynchrone ; au chargement de la page il peut encore être vide (le runtime le dit lui-même dans `drag.js` et utilise une heuristique de secours). Selon le moment, la classe `macos` — et donc la marge `--titlebar-inset` — n'était pas posée. Correctif : `isMac(System.IsMac(), navigator.userAgent)` dans `frontend/src/lib/theme.ts` (le user agent de WKWebView contient « Macintosh »), avec 3 tests. À revérifier sur Mac (point 1 du guide).
117+
118+ Guide corrigé : Settings affiche le chemin complet ; `open '~/…'` entre guillemets ne développe pas `~` — commandes `open` et de remise en place ajoutées.
modified .tickets/issues/0013-pont-d-evenements-entre-go-et-le-frontend.yaml +27 -7
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 13
33 title: "Pont d'événements entre Go et le frontend"
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-29T07:20:11.000Z
9-updatedAt: 2026-10-29T07:20:11.000Z
9+updatedAt: 2026-10-29T10:19:30.000Z
1010 labels:
1111 - "kind::feature"
1212 - "area::infra"
@@ -30,34 +30,54 @@ body: |
3030 tasks:
3131 - id: 1
3232 title: "Définir les types d'événements"
33- state: open
33+ state: closed
3434 priority: medium
3535 author:
3636 name: k33g
3737 email: ph.charriere@gmail.com
3838 createdAt: 2026-10-29T07:20:11.000Z
39+ updatedAt: 2026-10-29T10:19:30.000Z
40+ closedAt: 2026-10-29T10:19:30.000Z
3941 - id: 2
4042 title: "Émission côté Go"
41- state: open
43+ state: closed
4244 priority: medium
4345 author:
4446 name: k33g
4547 email: ph.charriere@gmail.com
4648 createdAt: 2026-10-29T07:20:11.000Z
49+ updatedAt: 2026-10-29T10:19:30.000Z
50+ closedAt: 2026-10-29T10:19:30.000Z
4751 - id: 3
4852 title: "Abonnement côté frontend avec nettoyage"
49- state: open
53+ state: closed
5054 priority: medium
5155 author:
5256 name: k33g
5357 email: ph.charriere@gmail.com
5458 createdAt: 2026-10-29T07:20:11.000Z
59+ updatedAt: 2026-10-29T10:19:30.000Z
60+ closedAt: 2026-10-29T10:19:30.000Z
5561 - id: 4
5662 title: "Test de débit et batching"
57- state: open
63+ state: closed
5864 priority: medium
5965 author:
6066 name: k33g
6167 email: ph.charriere@gmail.com
6268 createdAt: 2026-10-29T07:20:11.000Z
63-comments: []
69+ updatedAt: 2026-10-29T10:19:30.000Z
70+ closedAt: 2026-10-29T10:19:30.000Z
71+comments:
72+ - id: 1
73+ author:
74+ name: k33g
75+ email: ph.charriere@gmail.com
76+ createdAt: 2026-10-29T10:19:30.000Z
77+ body: |
78+ ## Résultat
79+
80+ - Go : package `internal/events` (noms, charges, interface `Emitter`, `Recorder` pour les tests) ; les événements sont enregistrés avec leur type dans `main.go` (`application.RegisterEvent[T]`), ce qui génère constantes et types TypeScript.
81+ - Frontend : `subscribe(name, handler)` renvoie la fonction de désabonnement (à retourner d'un `$effect`) ; `createFrameBatcher` regroupe une rafale en une mise à jour par frame (testé avec 500 éléments).
82+ - Événements de M1 : `config:changed`, `agent:state`, `agent:log`. Les autres du catalogue arriveront avec leurs fonctionnalités.
83+ - Débit : le rendu « une fois par frame » avait déjà été mesuré à 500 chunks/s dans le spike #3 ; le batching côté Go n'est pas nécessaire.
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 132 id: 13
3 title: "Pont d'événements entre Go et le frontend"3 title: "Pont d'événements entre Go et le frontend"
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-29T07:20:11.000Z8 createdAt: 2026-10-29T07:20:11.000Z
9-updatedAt: 2026-10-29T07:20:11.000Z9+updatedAt: 2026-10-29T10:19:30.000Z
10 labels:10 labels:
11 - "kind::feature"11 - "kind::feature"
12 - "area::infra"12 - "area::infra"
@@ -30,34 +30,54 @@ body: |
30 tasks:30 tasks:
31 - id: 131 - id: 1
32 title: "Définir les types d'événements"32 title: "Définir les types d'événements"
33- state: open33+ state: closed
34 priority: medium34 priority: medium
35 author:35 author:
36 name: k33g36 name: k33g
37 email: ph.charriere@gmail.com37 email: ph.charriere@gmail.com
38 createdAt: 2026-10-29T07:20:11.000Z38 createdAt: 2026-10-29T07:20:11.000Z
39+ updatedAt: 2026-10-29T10:19:30.000Z
40+ closedAt: 2026-10-29T10:19:30.000Z
39 - id: 241 - id: 2
40 title: "Émission côté Go"42 title: "Émission côté Go"
41- state: open43+ state: closed
42 priority: medium44 priority: medium
43 author:45 author:
44 name: k33g46 name: k33g
45 email: ph.charriere@gmail.com47 email: ph.charriere@gmail.com
46 createdAt: 2026-10-29T07:20:11.000Z48 createdAt: 2026-10-29T07:20:11.000Z
49+ updatedAt: 2026-10-29T10:19:30.000Z
50+ closedAt: 2026-10-29T10:19:30.000Z
47 - id: 351 - id: 3
48 title: "Abonnement côté frontend avec nettoyage"52 title: "Abonnement côté frontend avec nettoyage"
49- state: open53+ state: closed
50 priority: medium54 priority: medium
51 author:55 author:
52 name: k33g56 name: k33g
53 email: ph.charriere@gmail.com57 email: ph.charriere@gmail.com
54 createdAt: 2026-10-29T07:20:11.000Z58 createdAt: 2026-10-29T07:20:11.000Z
59+ updatedAt: 2026-10-29T10:19:30.000Z
60+ closedAt: 2026-10-29T10:19:30.000Z
55 - id: 461 - id: 4
56 title: "Test de débit et batching"62 title: "Test de débit et batching"
57- state: open63+ state: closed
58 priority: medium64 priority: medium
59 author:65 author:
60 name: k33g66 name: k33g
61 email: ph.charriere@gmail.com67 email: ph.charriere@gmail.com
62 createdAt: 2026-10-29T07:20:11.000Z68 createdAt: 2026-10-29T07:20:11.000Z
63-comments: []69+ updatedAt: 2026-10-29T10:19:30.000Z
70+ closedAt: 2026-10-29T10:19:30.000Z
71+comments:
72+ - id: 1
73+ author:
74+ name: k33g
75+ email: ph.charriere@gmail.com
76+ createdAt: 2026-10-29T10:19:30.000Z
77+ body: |
78+ ## Résultat
79+
80+ - Go : package `internal/events` (noms, charges, interface `Emitter`, `Recorder` pour les tests) ; les événements sont enregistrés avec leur type dans `main.go` (`application.RegisterEvent[T]`), ce qui génère constantes et types TypeScript.
81+ - Frontend : `subscribe(name, handler)` renvoie la fonction de désabonnement (à retourner d'un `$effect`) ; `createFrameBatcher` regroupe une rafale en une mise à jour par frame (testé avec 500 éléments).
82+ - Événements de M1 : `config:changed`, `agent:state`, `agent:log`. Les autres du catalogue arriveront avec leurs fonctionnalités.
83+ - Débit : le rendu « une fois par frame » avait déjà été mesuré à 500 chunks/s dans le spike #3 ; le batching côté Go n'est pas nécessaire.
added .tickets/issues/0038-creer-un-kit-de-sandbox-avec-qlty-pour-le-contro.yaml +70 -0
new file mode 100644
@@ -0,0 +1,70 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 38
3+title: "Créer un kit de sandbox avec qlty pour le contrôle qualité"
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-29T09:51:31.000Z
9+updatedAt: 2026-10-29T09:51:31.000Z
10+labels:
11+ - "kind::chore"
12+ - "area::quality"
13+ - "priority::medium"
14+ - "size::M"
15+epic: "Qualité & Release"
16+related:
17+ - 8
18+body: |
19+ ## Contexte
20+ La méthode de travail du projet (skill `methodical-dev`) impose un contrôle qualité mesuré par **qlty** (skill `quality`) avant de déclarer une évolution terminée. Or la sandbox utilisée pour Initialize et M1 n'a pas qlty : le skill demande alors de s'arrêter et de **ne pas l'installer à la main**, car la version doit venir du kit de la sandbox (`dev-toolkit`) pour être la même partout.
21+
22+ Résultat : Initialize a été livrée sans mesure qlty, et M1 démarre avec un palliatif décidé avec le propriétaire : **golangci-lint lancé en `go tool`** (version épinglée dans `go.mod`), plus `svelte-check` et la vérification des en-têtes de licence (voir #8).
23+
24+ ## Pourquoi un kit
25+ - qlty agrège plusieurs linters (gofmt, golangci-lint, osv-scanner pour les CVE des dépendances, trufflehog pour les secrets, radarlint…) que golangci-lint seul ne couvre pas : **les vulnérabilités des dépendances et les secrets ne sont pas vérifiés aujourd'hui**.
26+ - Le skill `quality` écrit des rapports datés dans `.quality/` et un historique : sans qlty, aucune tendance n'est suivie.
27+ - Un kit rend l'outillage reproductible : toute nouvelle sandbox (ou un autre agent) a la même version de qlty, de Go, de Node et de pnpm.
28+
29+ ## À faire
30+ Créer (ou compléter) un kit de sandbox Docker qui embarque qlty et les dépendances de build du projet, puis mesurer le projet une première fois.
31+
32+ ## Critères d'acceptation
33+ - [ ] Une sandbox créée avec le kit a `qlty`, Go ≥ 1.26, Node, pnpm, et les paquets de build Wails Linux (`pkg-config`, `libgtk-4-dev`, `libwebkitgtk-6.0-dev`).
34+ - [ ] `python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .` s'exécute et produit `.quality/report-latest.md`.
35+ - [ ] Le premier rapport est commité ; les écarts au gate deviennent des issues.
36+ - [ ] Décider si golangci-lint en `go tool` reste (CI, Mac) ou si qlty le remplace partout.
37+tasks:
38+ - id: 1
39+ title: "Définir le contenu du kit (qlty, Go, Node, pnpm, dépendances Wails Linux)"
40+ state: open
41+ priority: high
42+ author:
43+ name: k33g
44+ email: ph.charriere@gmail.com
45+ createdAt: 2026-10-29T09:51:31.000Z
46+ - id: 2
47+ title: "Construire et publier le kit, créer une sandbox avec"
48+ state: open
49+ priority: high
50+ author:
51+ name: k33g
52+ email: ph.charriere@gmail.com
53+ createdAt: 2026-10-29T09:51:31.000Z
54+ - id: 3
55+ title: "Premier passage du skill quality et rapport commité dans .quality/"
56+ state: open
57+ priority: medium
58+ author:
59+ name: k33g
60+ email: ph.charriere@gmail.com
61+ createdAt: 2026-10-29T09:51:31.000Z
62+ - id: 4
63+ title: "Décider du sort de golangci-lint en go tool (CI et Mac)"
64+ state: open
65+ priority: low
66+ author:
67+ name: k33g
68+ email: ph.charriere@gmail.com
69+ createdAt: 2026-10-29T09:51:31.000Z
70+comments: []
new file mode 100644
@@ -0,0 +1,70 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 38
3+title: "Créer un kit de sandbox avec qlty pour le contrôle qualité"
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-29T09:51:31.000Z
9+updatedAt: 2026-10-29T09:51:31.000Z
10+labels:
11+ - "kind::chore"
12+ - "area::quality"
13+ - "priority::medium"
14+ - "size::M"
15+epic: "Qualité & Release"
16+related:
17+ - 8
18+body: |
19+ ## Contexte
20+ La méthode de travail du projet (skill `methodical-dev`) impose un contrôle qualité mesuré par **qlty** (skill `quality`) avant de déclarer une évolution terminée. Or la sandbox utilisée pour Initialize et M1 n'a pas qlty : le skill demande alors de s'arrêter et de **ne pas l'installer à la main**, car la version doit venir du kit de la sandbox (`dev-toolkit`) pour être la même partout.
21+
22+ Résultat : Initialize a été livrée sans mesure qlty, et M1 démarre avec un palliatif décidé avec le propriétaire : **golangci-lint lancé en `go tool`** (version épinglée dans `go.mod`), plus `svelte-check` et la vérification des en-têtes de licence (voir #8).
23+
24+ ## Pourquoi un kit
25+ - qlty agrège plusieurs linters (gofmt, golangci-lint, osv-scanner pour les CVE des dépendances, trufflehog pour les secrets, radarlint…) que golangci-lint seul ne couvre pas : **les vulnérabilités des dépendances et les secrets ne sont pas vérifiés aujourd'hui**.
26+ - Le skill `quality` écrit des rapports datés dans `.quality/` et un historique : sans qlty, aucune tendance n'est suivie.
27+ - Un kit rend l'outillage reproductible : toute nouvelle sandbox (ou un autre agent) a la même version de qlty, de Go, de Node et de pnpm.
28+
29+ ## À faire
30+ Créer (ou compléter) un kit de sandbox Docker qui embarque qlty et les dépendances de build du projet, puis mesurer le projet une première fois.
31+
32+ ## Critères d'acceptation
33+ - [ ] Une sandbox créée avec le kit a `qlty`, Go ≥ 1.26, Node, pnpm, et les paquets de build Wails Linux (`pkg-config`, `libgtk-4-dev`, `libwebkitgtk-6.0-dev`).
34+ - [ ] `python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .` s'exécute et produit `.quality/report-latest.md`.
35+ - [ ] Le premier rapport est commité ; les écarts au gate deviennent des issues.
36+ - [ ] Décider si golangci-lint en `go tool` reste (CI, Mac) ou si qlty le remplace partout.
37+tasks:
38+ - id: 1
39+ title: "Définir le contenu du kit (qlty, Go, Node, pnpm, dépendances Wails Linux)"
40+ state: open
41+ priority: high
42+ author:
43+ name: k33g
44+ email: ph.charriere@gmail.com
45+ createdAt: 2026-10-29T09:51:31.000Z
46+ - id: 2
47+ title: "Construire et publier le kit, créer une sandbox avec"
48+ state: open
49+ priority: high
50+ author:
51+ name: k33g
52+ email: ph.charriere@gmail.com
53+ createdAt: 2026-10-29T09:51:31.000Z
54+ - id: 3
55+ title: "Premier passage du skill quality et rapport commité dans .quality/"
56+ state: open
57+ priority: medium
58+ author:
59+ name: k33g
60+ email: ph.charriere@gmail.com
61+ createdAt: 2026-10-29T09:51:31.000Z
62+ - id: 4
63+ title: "Décider du sort de golangci-lint en go tool (CI et Mac)"
64+ state: open
65+ priority: low
66+ author:
67+ name: k33g
68+ email: ph.charriere@gmail.com
69+ createdAt: 2026-10-29T09:51:31.000Z
70+comments: []
modified Makefile +65 -5
@@ -1,16 +1,76 @@
11 # Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
22 #
33 # Single entry point for the project's commands. `make help` lists them.
4+# Building and packaging delegate to the Wails Taskfile through the wails3 CLI.
45
5-.PHONY: help test test-go test-scripts
6+GOLANGCI_LINT := go tool -modfile=tools/go.mod golangci-lint
7+PNPM := pnpm --dir frontend
8+
9+.PHONY: help install bindings frontend dev build package install-test-config \
10+ check test test-go test-frontend test-scripts test-real \
11+ lint lint-go lint-frontend lint-headers bindings-check
612
713 help: ## List the available targets
8- @grep -E '^[a-z-]+:.*## ' $(MAKEFILE_LIST) | awk -F':.*## ' '{printf " %-14s %s\n", $$1, $$2}'
14+ @grep -E '^[a-z-]+:.*## ' $(MAKEFILE_LIST) | awk -F':.*## ' '{printf " %-16s %s\n", $$1, $$2}'
15+
16+# ---------------------------------------------------------------- build
17+
18+install: ## Install the frontend dependencies (pnpm, both macOS and Linux binaries)
19+ @$(PNPM) install
20+
21+bindings: ## Regenerate the TypeScript bindings of the Go services and events
22+ @wails3 generate bindings -clean=true -ts -i
23+
24+frontend: install ## Build the frontend into frontend/dist (embedded in the binary)
25+ @$(PNPM) run build
26+
27+# go build / go test need frontend/dist to embed: build it once if missing.
28+frontend/dist/index.html:
29+ @$(MAKE) --no-print-directory frontend
30+
31+dev: ## Run the application with hot reload
32+ @wails3 dev
33+
34+build: ## Build the application into bin/
35+ @wails3 build
36+
37+package: ## Package the application (macOS: bin/sidekicks.app)
38+ @wails3 package
39+
40+install-test-config: ## Write the test agents configuration at the default location (absolute paths)
41+ @scripts/install-test-config.sh
942
10-test: test-go test-scripts ## Run the whole test suite (no real agent, no network)
43+# ---------------------------------------------------------------- tests
1144
12-test-go: ## Run the Go unit tests (spikes/ are separate modules and are not included)
45+check: lint test ## Everything the CI runs: lint, then all the tests
46+
47+test: test-go test-frontend test-scripts ## Run all the tests (no real agent, no network)
48+
49+test-go: frontend/dist/index.html ## Go unit tests
1350 @go test ./...
1451
15-test-scripts: ## Test the shell tooling (scripts/agents.sh)
52+test-frontend: ## Frontend unit tests (vitest)
53+ @$(PNPM) test
54+
55+test-scripts: ## Tests of the shell tooling
1656 @scripts/agents_test.sh
57+ @scripts/check-license-headers_test.sh
58+
59+test-real: ## Tests against the real mm agents (needs mm and llmman; 2 agents at most)
60+ @go test -tags realagents -count=1 ./...
61+
62+# ---------------------------------------------------------------- lint
63+
64+lint: lint-go lint-frontend lint-headers ## Run every linter
65+
66+lint-go: frontend/dist/index.html ## golangci-lint (version pinned in tools/go.mod)
67+ @$(GOLANGCI_LINT) run ./...
68+
69+lint-frontend: ## svelte-check (types and Svelte warnings)
70+ @$(PNPM) run check
71+
72+lint-headers: ## Check the licence header of every source file
73+ @scripts/check-license-headers.sh
74+
75+bindings-check: bindings ## Fail when the committed bindings are not up to date
76+ @git diff --exit-code -- frontend/bindings
@@ -1,16 +1,76 @@
1 # Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE1 # Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2 #2 #
3 # Single entry point for the project's commands. `make help` lists them.3 # Single entry point for the project's commands. `make help` lists them.
4+# Building and packaging delegate to the Wails Taskfile through the wails3 CLI.
4 5
5-.PHONY: help test test-go test-scripts6+GOLANGCI_LINT := go tool -modfile=tools/go.mod golangci-lint
7+PNPM := pnpm --dir frontend
8+
9+.PHONY: help install bindings frontend dev build package install-test-config \
10+ check test test-go test-frontend test-scripts test-real \
11+ lint lint-go lint-frontend lint-headers bindings-check
6 12
7 help: ## List the available targets13 help: ## List the available targets
8- @grep -E '^[a-z-]+:.*## ' $(MAKEFILE_LIST) | awk -F':.*## ' '{printf " %-14s %s\n", $$1, $$2}'14+ @grep -E '^[a-z-]+:.*## ' $(MAKEFILE_LIST) | awk -F':.*## ' '{printf " %-16s %s\n", $$1, $$2}'
15+
16+# ---------------------------------------------------------------- build
17+
18+install: ## Install the frontend dependencies (pnpm, both macOS and Linux binaries)
19+ @$(PNPM) install
20+
21+bindings: ## Regenerate the TypeScript bindings of the Go services and events
22+ @wails3 generate bindings -clean=true -ts -i
23+
24+frontend: install ## Build the frontend into frontend/dist (embedded in the binary)
25+ @$(PNPM) run build
26+
27+# go build / go test need frontend/dist to embed: build it once if missing.
28+frontend/dist/index.html:
29+ @$(MAKE) --no-print-directory frontend
30+
31+dev: ## Run the application with hot reload
32+ @wails3 dev
33+
34+build: ## Build the application into bin/
35+ @wails3 build
36+
37+package: ## Package the application (macOS: bin/sidekicks.app)
38+ @wails3 package
39+
40+install-test-config: ## Write the test agents configuration at the default location (absolute paths)
41+ @scripts/install-test-config.sh
9 42
10-test: test-go test-scripts ## Run the whole test suite (no real agent, no network)43+# ---------------------------------------------------------------- tests
11 44
12-test-go: ## Run the Go unit tests (spikes/ are separate modules and are not included)45+check: lint test ## Everything the CI runs: lint, then all the tests
46+
47+test: test-go test-frontend test-scripts ## Run all the tests (no real agent, no network)
48+
49+test-go: frontend/dist/index.html ## Go unit tests
13 @go test ./...50 @go test ./...
14 51
15-test-scripts: ## Test the shell tooling (scripts/agents.sh)52+test-frontend: ## Frontend unit tests (vitest)
53+ @$(PNPM) test
54+
55+test-scripts: ## Tests of the shell tooling
16 @scripts/agents_test.sh56 @scripts/agents_test.sh
57+ @scripts/check-license-headers_test.sh
58+
59+test-real: ## Tests against the real mm agents (needs mm and llmman; 2 agents at most)
60+ @go test -tags realagents -count=1 ./...
61+
62+# ---------------------------------------------------------------- lint
63+
64+lint: lint-go lint-frontend lint-headers ## Run every linter
65+
66+lint-go: frontend/dist/index.html ## golangci-lint (version pinned in tools/go.mod)
67+ @$(GOLANGCI_LINT) run ./...
68+
69+lint-frontend: ## svelte-check (types and Svelte warnings)
70+ @$(PNPM) run check
71+
72+lint-headers: ## Check the licence header of every source file
73+ @scripts/check-license-headers.sh
74+
75+bindings-check: bindings ## Fail when the committed bindings are not up to date
76+ @git diff --exit-code -- frontend/bindings
added README.md +49 -0
new file mode 100644
@@ -0,0 +1,49 @@
1+# Sidekicks
2+
3+A desktop application to talk with AI agents in channels, for people who are not developers. Agents are reached through the [Agent Client Protocol](https://agentclientprotocol.com) (ACP), so any ACP agent can join. Built with Go and [Wails v3](https://wails.io), interface in Svelte 5.
4+
5+> **Status — milestone M1 (Foundations)**: the application starts, reads its configuration, lists the agents, starts and stops them over ACP, and shows their state and logs. Conversations arrive with milestone M2. Backlog and roadmap: `.tickets/` (IssueSpec).
6+
7+Documentation: [English](docs/en/README.md) · [Français](docs/fr/README.md) · [decisions (ADR)](docs/adr/) · [wireframes](docs/design/README.md)
8+
9+## Develop
10+
11+Prerequisites: Go ≥ 1.26, Node ≥ 20, pnpm 12.11.2 (`npm install -g pnpm@12.11.2`), the Wails CLI (`go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0`), and the Xcode command line tools on macOS — or `pkg-config libgtk-4-dev libwebkitgtk-6.0-dev` on Linux.
12+
13+```bash
14+make install # frontend dependencies (macOS and Linux binaries side by side)
15+make dev # run the application with hot reload
16+make check # lint + every test, as the CI does
17+make package # macOS: bin/sidekicks.app
18+make help # every target
19+```
20+
21+Run it with the four test agents of `agents/` (they need [mm](https://rickub.com/bots-garden/mini-me) and llmman):
22+
23+```bash
24+SIDEKICKS_CONFIG="$PWD/examples/test-agents.yaml" make dev # or: bin/sidekicks --config examples/test-agents.yaml
25+```
26+
27+Details: [How to develop the application](docs/en/how-to/develop-the-application.md) · [How to run the tests](docs/en/how-to/run-the-tests.md).
28+
29+## Layout
30+
31+| Path | What it is |
32+| --- | --- |
33+| `main.go` | Entry point: flags, Wails application, services, window |
34+| `internal/app` | The services bound to the interface (`AgentService`, `ConfigService`) |
35+| `internal/agents` | The agent processes: lazy start, limit, idle stop, states, logs |
36+| `internal/acp` | ACP v1 client over stdio (wraps `coder/acp-go-sdk`) |
37+| `internal/config` | Configuration file: load, validate, locate, create, save, watch |
38+| `internal/events` | Events sent from Go to the interface |
39+| `schema` | JSON Schema of the configuration file |
40+| `frontend` | Svelte 5 interface: panels, shell, state |
41+| `scripts` | Test-agent tooling (`agents.sh`), licence header check |
42+| `agents`, `examples` | The four test agents and a configuration using them |
43+| `spikes` | Throwaway prototypes of milestone Initialize |
44+
45+Package diagram: [docs/diagrams/packages.drawio](docs/diagrams/packages.drawio).
46+
47+## Licence
48+
49+Source-available, see [LICENSE](LICENSE): you may read the code and propose changes; using the code elsewhere or the application commercially needs an agreement. Every hand-written source file starts with a one-line licence header (`make lint-headers` checks it).
new file mode 100644
@@ -0,0 +1,49 @@
1+# Sidekicks
2+
3+A desktop application to talk with AI agents in channels, for people who are not developers. Agents are reached through the [Agent Client Protocol](https://agentclientprotocol.com) (ACP), so any ACP agent can join. Built with Go and [Wails v3](https://wails.io), interface in Svelte 5.
4+
5+> **Status — milestone M1 (Foundations)**: the application starts, reads its configuration, lists the agents, starts and stops them over ACP, and shows their state and logs. Conversations arrive with milestone M2. Backlog and roadmap: `.tickets/` (IssueSpec).
6+
7+Documentation: [English](docs/en/README.md) · [Français](docs/fr/README.md) · [decisions (ADR)](docs/adr/) · [wireframes](docs/design/README.md)
8+
9+## Develop
10+
11+Prerequisites: Go ≥ 1.26, Node ≥ 20, pnpm 12.11.2 (`npm install -g pnpm@12.11.2`), the Wails CLI (`go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0`), and the Xcode command line tools on macOS — or `pkg-config libgtk-4-dev libwebkitgtk-6.0-dev` on Linux.
12+
13+```bash
14+make install # frontend dependencies (macOS and Linux binaries side by side)
15+make dev # run the application with hot reload
16+make check # lint + every test, as the CI does
17+make package # macOS: bin/sidekicks.app
18+make help # every target
19+```
20+
21+Run it with the four test agents of `agents/` (they need [mm](https://rickub.com/bots-garden/mini-me) and llmman):
22+
23+```bash
24+SIDEKICKS_CONFIG="$PWD/examples/test-agents.yaml" make dev # or: bin/sidekicks --config examples/test-agents.yaml
25+```
26+
27+Details: [How to develop the application](docs/en/how-to/develop-the-application.md) · [How to run the tests](docs/en/how-to/run-the-tests.md).
28+
29+## Layout
30+
31+| Path | What it is |
32+| --- | --- |
33+| `main.go` | Entry point: flags, Wails application, services, window |
34+| `internal/app` | The services bound to the interface (`AgentService`, `ConfigService`) |
35+| `internal/agents` | The agent processes: lazy start, limit, idle stop, states, logs |
36+| `internal/acp` | ACP v1 client over stdio (wraps `coder/acp-go-sdk`) |
37+| `internal/config` | Configuration file: load, validate, locate, create, save, watch |
38+| `internal/events` | Events sent from Go to the interface |
39+| `schema` | JSON Schema of the configuration file |
40+| `frontend` | Svelte 5 interface: panels, shell, state |
41+| `scripts` | Test-agent tooling (`agents.sh`), licence header check |
42+| `agents`, `examples` | The four test agents and a configuration using them |
43+| `spikes` | Throwaway prototypes of milestone Initialize |
44+
45+Package diagram: [docs/diagrams/packages.drawio](docs/diagrams/packages.drawio).
46+
47+## Licence
48+
49+Source-available, see [LICENSE](LICENSE): you may read the code and propose changes; using the code elsewhere or the application commercially needs an agreement. Every hand-written source file starts with a one-line licence header (`make lint-headers` checks it).
added Taskfile.yml +68 -0
new file mode 100644
@@ -0,0 +1,68 @@
1+version: '3'
2+
3+vars:
4+ APP_NAME: "sidekicks"
5+ BIN_DIR: "bin"
6+ PACKAGE_MANAGER: '{{.PACKAGE_MANAGER | default "pnpm"}}'
7+ VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
8+ # Target OS for build/package/run. Defaults to the host OS, and is overridden
9+ # by `wails3 build GOOS=...` (or the GOOS env var) for cross-compilation. The
10+ # tasks below dispatch to the matching platform Taskfile via this variable.
11+ GOOS: '{{.GOOS | default OS}}'
12+
13+includes:
14+ common: ./build/Taskfile.yml
15+ windows: ./build/windows/Taskfile.yml
16+ darwin: ./build/darwin/Taskfile.yml
17+ linux: ./build/linux/Taskfile.yml
18+
19+tasks:
20+ build:
21+ summary: Builds the application
22+ cmds:
23+ - task: "{{.GOOS}}:build"
24+
25+ package:
26+ summary: Packages a production build of the application
27+ cmds:
28+ - task: "{{.GOOS}}:package"
29+
30+ run:
31+ summary: Runs the application
32+ cmds:
33+ - task: "{{.GOOS}}:run"
34+
35+ dev:
36+ summary: Runs the application in development mode
37+ cmds:
38+ - wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}
39+
40+ install:msix:tools:
41+ summary: Installs tools required for Windows MSIX packaging
42+ cmds:
43+ - task: windows:install:msix:tools
44+
45+ setup:docker:
46+ summary: Builds Docker image for cross-compilation (~800MB download)
47+ cmds:
48+ - task: common:setup:docker
49+
50+ build:server:
51+ summary: Builds the application in server mode (no GUI, HTTP server only)
52+ cmds:
53+ - task: common:build:server
54+
55+ run:server:
56+ summary: Runs the application in server mode
57+ cmds:
58+ - task: common:run:server
59+
60+ build:docker:
61+ summary: Builds a Docker image for server mode deployment
62+ cmds:
63+ - task: common:build:docker
64+
65+ run:docker:
66+ summary: Builds and runs the Docker image
67+ cmds:
68+ - task: common:run:docker
new file mode 100644
@@ -0,0 +1,68 @@
1+version: '3'
2+
3+vars:
4+ APP_NAME: "sidekicks"
5+ BIN_DIR: "bin"
6+ PACKAGE_MANAGER: '{{.PACKAGE_MANAGER | default "pnpm"}}'
7+ VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
8+ # Target OS for build/package/run. Defaults to the host OS, and is overridden
9+ # by `wails3 build GOOS=...` (or the GOOS env var) for cross-compilation. The
10+ # tasks below dispatch to the matching platform Taskfile via this variable.
11+ GOOS: '{{.GOOS | default OS}}'
12+
13+includes:
14+ common: ./build/Taskfile.yml
15+ windows: ./build/windows/Taskfile.yml
16+ darwin: ./build/darwin/Taskfile.yml
17+ linux: ./build/linux/Taskfile.yml
18+
19+tasks:
20+ build:
21+ summary: Builds the application
22+ cmds:
23+ - task: "{{.GOOS}}:build"
24+
25+ package:
26+ summary: Packages a production build of the application
27+ cmds:
28+ - task: "{{.GOOS}}:package"
29+
30+ run:
31+ summary: Runs the application
32+ cmds:
33+ - task: "{{.GOOS}}:run"
34+
35+ dev:
36+ summary: Runs the application in development mode
37+ cmds:
38+ - wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}
39+
40+ install:msix:tools:
41+ summary: Installs tools required for Windows MSIX packaging
42+ cmds:
43+ - task: windows:install:msix:tools
44+
45+ setup:docker:
46+ summary: Builds Docker image for cross-compilation (~800MB download)
47+ cmds:
48+ - task: common:setup:docker
49+
50+ build:server:
51+ summary: Builds the application in server mode (no GUI, HTTP server only)
52+ cmds:
53+ - task: common:build:server
54+
55+ run:server:
56+ summary: Runs the application in server mode
57+ cmds:
58+ - task: common:run:server
59+
60+ build:docker:
61+ summary: Builds a Docker image for server mode deployment
62+ cmds:
63+ - task: common:build:docker
64+
65+ run:docker:
66+ summary: Builds and runs the Docker image
67+ cmds:
68+ - task: common:run:docker
added build/Taskfile.yml +398 -0
new file mode 100644
@@ -0,0 +1,398 @@
1+version: '3'
2+
3+tasks:
4+ go:mod:tidy:
5+ summary: Runs `go mod tidy`
6+ internal: true
7+ # Universal/multi-arch builds invoke this concurrently from parallel deps;
8+ # two `go mod tidy` processes racing on go.mod can corrupt it (#4637).
9+ run: once
10+ cmds:
11+ - go mod tidy
12+
13+ install:frontend:deps:
14+ summary: Install frontend dependencies
15+ run: once
16+ cmds:
17+ - task: install:frontend:deps:{{.PACKAGE_MANAGER}}
18+
19+ install:frontend:deps:npm:
20+ dir: frontend
21+ sources:
22+ - package.json
23+ - package-lock.json
24+ generates:
25+ - node_modules
26+ preconditions:
27+ - sh: npm version
28+ msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/"
29+ cmds:
30+ - npm install
31+
32+ install:frontend:deps:bun:
33+ dir: frontend
34+ sources:
35+ - package.json
36+ - bun.lock
37+ - bun.lockb
38+ generates:
39+ - node_modules
40+ preconditions:
41+ - sh: bun --version
42+ msg: "bun not found"
43+ cmds:
44+ - bun install
45+
46+ install:frontend:deps:pnpm:
47+ dir: frontend
48+ sources:
49+ - package.json
50+ - pnpm-lock.yaml
51+ generates:
52+ - node_modules
53+ preconditions:
54+ - sh: pnpm --version
55+ msg: "pnpm not found"
56+ cmds:
57+ - pnpm install
58+
59+ install:frontend:deps:yarn:
60+ dir: frontend
61+ sources:
62+ - package.json
63+ - yarn.lock
64+ status:
65+ - test -d node_modules || test -f .pnp.cjs
66+ preconditions:
67+ - sh: yarn --version
68+ msg: "yarn not found"
69+ cmds:
70+ - yarn install
71+
72+ build:frontend:
73+ label: build:frontend (DEV={{.DEV}} RUNNER={{.PACKAGE_MANAGER}})
74+ summary: Build the frontend project
75+ # darwin:build:universal runs its per-arch builds as parallel deps, each of
76+ # which depends on this task. Without run:once the two executions race:
77+ # one regenerates frontend/bindings (-clean deletes it first) while the
78+ # other's bundler is reading it, failing intermittently with
79+ # 'Could not resolve "./bindings/<pkg>"' (#4637).
80+ run: once
81+ dir: frontend
82+ sources:
83+ - "**/*"
84+ - exclude: node_modules/**/*
85+ generates:
86+ - dist/**/*
87+ deps:
88+ - task: install:frontend:deps
89+ - task: generate:bindings
90+ vars:
91+ BUILD_FLAGS:
92+ ref: .BUILD_FLAGS
93+ OBFUSCATED:
94+ ref: .OBFUSCATED
95+ cmds:
96+ - task: frontend:run
97+ vars:
98+ SCRIPT: '{{if eq .DEV "true"}}build:dev{{else}}build{{end}}'
99+ env:
100+ PRODUCTION: '{{if eq .DEV "true"}}false{{else}}true{{end}}'
101+
102+ frontend:run:
103+ summary: Run a frontend script with selected runner
104+ cmds:
105+ - task: frontend:run:{{.PACKAGE_MANAGER}}
106+ vars:
107+ SCRIPT: "{{.SCRIPT}}"
108+ vars:
109+ SCRIPT: "{{.SCRIPT}}"
110+
111+ frontend:run:npm:
112+ dir: frontend
113+ cmds:
114+ - npm run {{.SCRIPT}} -q
115+ vars:
116+ SCRIPT: "{{.SCRIPT}}"
117+
118+ frontend:run:yarn:
119+ dir: frontend
120+ cmds:
121+ - yarn {{.SCRIPT}}
122+ vars:
123+ SCRIPT: "{{.SCRIPT}}"
124+
125+ frontend:run:pnpm:
126+ dir: frontend
127+ cmds:
128+ - pnpm run {{.SCRIPT}}
129+ vars:
130+ SCRIPT: "{{.SCRIPT}}"
131+
132+ frontend:run:bun:
133+ dir: frontend
134+ cmds:
135+ - bun run {{.SCRIPT}}
136+ vars:
137+ SCRIPT: "{{.SCRIPT}}"
138+
139+ frontend:vendor:puppertino:
140+ summary: Fetches Puppertino CSS into frontend/public for consistent mobile styling
141+ sources:
142+ - frontend/public/puppertino/puppertino.css
143+ generates:
144+ - frontend/public/puppertino/puppertino.css
145+ cmds:
146+ - |
147+ set -euo pipefail
148+ mkdir -p frontend/public/puppertino
149+ # If bundled Puppertino exists, prefer it. Otherwise, try to fetch, but don't fail build on error.
150+ if [ ! -f frontend/public/puppertino/puppertino.css ]; then
151+ echo "No bundled Puppertino found. Attempting to fetch from GitHub..."
152+ if curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/dist/css/full.css -o frontend/public/puppertino/puppertino.css; then
153+ curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/LICENSE -o frontend/public/puppertino/LICENSE || true
154+ echo "Puppertino CSS downloaded to frontend/public/puppertino/puppertino.css"
155+ else
156+ echo "Warning: Could not fetch Puppertino CSS. Proceeding without download since template may bundle it."
157+ fi
158+ else
159+ echo "Using bundled Puppertino at frontend/public/puppertino/puppertino.css"
160+ fi
161+ # Ensure index.html includes Puppertino CSS and button classes
162+ INDEX_HTML=frontend/index.html
163+ if [ -f "$INDEX_HTML" ]; then
164+ if ! grep -q 'href="/puppertino/puppertino.css"' "$INDEX_HTML"; then
165+ # Insert Puppertino link tag after style.css link
166+ awk '
167+ /href="\/style.css"\/?/ && !x { print; print " <link rel=\"stylesheet\" href=\"/puppertino/puppertino.css\"/>"; x=1; next }1
168+ ' "$INDEX_HTML" > "$INDEX_HTML.tmp" && mv "$INDEX_HTML.tmp" "$INDEX_HTML"
169+ fi
170+ # Replace default .btn with Puppertino primary button classes if present
171+ sed -E -i'' 's/class=\"btn\"/class=\"p-btn p-prim-col\"/g' "$INDEX_HTML" || true
172+ fi
173+
174+
175+
176+ generate:bindings:
177+ summary: Generates bindings for the frontend
178+ run: once
179+ deps:
180+ - task: go:mod:tidy
181+ sources:
182+ - "**/*.[jt]s"
183+ - exclude: frontend/**/*
184+ - frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output
185+ - "**/*.go"
186+ - go.mod
187+ - go.sum
188+ generates:
189+ - frontend/bindings/**/*
190+ cmds:
191+ - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true{{if eq .OBFUSCATED "true"}} -obfuscated{{end}} -ts -i
192+
193+ generate:icons:
194+ summary: Generates Windows `.ico` and Mac `.icns` from an image; on macOS, `-iconcomposerinput appicon.icon -macassetdir darwin` also produces `Assets.car` from a `.icon` file (skipped on other platforms).
195+ run: once
196+ dir: build
197+ sources:
198+ - "appicon.png"
199+ - "appicon.icon"
200+ generates:
201+ - "darwin/icons.icns"
202+ - "windows/icon.ico"
203+ cmds:
204+ - wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico -iconcomposerinput appicon.icon -macassetdir darwin
205+
206+ dev:frontend:
207+ summary: Runs the frontend in development mode
208+ deps:
209+ - task: install:frontend:deps
210+ cmds:
211+ - task: frontend:dev:{{.PACKAGE_MANAGER}}
212+
213+ frontend:dev:npm:
214+ dir: frontend
215+ cmds:
216+ - npm run dev -- --port {{.VITE_PORT}} --strictPort
217+
218+ frontend:dev:yarn:
219+ dir: frontend
220+ cmds:
221+ - yarn dev --port {{.VITE_PORT}} --strictPort
222+
223+ frontend:dev:pnpm:
224+ dir: frontend
225+ cmds:
226+ - pnpm dev --port {{.VITE_PORT}} --strictPort
227+
228+ frontend:dev:bun:
229+ dir: frontend
230+ cmds:
231+ - bun run dev --port {{.VITE_PORT}} --strictPort
232+
233+ update:build-assets:
234+ summary: Updates the build assets
235+ dir: build
236+ cmds:
237+ - wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir .
238+
239+ build:server:
240+ summary: Builds the application in server mode (no GUI, HTTP server only)
241+ desc: |
242+ Builds a production server binary by default: -tags server,production,
243+ -trimpath and a stripped binary, mirroring the desktop `build` task.
244+ Server mode runs as a pure HTTP server without native GUI dependencies.
245+
246+ Usage: task build:server [DEV=true] [OBFUSCATED=true] [EXTRA_TAGS=tag1,tag2]
247+ DEV=true development server (-tags server, no strip, inlining kept)
248+ OBFUSCATED=true obfuscated build via garble (requires garble installed)
249+ EXTRA_TAGS additional comma-separated build tags
250+ deps:
251+ - task: build:frontend
252+ vars:
253+ DEV:
254+ ref: .DEV
255+ BUILD_FLAGS:
256+ ref: .BUILD_FLAGS
257+ OBFUSCATED:
258+ ref: .OBFUSCATED
259+ preconditions:
260+ - sh: '{{if eq .OBFUSCATED "true"}}command -v garble >/dev/null 2>&1{{else}}true{{end}}'
261+ msg: "garble is required for obfuscated builds. Install it with: go install mvdan.cc/garble@v0.16.0 (requires Go 1.24+). See https://github.com/burrowers/garble/releases for version/toolchain compatibility."
262+ cmds:
263+ - '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}}"'
264+ vars:
265+ BUILD_FLAGS: '-tags server{{if eq .DEV "true"}}{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -buildvcs=false -gcflags=all="-l"{{else}},production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
266+
267+ run:server:
268+ summary: Builds and runs a development server (DEV=true)
269+ deps:
270+ - task: build:server
271+ vars:
272+ DEV: "true"
273+ cmds:
274+ - '"./{{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}}"'
275+
276+ build:docker:
277+ summary: Builds a Docker image for server mode deployment
278+ desc: |
279+ Creates a minimal Docker image containing the production server binary.
280+ Defaults to a pure-Go static binary on a distroless/static base. The
281+ production frontend is built first so the embedded assets are current.
282+
283+ Usage: task build:docker [TAG=myapp:latest] [CGO_ENABLED=1] [GO_IMAGE=...] [RUNTIME_IMAGE=...]
284+ For CGO apps, set CGO_ENABLED=1 with libc-compatible builder/runtime images, e.g.:
285+ task build:docker CGO_ENABLED=1 GO_IMAGE=golang:bookworm RUNTIME_IMAGE=gcr.io/distroless/base-debian12
286+ deps:
287+ # Build the production frontend so frontend/dist (embedded by the Go build
288+ # inside the image) is present and current in the Docker build context.
289+ # Pass the server,production tags so binding generation analyses the same
290+ # build the Docker image compiles, not the default-tag build.
291+ - task: build:frontend
292+ vars:
293+ BUILD_FLAGS: "-tags server,production"
294+ cmds:
295+ - >-
296+ docker build
297+ --build-arg CGO_ENABLED={{.CGO_ENABLED | default "0"}}
298+ --build-arg GO_IMAGE={{.GO_IMAGE | default "golang:alpine"}}
299+ --build-arg RUNTIME_IMAGE={{.RUNTIME_IMAGE | default "gcr.io/distroless/static-debian12"}}
300+ -t {{.TAG | default (printf "%s:latest" .APP_NAME)}}
301+ -f build/docker/Dockerfile.server .
302+ vars:
303+ TAG: "{{.TAG}}"
304+ preconditions:
305+ - sh: docker info > /dev/null 2>&1
306+ msg: "Docker is required. Please install Docker first."
307+ - sh: test -f build/docker/Dockerfile.server
308+ msg: "Dockerfile.server not found. Run 'wails3 update build-assets' to generate it."
309+
310+ run:docker:
311+ summary: Builds and runs the Docker image
312+ desc: |
313+ Builds the Docker image and runs it, exposing port 8080.
314+ Usage: task run:docker [TAG=myapp:latest] [PORT=8080]
315+ Note: The internal container port is always 8080. The PORT variable
316+ only changes the host port mapping. Ensure your app uses port 8080
317+ or modify the Dockerfile to match your ServerOptions.Port setting.
318+ deps:
319+ - task: build:docker
320+ vars:
321+ TAG:
322+ ref: .TAG
323+ cmds:
324+ - docker run --rm -p {{.PORT | default "8080"}}:8080 {{.TAG | default (printf "%s:latest" .APP_NAME)}}
325+ vars:
326+ TAG: "{{.TAG}}"
327+ PORT: "{{.PORT}}"
328+
329+ setup:docker:
330+ summary: Builds Docker image for cross-compilation (~800MB download)
331+ desc: |
332+ Builds the Docker image needed for cross-compiling to any platform.
333+ Run this once to enable cross-platform builds from any OS.
334+ cmds:
335+ - docker build -t wails-cross -f build/docker/Dockerfile.cross build/docker/
336+ preconditions:
337+ - sh: docker info > /dev/null 2>&1
338+ msg: "Docker is required. Please install Docker first."
339+
340+ ios:device:list:
341+ summary: Lists connected iOS devices (UDIDs)
342+ cmds:
343+ - xcrun xcdevice list
344+
345+ ios:run:device:
346+ summary: Build, install, and launch on a physical iPhone using Apple tools (xcodebuild/devicectl)
347+ vars:
348+ PROJECT: '{{.PROJECT}}' # e.g., build/ios/xcode/<YourProject>.xcodeproj
349+ SCHEME: '{{.SCHEME}}' # e.g., ios.dev
350+ CONFIG: '{{.CONFIG | default "Debug"}}'
351+ DERIVED: '{{.DERIVED | default "build/ios/DerivedData"}}'
352+ UDID: '{{.UDID}}' # from `task ios:device:list`
353+ BUNDLE_ID: '{{.BUNDLE_ID}}' # e.g., com.yourco.wails.ios.dev
354+ TEAM_ID: '{{.TEAM_ID}}' # optional, if your project is not already set up for signing
355+ preconditions:
356+ - sh: xcrun -f xcodebuild
357+ msg: "xcodebuild not found. Please install Xcode."
358+ - sh: xcrun -f devicectl
359+ msg: "devicectl not found. Please update to Xcode 15+ (which includes devicectl)."
360+ - sh: test -n '{{.PROJECT}}'
361+ msg: "Set PROJECT to your .xcodeproj path (e.g., PROJECT=build/ios/xcode/App.xcodeproj)."
362+ - sh: test -n '{{.SCHEME}}'
363+ msg: "Set SCHEME to your app scheme (e.g., SCHEME=ios.dev)."
364+ - sh: test -n '{{.UDID}}'
365+ msg: "Set UDID to your device UDID (see: task ios:device:list)."
366+ - sh: test -n '{{.BUNDLE_ID}}'
367+ msg: "Set BUNDLE_ID to your app's bundle identifier (e.g., com.yourco.wails.ios.dev)."
368+ cmds:
369+ - |
370+ set -euo pipefail
371+ echo "Building for device: UDID={{.UDID}} SCHEME={{.SCHEME}} PROJECT={{.PROJECT}}"
372+ XCB_ARGS=(
373+ -project "{{.PROJECT}}"
374+ -scheme "{{.SCHEME}}"
375+ -configuration "{{.CONFIG}}"
376+ -destination "id={{.UDID}}"
377+ -derivedDataPath "{{.DERIVED}}"
378+ -allowProvisioningUpdates
379+ -allowProvisioningDeviceRegistration
380+ )
381+ # Optionally inject signing identifiers if provided
382+ if [ -n '{{.TEAM_ID}}' ]; then XCB_ARGS+=(DEVELOPMENT_TEAM={{.TEAM_ID}}); fi
383+ if [ -n '{{.BUNDLE_ID}}' ]; then XCB_ARGS+=(PRODUCT_BUNDLE_IDENTIFIER={{.BUNDLE_ID}}); fi
384+ xcodebuild "${XCB_ARGS[@]}" build | xcpretty || true
385+ # If xcpretty isn't installed, run without it
386+ if [ "${PIPESTATUS[0]}" -ne 0 ]; then
387+ xcodebuild "${XCB_ARGS[@]}" build
388+ fi
389+ # Find built .app
390+ APP_PATH=$(find "{{.DERIVED}}/Build/Products" -type d -name "*.app" -maxdepth 3 | head -n 1)
391+ if [ -z "$APP_PATH" ]; then
392+ echo "Could not locate built .app under {{.DERIVED}}/Build/Products" >&2
393+ exit 1
394+ fi
395+ echo "Installing: $APP_PATH"
396+ xcrun devicectl device install app --device "{{.UDID}}" "$APP_PATH"
397+ echo "Launching: {{.BUNDLE_ID}}"
398+ xcrun devicectl device process launch --device "{{.UDID}}" --stderr console --stdout console "{{.BUNDLE_ID}}"
new file mode 100644
@@ -0,0 +1,398 @@
1+version: '3'
2+
3+tasks:
4+ go:mod:tidy:
5+ summary: Runs `go mod tidy`
6+ internal: true
7+ # Universal/multi-arch builds invoke this concurrently from parallel deps;
8+ # two `go mod tidy` processes racing on go.mod can corrupt it (#4637).
9+ run: once
10+ cmds:
11+ - go mod tidy
12+
13+ install:frontend:deps:
14+ summary: Install frontend dependencies
15+ run: once
16+ cmds:
17+ - task: install:frontend:deps:{{.PACKAGE_MANAGER}}
18+
19+ install:frontend:deps:npm:
20+ dir: frontend
21+ sources:
22+ - package.json
23+ - package-lock.json
24+ generates:
25+ - node_modules
26+ preconditions:
27+ - sh: npm version
28+ msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/"
29+ cmds:
30+ - npm install
31+
32+ install:frontend:deps:bun:
33+ dir: frontend
34+ sources:
35+ - package.json
36+ - bun.lock
37+ - bun.lockb
38+ generates:
39+ - node_modules
40+ preconditions:
41+ - sh: bun --version
42+ msg: "bun not found"
43+ cmds:
44+ - bun install
45+
46+ install:frontend:deps:pnpm:
47+ dir: frontend
48+ sources:
49+ - package.json
50+ - pnpm-lock.yaml
51+ generates:
52+ - node_modules
53+ preconditions:
54+ - sh: pnpm --version
55+ msg: "pnpm not found"
56+ cmds:
57+ - pnpm install
58+
59+ install:frontend:deps:yarn:
60+ dir: frontend
61+ sources:
62+ - package.json
63+ - yarn.lock
64+ status:
65+ - test -d node_modules || test -f .pnp.cjs
66+ preconditions:
67+ - sh: yarn --version
68+ msg: "yarn not found"
69+ cmds:
70+ - yarn install
71+
72+ build:frontend:
73+ label: build:frontend (DEV={{.DEV}} RUNNER={{.PACKAGE_MANAGER}})
74+ summary: Build the frontend project
75+ # darwin:build:universal runs its per-arch builds as parallel deps, each of
76+ # which depends on this task. Without run:once the two executions race:
77+ # one regenerates frontend/bindings (-clean deletes it first) while the
78+ # other's bundler is reading it, failing intermittently with
79+ # 'Could not resolve "./bindings/<pkg>"' (#4637).
80+ run: once
81+ dir: frontend
82+ sources:
83+ - "**/*"
84+ - exclude: node_modules/**/*
85+ generates:
86+ - dist/**/*
87+ deps:
88+ - task: install:frontend:deps
89+ - task: generate:bindings
90+ vars:
91+ BUILD_FLAGS:
92+ ref: .BUILD_FLAGS
93+ OBFUSCATED:
94+ ref: .OBFUSCATED
95+ cmds:
96+ - task: frontend:run
97+ vars:
98+ SCRIPT: '{{if eq .DEV "true"}}build:dev{{else}}build{{end}}'
99+ env:
100+ PRODUCTION: '{{if eq .DEV "true"}}false{{else}}true{{end}}'
101+
102+ frontend:run:
103+ summary: Run a frontend script with selected runner
104+ cmds:
105+ - task: frontend:run:{{.PACKAGE_MANAGER}}
106+ vars:
107+ SCRIPT: "{{.SCRIPT}}"
108+ vars:
109+ SCRIPT: "{{.SCRIPT}}"
110+
111+ frontend:run:npm:
112+ dir: frontend
113+ cmds:
114+ - npm run {{.SCRIPT}} -q
115+ vars:
116+ SCRIPT: "{{.SCRIPT}}"
117+
118+ frontend:run:yarn:
119+ dir: frontend
120+ cmds:
121+ - yarn {{.SCRIPT}}
122+ vars:
123+ SCRIPT: "{{.SCRIPT}}"
124+
125+ frontend:run:pnpm:
126+ dir: frontend
127+ cmds:
128+ - pnpm run {{.SCRIPT}}
129+ vars:
130+ SCRIPT: "{{.SCRIPT}}"
131+
132+ frontend:run:bun:
133+ dir: frontend
134+ cmds:
135+ - bun run {{.SCRIPT}}
136+ vars:
137+ SCRIPT: "{{.SCRIPT}}"
138+
139+ frontend:vendor:puppertino:
140+ summary: Fetches Puppertino CSS into frontend/public for consistent mobile styling
141+ sources:
142+ - frontend/public/puppertino/puppertino.css
143+ generates:
144+ - frontend/public/puppertino/puppertino.css
145+ cmds:
146+ - |
147+ set -euo pipefail
148+ mkdir -p frontend/public/puppertino
149+ # If bundled Puppertino exists, prefer it. Otherwise, try to fetch, but don't fail build on error.
150+ if [ ! -f frontend/public/puppertino/puppertino.css ]; then
151+ echo "No bundled Puppertino found. Attempting to fetch from GitHub..."
152+ if curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/dist/css/full.css -o frontend/public/puppertino/puppertino.css; then
153+ curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/LICENSE -o frontend/public/puppertino/LICENSE || true
154+ echo "Puppertino CSS downloaded to frontend/public/puppertino/puppertino.css"
155+ else
156+ echo "Warning: Could not fetch Puppertino CSS. Proceeding without download since template may bundle it."
157+ fi
158+ else
159+ echo "Using bundled Puppertino at frontend/public/puppertino/puppertino.css"
160+ fi
161+ # Ensure index.html includes Puppertino CSS and button classes
162+ INDEX_HTML=frontend/index.html
163+ if [ -f "$INDEX_HTML" ]; then
164+ if ! grep -q 'href="/puppertino/puppertino.css"' "$INDEX_HTML"; then
165+ # Insert Puppertino link tag after style.css link
166+ awk '
167+ /href="\/style.css"\/?/ && !x { print; print " <link rel=\"stylesheet\" href=\"/puppertino/puppertino.css\"/>"; x=1; next }1
168+ ' "$INDEX_HTML" > "$INDEX_HTML.tmp" && mv "$INDEX_HTML.tmp" "$INDEX_HTML"
169+ fi
170+ # Replace default .btn with Puppertino primary button classes if present
171+ sed -E -i'' 's/class=\"btn\"/class=\"p-btn p-prim-col\"/g' "$INDEX_HTML" || true
172+ fi
173+
174+
175+
176+ generate:bindings:
177+ summary: Generates bindings for the frontend
178+ run: once
179+ deps:
180+ - task: go:mod:tidy
181+ sources:
182+ - "**/*.[jt]s"
183+ - exclude: frontend/**/*
184+ - frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output
185+ - "**/*.go"
186+ - go.mod
187+ - go.sum
188+ generates:
189+ - frontend/bindings/**/*
190+ cmds:
191+ - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true{{if eq .OBFUSCATED "true"}} -obfuscated{{end}} -ts -i
192+
193+ generate:icons:
194+ summary: Generates Windows `.ico` and Mac `.icns` from an image; on macOS, `-iconcomposerinput appicon.icon -macassetdir darwin` also produces `Assets.car` from a `.icon` file (skipped on other platforms).
195+ run: once
196+ dir: build
197+ sources:
198+ - "appicon.png"
199+ - "appicon.icon"
200+ generates:
201+ - "darwin/icons.icns"
202+ - "windows/icon.ico"
203+ cmds:
204+ - wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico -iconcomposerinput appicon.icon -macassetdir darwin
205+
206+ dev:frontend:
207+ summary: Runs the frontend in development mode
208+ deps:
209+ - task: install:frontend:deps
210+ cmds:
211+ - task: frontend:dev:{{.PACKAGE_MANAGER}}
212+
213+ frontend:dev:npm:
214+ dir: frontend
215+ cmds:
216+ - npm run dev -- --port {{.VITE_PORT}} --strictPort
217+
218+ frontend:dev:yarn:
219+ dir: frontend
220+ cmds:
221+ - yarn dev --port {{.VITE_PORT}} --strictPort
222+
223+ frontend:dev:pnpm:
224+ dir: frontend
225+ cmds:
226+ - pnpm dev --port {{.VITE_PORT}} --strictPort
227+
228+ frontend:dev:bun:
229+ dir: frontend
230+ cmds:
231+ - bun run dev --port {{.VITE_PORT}} --strictPort
232+
233+ update:build-assets:
234+ summary: Updates the build assets
235+ dir: build
236+ cmds:
237+ - wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir .
238+
239+ build:server:
240+ summary: Builds the application in server mode (no GUI, HTTP server only)
241+ desc: |
242+ Builds a production server binary by default: -tags server,production,
243+ -trimpath and a stripped binary, mirroring the desktop `build` task.
244+ Server mode runs as a pure HTTP server without native GUI dependencies.
245+
246+ Usage: task build:server [DEV=true] [OBFUSCATED=true] [EXTRA_TAGS=tag1,tag2]
247+ DEV=true development server (-tags server, no strip, inlining kept)
248+ OBFUSCATED=true obfuscated build via garble (requires garble installed)
249+ EXTRA_TAGS additional comma-separated build tags
250+ deps:
251+ - task: build:frontend
252+ vars:
253+ DEV:
254+ ref: .DEV
255+ BUILD_FLAGS:
256+ ref: .BUILD_FLAGS
257+ OBFUSCATED:
258+ ref: .OBFUSCATED
259+ preconditions:
260+ - sh: '{{if eq .OBFUSCATED "true"}}command -v garble >/dev/null 2>&1{{else}}true{{end}}'
261+ msg: "garble is required for obfuscated builds. Install it with: go install mvdan.cc/garble@v0.16.0 (requires Go 1.24+). See https://github.com/burrowers/garble/releases for version/toolchain compatibility."
262+ cmds:
263+ - '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}}"'
264+ vars:
265+ BUILD_FLAGS: '-tags server{{if eq .DEV "true"}}{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -buildvcs=false -gcflags=all="-l"{{else}},production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
266+
267+ run:server:
268+ summary: Builds and runs a development server (DEV=true)
269+ deps:
270+ - task: build:server
271+ vars:
272+ DEV: "true"
273+ cmds:
274+ - '"./{{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}}"'
275+
276+ build:docker:
277+ summary: Builds a Docker image for server mode deployment
278+ desc: |
279+ Creates a minimal Docker image containing the production server binary.
280+ Defaults to a pure-Go static binary on a distroless/static base. The
281+ production frontend is built first so the embedded assets are current.
282+
283+ Usage: task build:docker [TAG=myapp:latest] [CGO_ENABLED=1] [GO_IMAGE=...] [RUNTIME_IMAGE=...]
284+ For CGO apps, set CGO_ENABLED=1 with libc-compatible builder/runtime images, e.g.:
285+ task build:docker CGO_ENABLED=1 GO_IMAGE=golang:bookworm RUNTIME_IMAGE=gcr.io/distroless/base-debian12
286+ deps:
287+ # Build the production frontend so frontend/dist (embedded by the Go build
288+ # inside the image) is present and current in the Docker build context.
289+ # Pass the server,production tags so binding generation analyses the same
290+ # build the Docker image compiles, not the default-tag build.
291+ - task: build:frontend
292+ vars:
293+ BUILD_FLAGS: "-tags server,production"
294+ cmds:
295+ - >-
296+ docker build
297+ --build-arg CGO_ENABLED={{.CGO_ENABLED | default "0"}}
298+ --build-arg GO_IMAGE={{.GO_IMAGE | default "golang:alpine"}}
299+ --build-arg RUNTIME_IMAGE={{.RUNTIME_IMAGE | default "gcr.io/distroless/static-debian12"}}
300+ -t {{.TAG | default (printf "%s:latest" .APP_NAME)}}
301+ -f build/docker/Dockerfile.server .
302+ vars:
303+ TAG: "{{.TAG}}"
304+ preconditions:
305+ - sh: docker info > /dev/null 2>&1
306+ msg: "Docker is required. Please install Docker first."
307+ - sh: test -f build/docker/Dockerfile.server
308+ msg: "Dockerfile.server not found. Run 'wails3 update build-assets' to generate it."
309+
310+ run:docker:
311+ summary: Builds and runs the Docker image
312+ desc: |
313+ Builds the Docker image and runs it, exposing port 8080.
314+ Usage: task run:docker [TAG=myapp:latest] [PORT=8080]
315+ Note: The internal container port is always 8080. The PORT variable
316+ only changes the host port mapping. Ensure your app uses port 8080
317+ or modify the Dockerfile to match your ServerOptions.Port setting.
318+ deps:
319+ - task: build:docker
320+ vars:
321+ TAG:
322+ ref: .TAG
323+ cmds:
324+ - docker run --rm -p {{.PORT | default "8080"}}:8080 {{.TAG | default (printf "%s:latest" .APP_NAME)}}
325+ vars:
326+ TAG: "{{.TAG}}"
327+ PORT: "{{.PORT}}"
328+
329+ setup:docker:
330+ summary: Builds Docker image for cross-compilation (~800MB download)
331+ desc: |
332+ Builds the Docker image needed for cross-compiling to any platform.
333+ Run this once to enable cross-platform builds from any OS.
334+ cmds:
335+ - docker build -t wails-cross -f build/docker/Dockerfile.cross build/docker/
336+ preconditions:
337+ - sh: docker info > /dev/null 2>&1
338+ msg: "Docker is required. Please install Docker first."
339+
340+ ios:device:list:
341+ summary: Lists connected iOS devices (UDIDs)
342+ cmds:
343+ - xcrun xcdevice list
344+
345+ ios:run:device:
346+ summary: Build, install, and launch on a physical iPhone using Apple tools (xcodebuild/devicectl)
347+ vars:
348+ PROJECT: '{{.PROJECT}}' # e.g., build/ios/xcode/<YourProject>.xcodeproj
349+ SCHEME: '{{.SCHEME}}' # e.g., ios.dev
350+ CONFIG: '{{.CONFIG | default "Debug"}}'
351+ DERIVED: '{{.DERIVED | default "build/ios/DerivedData"}}'
352+ UDID: '{{.UDID}}' # from `task ios:device:list`
353+ BUNDLE_ID: '{{.BUNDLE_ID}}' # e.g., com.yourco.wails.ios.dev
354+ TEAM_ID: '{{.TEAM_ID}}' # optional, if your project is not already set up for signing
355+ preconditions:
356+ - sh: xcrun -f xcodebuild
357+ msg: "xcodebuild not found. Please install Xcode."
358+ - sh: xcrun -f devicectl
359+ msg: "devicectl not found. Please update to Xcode 15+ (which includes devicectl)."
360+ - sh: test -n '{{.PROJECT}}'
361+ msg: "Set PROJECT to your .xcodeproj path (e.g., PROJECT=build/ios/xcode/App.xcodeproj)."
362+ - sh: test -n '{{.SCHEME}}'
363+ msg: "Set SCHEME to your app scheme (e.g., SCHEME=ios.dev)."
364+ - sh: test -n '{{.UDID}}'
365+ msg: "Set UDID to your device UDID (see: task ios:device:list)."
366+ - sh: test -n '{{.BUNDLE_ID}}'
367+ msg: "Set BUNDLE_ID to your app's bundle identifier (e.g., com.yourco.wails.ios.dev)."
368+ cmds:
369+ - |
370+ set -euo pipefail
371+ echo "Building for device: UDID={{.UDID}} SCHEME={{.SCHEME}} PROJECT={{.PROJECT}}"
372+ XCB_ARGS=(
373+ -project "{{.PROJECT}}"
374+ -scheme "{{.SCHEME}}"
375+ -configuration "{{.CONFIG}}"
376+ -destination "id={{.UDID}}"
377+ -derivedDataPath "{{.DERIVED}}"
378+ -allowProvisioningUpdates
379+ -allowProvisioningDeviceRegistration
380+ )
381+ # Optionally inject signing identifiers if provided
382+ if [ -n '{{.TEAM_ID}}' ]; then XCB_ARGS+=(DEVELOPMENT_TEAM={{.TEAM_ID}}); fi
383+ if [ -n '{{.BUNDLE_ID}}' ]; then XCB_ARGS+=(PRODUCT_BUNDLE_IDENTIFIER={{.BUNDLE_ID}}); fi
384+ xcodebuild "${XCB_ARGS[@]}" build | xcpretty || true
385+ # If xcpretty isn't installed, run without it
386+ if [ "${PIPESTATUS[0]}" -ne 0 ]; then
387+ xcodebuild "${XCB_ARGS[@]}" build
388+ fi
389+ # Find built .app
390+ APP_PATH=$(find "{{.DERIVED}}/Build/Products" -type d -name "*.app" -maxdepth 3 | head -n 1)
391+ if [ -z "$APP_PATH" ]; then
392+ echo "Could not locate built .app under {{.DERIVED}}/Build/Products" >&2
393+ exit 1
394+ fi
395+ echo "Installing: $APP_PATH"
396+ xcrun devicectl device install app --device "{{.UDID}}" "$APP_PATH"
397+ echo "Launching: {{.BUNDLE_ID}}"
398+ xcrun devicectl device process launch --device "{{.UDID}}" --stderr console --stdout console "{{.BUNDLE_ID}}"
added build/appicon.icon/Assets/wails_icon_vector.svg +9 -0
new file mode 100644
@@ -0,0 +1,9 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3+<svg width="100%" height="100%" viewBox="0 0 583 533" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
4+ <g transform="matrix(1,0,0,1,-246,-251)">
5+ <g id="Ebene1">
6+ <path d="M246,251L265,784L401,784L506,450L507,450L505,784L641,784L829,251L682,251L596,567L595,567L596,251L478,251L378,568L391,251L246,251Z"/>
7+ </g>
8+ </g>
9+</svg>
new file mode 100644
@@ -0,0 +1,9 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3+<svg width="100%" height="100%" viewBox="0 0 583 533" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
4+ <g transform="matrix(1,0,0,1,-246,-251)">
5+ <g id="Ebene1">
6+ <path d="M246,251L265,784L401,784L506,450L507,450L505,784L641,784L829,251L682,251L596,567L595,567L596,251L478,251L378,568L391,251L246,251Z"/>
7+ </g>
8+ </g>
9+</svg>
added build/appicon.icon/icon.json +51 -0
new file mode 100644
@@ -0,0 +1,51 @@
1+{
2+ "fill" : {
3+ "automatic-gradient" : "extended-gray:1.00000,1.00000"
4+ },
5+ "groups" : [
6+ {
7+ "layers" : [
8+ {
9+ "fill-specializations" : [
10+ {
11+ "appearance" : "dark",
12+ "value" : {
13+ "solid" : "srgb:0.92143,0.92145,0.92144,1.00000"
14+ }
15+ },
16+ {
17+ "appearance" : "tinted",
18+ "value" : {
19+ "solid" : "srgb:0.83742,0.83744,0.83743,1.00000"
20+ }
21+ }
22+ ],
23+ "image-name" : "wails_icon_vector.svg",
24+ "name" : "wails_icon_vector",
25+ "position" : {
26+ "scale" : 0.85,
27+ "translation-in-points" : [
28+ 0.0,
29+ 0.0
30+ ]
31+ }
32+ }
33+ ],
34+ "shadow" : {
35+ "kind" : "neutral",
36+ "opacity" : 0.5
37+ },
38+ "specular" : true,
39+ "translucency" : {
40+ "enabled" : true,
41+ "value" : 0.5
42+ }
43+ }
44+ ],
45+ "supported-platforms" : {
46+ "circles" : [
47+ "watchOS"
48+ ],
49+ "squares" : "shared"
50+ }
51+}
new file mode 100644
@@ -0,0 +1,51 @@
1+{
2+ "fill" : {
3+ "automatic-gradient" : "extended-gray:1.00000,1.00000"
4+ },
5+ "groups" : [
6+ {
7+ "layers" : [
8+ {
9+ "fill-specializations" : [
10+ {
11+ "appearance" : "dark",
12+ "value" : {
13+ "solid" : "srgb:0.92143,0.92145,0.92144,1.00000"
14+ }
15+ },
16+ {
17+ "appearance" : "tinted",
18+ "value" : {
19+ "solid" : "srgb:0.83742,0.83744,0.83743,1.00000"
20+ }
21+ }
22+ ],
23+ "image-name" : "wails_icon_vector.svg",
24+ "name" : "wails_icon_vector",
25+ "position" : {
26+ "scale" : 0.85,
27+ "translation-in-points" : [
28+ 0.0,
29+ 0.0
30+ ]
31+ }
32+ }
33+ ],
34+ "shadow" : {
35+ "kind" : "neutral",
36+ "opacity" : 0.5
37+ },
38+ "specular" : true,
39+ "translucency" : {
40+ "enabled" : true,
41+ "value" : 0.5
42+ }
43+ }
44+ ],
45+ "supported-platforms" : {
46+ "circles" : [
47+ "watchOS"
48+ ],
49+ "squares" : "shared"
50+ }
51+}
added build/appicon.png +0 -0
new file mode 100644
Binary files /dev/null and b/build/appicon.png differ
new file mode 100644
Binary files /dev/null and b/build/appicon.png differBinary files /dev/null and b/build/appicon.png differ
added build/config.yml +87 -0
new file mode 100644
@@ -0,0 +1,87 @@
1+# This file contains the configuration for this project.
2+# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets.
3+# Note that this will overwrite any changes you have made to the assets.
4+version: '3'
5+
6+# This information is used to generate the build assets.
7+info:
8+ companyName: "Philippe Charrière" # The name of the company
9+ productName: "Sidekicks" # The name of the application
10+ productIdentifier: "com.k33g.sidekicks" # The unique product identifier
11+ description: "Converse with AI agents in channels" # The application description
12+ copyright: "© 2026 Philippe Charrière — Sidekicks Source-Available License 1.0" # Copyright text
13+ comments: "Sidekicks" # Comments
14+ version: "0.0.1" # The application version
15+ # cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional)
16+ # # Should match the name of your .icon file without the extension
17+ # # If not set and Assets.car exists, defaults to "appicon"
18+
19+# iOS build configuration (uncomment to customise iOS project generation)
20+# Note: Keys under `ios` OVERRIDE values under `info` when set.
21+# ios:
22+# # The iOS bundle identifier used in the generated Xcode project (CFBundleIdentifier)
23+# bundleID: "com.mycompany.myproduct"
24+# # The display name shown under the app icon (CFBundleDisplayName/CFBundleName)
25+# displayName: "My Product"
26+# # The app version to embed in Info.plist (CFBundleShortVersionString/CFBundleVersion)
27+# version: "0.0.1"
28+# # The company/organisation name for templates and project settings
29+# company: "My Company"
30+# # Additional comments to embed in Info.plist metadata
31+# comments: "Some Product Comments"
32+
33+# Dev mode configuration
34+dev_mode:
35+ root_path: .
36+ log_level: warn
37+ debounce: 1000
38+ ignore:
39+ dir:
40+ - .git
41+ - node_modules
42+ - frontend
43+ - bin
44+ file:
45+ - .DS_Store
46+ - .gitignore
47+ - .gitkeep
48+ - "*_test.go"
49+ watched_extension:
50+ - "*.go"
51+ - "*.js" # Watch for changes to JS/TS files included using the //wails:include directive.
52+ - "*.ts" # The frontend directory will be excluded entirely by the setting above.
53+ git_ignore: true
54+ executes:
55+ - cmd: wails3 build DEV=true
56+ type: blocking
57+ - cmd: wails3 task common:dev:frontend
58+ type: background
59+ # The standard frontend task waits up to 60s for HTTP 200 from
60+ # FRONTEND_DEVSERVER_URL before starting the app. Override if needed;
61+ # custom background commands can opt in with the same readiness block.
62+ # readiness:
63+ # http: "http://localhost:5173/"
64+ # timeout: "60s"
65+ # interval: "200ms"
66+ - cmd: wails3 task run
67+ type: primary
68+ exit_policy: shutdown
69+
70+# File Associations
71+# More information at: https://v3.wails.io/noit/done/yet
72+fileAssociations:
73+# - ext: wails
74+# name: Wails
75+# description: Wails Application File
76+# iconName: wailsFileIcon
77+# role: Editor
78+# - ext: jpg
79+# name: JPEG
80+# description: Image File
81+# iconName: jpegFileIcon
82+# role: Editor
83+# mimeType: image/jpeg # (optional)
84+
85+# Other data
86+other:
87+ - name: My Other Data
new file mode 100644
@@ -0,0 +1,87 @@
1+# This file contains the configuration for this project.
2+# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets.
3+# Note that this will overwrite any changes you have made to the assets.
4+version: '3'
5+
6+# This information is used to generate the build assets.
7+info:
8+ companyName: "Philippe Charrière" # The name of the company
9+ productName: "Sidekicks" # The name of the application
10+ productIdentifier: "com.k33g.sidekicks" # The unique product identifier
11+ description: "Converse with AI agents in channels" # The application description
12+ copyright: "© 2026 Philippe Charrière — Sidekicks Source-Available License 1.0" # Copyright text
13+ comments: "Sidekicks" # Comments
14+ version: "0.0.1" # The application version
15+ # cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional)
16+ # # Should match the name of your .icon file without the extension
17+ # # If not set and Assets.car exists, defaults to "appicon"
18+
19+# iOS build configuration (uncomment to customise iOS project generation)
20+# Note: Keys under `ios` OVERRIDE values under `info` when set.
21+# ios:
22+# # The iOS bundle identifier used in the generated Xcode project (CFBundleIdentifier)
23+# bundleID: "com.mycompany.myproduct"
24+# # The display name shown under the app icon (CFBundleDisplayName/CFBundleName)
25+# displayName: "My Product"
26+# # The app version to embed in Info.plist (CFBundleShortVersionString/CFBundleVersion)
27+# version: "0.0.1"
28+# # The company/organisation name for templates and project settings
29+# company: "My Company"
30+# # Additional comments to embed in Info.plist metadata
31+# comments: "Some Product Comments"
32+
33+# Dev mode configuration
34+dev_mode:
35+ root_path: .
36+ log_level: warn
37+ debounce: 1000
38+ ignore:
39+ dir:
40+ - .git
41+ - node_modules
42+ - frontend
43+ - bin
44+ file:
45+ - .DS_Store
46+ - .gitignore
47+ - .gitkeep
48+ - "*_test.go"
49+ watched_extension:
50+ - "*.go"
51+ - "*.js" # Watch for changes to JS/TS files included using the //wails:include directive.
52+ - "*.ts" # The frontend directory will be excluded entirely by the setting above.
53+ git_ignore: true
54+ executes:
55+ - cmd: wails3 build DEV=true
56+ type: blocking
57+ - cmd: wails3 task common:dev:frontend
58+ type: background
59+ # The standard frontend task waits up to 60s for HTTP 200 from
60+ # FRONTEND_DEVSERVER_URL before starting the app. Override if needed;
61+ # custom background commands can opt in with the same readiness block.
62+ # readiness:
63+ # http: "http://localhost:5173/"
64+ # timeout: "60s"
65+ # interval: "200ms"
66+ - cmd: wails3 task run
67+ type: primary
68+ exit_policy: shutdown
69+
70+# File Associations
71+# More information at: https://v3.wails.io/noit/done/yet
72+fileAssociations:
73+# - ext: wails
74+# name: Wails
75+# description: Wails Application File
76+# iconName: wailsFileIcon
77+# role: Editor
78+# - ext: jpg
79+# name: JPEG
80+# description: Image File
81+# iconName: jpegFileIcon
82+# role: Editor
83+# mimeType: image/jpeg # (optional)
84+
85+# Other data
86+other:
87+ - name: My Other Data
added build/darwin/Assets.car +0 -0
new file mode 100644
Binary files /dev/null and b/build/darwin/Assets.car differ
new file mode 100644
Binary files /dev/null and b/build/darwin/Assets.car differBinary files /dev/null and b/build/darwin/Assets.car differ
added build/darwin/Info.dev.plist +34 -0
new file mode 100644
@@ -0,0 +1,34 @@
1+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2+<plist version="1.0">
3+ <dict>
4+ <key>CFBundlePackageType</key>
5+ <string>APPL</string>
6+ <key>CFBundleName</key>
7+ <string>Sidekicks</string>
8+ <key>CFBundleExecutable</key>
9+ <string>sidekicks</string>
10+ <key>CFBundleIdentifier</key>
11+ <string>com.k33g.sidekicks</string>
12+ <key>CFBundleVersion</key>
13+ <string>0.0.1</string>
14+ <key>CFBundleGetInfoString</key>
15+ <string>This is a comment</string>
16+ <key>CFBundleShortVersionString</key>
17+ <string>0.0.1</string>
18+ <key>CFBundleIconFile</key>
19+ <string>icons</string>
20+ <key>CFBundleIconName</key>
21+ <string>appicon</string>
22+ <key>LSMinimumSystemVersion</key>
23+ <string>12.0.0</string>
24+ <key>NSHighResolutionCapable</key>
25+ <string>true</string>
26+ <key>NSHumanReadableCopyright</key>
27+ <string>© 2026 Philippe Charrière</string>
28+ <key>NSAppTransportSecurity</key>
29+ <dict>
30+ <key>NSAllowsLocalNetworking</key>
31+ <true/>
32+ </dict>
33+ </dict>
34+</plist>
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,34 @@
1+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2+<plist version="1.0">
3+ <dict>
4+ <key>CFBundlePackageType</key>
5+ <string>APPL</string>
6+ <key>CFBundleName</key>
7+ <string>Sidekicks</string>
8+ <key>CFBundleExecutable</key>
9+ <string>sidekicks</string>
10+ <key>CFBundleIdentifier</key>
11+ <string>com.k33g.sidekicks</string>
12+ <key>CFBundleVersion</key>
13+ <string>0.0.1</string>
14+ <key>CFBundleGetInfoString</key>
15+ <string>This is a comment</string>
16+ <key>CFBundleShortVersionString</key>
17+ <string>0.0.1</string>
18+ <key>CFBundleIconFile</key>
19+ <string>icons</string>
20+ <key>CFBundleIconName</key>
21+ <string>appicon</string>
22+ <key>LSMinimumSystemVersion</key>
23+ <string>12.0.0</string>
24+ <key>NSHighResolutionCapable</key>
25+ <string>true</string>
26+ <key>NSHumanReadableCopyright</key>
27+ <string>© 2026 Philippe Charrière</string>
28+ <key>NSAppTransportSecurity</key>
29+ <dict>
30+ <key>NSAllowsLocalNetworking</key>
31+ <true/>
32+ </dict>
33+ </dict>
34+</plist>
\ No newline at end of file\ No newline at end of file
added build/darwin/Info.plist +29 -0
new file mode 100644
@@ -0,0 +1,29 @@
1+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2+<plist version="1.0">
3+ <dict>
4+ <key>CFBundlePackageType</key>
5+ <string>APPL</string>
6+ <key>CFBundleName</key>
7+ <string>Sidekicks</string>
8+ <key>CFBundleExecutable</key>
9+ <string>sidekicks</string>
10+ <key>CFBundleIdentifier</key>
11+ <string>com.k33g.sidekicks</string>
12+ <key>CFBundleVersion</key>
13+ <string>0.0.1</string>
14+ <key>CFBundleGetInfoString</key>
15+ <string>This is a comment</string>
16+ <key>CFBundleShortVersionString</key>
17+ <string>0.0.1</string>
18+ <key>CFBundleIconFile</key>
19+ <string>icons</string>
20+ <key>CFBundleIconName</key>
21+ <string>appicon</string>
22+ <key>LSMinimumSystemVersion</key>
23+ <string>12.0.0</string>
24+ <key>NSHighResolutionCapable</key>
25+ <string>true</string>
26+ <key>NSHumanReadableCopyright</key>
27+ <string>© 2026 Philippe Charrière</string>
28+ </dict>
29+</plist>
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,29 @@
1+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2+<plist version="1.0">
3+ <dict>
4+ <key>CFBundlePackageType</key>
5+ <string>APPL</string>
6+ <key>CFBundleName</key>
7+ <string>Sidekicks</string>
8+ <key>CFBundleExecutable</key>
9+ <string>sidekicks</string>
10+ <key>CFBundleIdentifier</key>
11+ <string>com.k33g.sidekicks</string>
12+ <key>CFBundleVersion</key>
13+ <string>0.0.1</string>
14+ <key>CFBundleGetInfoString</key>
15+ <string>This is a comment</string>
16+ <key>CFBundleShortVersionString</key>
17+ <string>0.0.1</string>
18+ <key>CFBundleIconFile</key>
19+ <string>icons</string>
20+ <key>CFBundleIconName</key>
21+ <string>appicon</string>
22+ <key>LSMinimumSystemVersion</key>
23+ <string>12.0.0</string>
24+ <key>NSHighResolutionCapable</key>
25+ <string>true</string>
26+ <key>NSHumanReadableCopyright</key>
27+ <string>© 2026 Philippe Charrière</string>
28+ </dict>
29+</plist>
\ No newline at end of file\ No newline at end of file
added build/darwin/Taskfile.yml +230 -0
new file mode 100644
@@ -0,0 +1,230 @@
1+version: '3'
2+
3+includes:
4+ common: ../Taskfile.yml
5+
6+vars:
7+ # Docker image for cross-compilation (used when building on non-macOS)
8+ CROSS_IMAGE: wails-cross
9+
10+tasks:
11+ build:
12+ summary: Builds the application
13+ cmds:
14+ - task: '{{if eq OS "darwin"}}build:native{{else}}build:docker{{end}}'
15+ vars:
16+ ARCH: '{{.ARCH}}'
17+ DEV: '{{.DEV}}'
18+ OUTPUT: '{{.OUTPUT}}'
19+ EXTRA_TAGS: '{{.EXTRA_TAGS}}'
20+ OBFUSCATED: '{{.OBFUSCATED}}'
21+ GARBLE_ARGS: '{{.GARBLE_ARGS}}'
22+ vars:
23+ DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
24+ OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
25+
26+ build:native:
27+ summary: Builds the application natively on macOS
28+ internal: true
29+ deps:
30+ - task: common:go:mod:tidy
31+ - task: common:build:frontend
32+ vars:
33+ BUILD_FLAGS:
34+ ref: .BUILD_FLAGS
35+ OBFUSCATED:
36+ ref: .OBFUSCATED
37+ DEV:
38+ ref: .DEV
39+ - task: common:generate:icons
40+ preconditions:
41+ - sh: '{{if eq .OBFUSCATED "true"}}command -v garble >/dev/null 2>&1{{else}}true{{end}}'
42+ msg: "garble is required for obfuscated builds. Install it with: go install mvdan.cc/garble@v0.16.0 (requires Go 1.24+). See https://github.com/burrowers/garble/releases for version/toolchain compatibility."
43+ cmds:
44+ - wails3 tool prepare-macos-output --dir "{{.BIN_DIR}}"
45+ - '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o "{{.OUTPUT}}"'
46+ vars:
47+ BUILD_FLAGS: '{{if eq .DEV "true"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
48+ DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
49+ OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
50+ env:
51+ GOOS: darwin
52+ CGO_ENABLED: 1
53+ GOARCH: '{{.ARCH | default ARCH}}'
54+ CGO_CFLAGS: "-mmacosx-version-min=12.0"
55+ CGO_LDFLAGS: "-mmacosx-version-min=12.0"
56+ MACOSX_DEPLOYMENT_TARGET: "12.0"
57+
58+ build:docker:
59+ summary: Cross-compiles for macOS using Docker (for Linux/Windows hosts)
60+ internal: true
61+ deps:
62+ - task: common:build:frontend
63+ vars:
64+ OBFUSCATED:
65+ ref: .OBFUSCATED
66+ - task: common:generate:icons
67+ preconditions:
68+ - sh: docker info > /dev/null 2>&1
69+ msg: "Docker is required for cross-compilation. Please install Docker."
70+ - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
71+ msg: |
72+ Docker image '{{.CROSS_IMAGE}}' not found.
73+ Build it first: wails3 task setup:docker
74+ cmds:
75+ - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.DOCKER_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{if eq .OBFUSCATED "true"}}-e OBFUSCATED=true{{end}} {{if .GARBLE_ARGS}}-e GARBLE_ARGS="{{.GARBLE_ARGS}}"{{end}} {{.CROSS_IMAGE}} darwin {{.DOCKER_ARCH}}
76+ - cmd: docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
77+ platforms: [linux, darwin]
78+ - mkdir -p {{.BIN_DIR}}
79+ - mv "bin/{{.APP_NAME}}-darwin-{{.DOCKER_ARCH}}" "{{.OUTPUT}}"
80+ vars:
81+ DOCKER_ARCH: '{{if eq .ARCH "arm64"}}arm64{{else if eq .ARCH "amd64"}}amd64{{else}}arm64{{end}}'
82+ DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
83+ OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
84+ # Generate Docker volume mounts: Go module cache + go.mod replace directives
85+ # Uses wails3 tool docker-mounts for cross-platform compatibility (Windows/Linux/macOS)
86+ DOCKER_MOUNTS:
87+ sh: 'wails3 tool docker-mounts'
88+
89+ build:universal:
90+ summary: Builds darwin universal binary (arm64 + amd64)
91+ deps:
92+ - task: build
93+ vars:
94+ ARCH: amd64
95+ OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64"
96+ - task: build
97+ vars:
98+ ARCH: arm64
99+ OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
100+ cmds:
101+ - task: '{{if eq OS "darwin"}}build:universal:lipo:native{{else}}build:universal:lipo:go{{end}}'
102+
103+ build:universal:lipo:native:
104+ summary: Creates universal binary using native lipo (macOS)
105+ internal: true
106+ cmds:
107+ - lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
108+ - rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
109+
110+ build:universal:lipo:go:
111+ summary: Creates universal binary using wails3 tool lipo (Linux/Windows)
112+ internal: true
113+ cmds:
114+ - wails3 tool lipo -output "{{.BIN_DIR}}/{{.APP_NAME}}" -input "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" -input "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
115+ - rm -f "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
116+
117+ package:
118+ summary: Packages the application into a `.app` bundle
119+ deps:
120+ - task: build
121+ cmds:
122+ - task: create:app:bundle
123+
124+ package:universal:
125+ summary: Packages darwin universal binary (arm64 + amd64)
126+ deps:
127+ - task: build:universal
128+ cmds:
129+ - task: create:app:bundle
130+
131+ package:dmg:
132+ summary: Packages the application into a styled `.dmg`
133+ deps:
134+ - task: package
135+ cmds:
136+ - task: create:dmg
137+
138+ create:dmg:
139+ summary: Creates a configurable `.dmg` from the `.app` bundle
140+ platforms: [darwin]
141+ cmds:
142+ - >-
143+ wails3 tool package --format dmg --name "{{.APP_NAME}}" --out "{{.BIN_DIR}}"
144+ --background "{{.DMG_BACKGROUND}}"
145+ --volume-icon "{{.DMG_VOLUME_ICON}}"
146+ --file-icon "{{.DMG_FILE_ICON}}"
147+ --window-width "{{.DMG_WINDOW_WIDTH}}"
148+ --window-height "{{.DMG_WINDOW_HEIGHT}}"
149+ {{if .DMG_FILES}}--files "{{.DMG_FILES}}"{{end}}
150+ vars:
151+ DMG_BACKGROUND: '{{.DMG_BACKGROUND | default "build/darwin/dmg-background.png"}}'
152+ DMG_VOLUME_ICON: '{{.DMG_VOLUME_ICON | default "build/darwin/icons.icns"}}'
153+ DMG_FILE_ICON: '{{.DMG_FILE_ICON | default "build/darwin/dmg-file-icon.icns"}}'
154+ DMG_WINDOW_WIDTH: '{{.DMG_WINDOW_WIDTH | default "540"}}'
155+ DMG_WINDOW_HEIGHT: '{{.DMG_WINDOW_HEIGHT | default "380"}}'
156+ DMG_FILES: '{{.DMG_FILES | default ""}}'
157+
158+
159+ create:app:bundle:
160+ summary: Creates an `.app` bundle
161+ cmds:
162+ # A reused bundle can acquire Finder metadata in synced folders. Build a
163+ # fresh bundle so codesign never has to replace a contaminated signature.
164+ - test -n "{{.BIN_DIR}}" && test -n "{{.APP_NAME}}" && rm -rf -- "{{.BIN_DIR}}/{{.APP_NAME}}.app"
165+ - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS"
166+ - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
167+ - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
168+ - |
169+ if [ -f build/darwin/Assets.car ]; then
170+ cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
171+ fi
172+ - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS"
173+ - cp build/darwin/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents"
174+ - task: '{{if eq OS "darwin"}}codesign:adhoc{{else}}codesign:skip{{end}}'
175+
176+ codesign:adhoc:
177+ summary: Ad-hoc signs the app bundle (macOS only)
178+ internal: true
179+ cmds:
180+ - xattr -cr "{{.BIN_DIR}}/{{.APP_NAME}}.app"
181+ - codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app"
182+ - codesign --verify --deep --strict "{{.BIN_DIR}}/{{.APP_NAME}}.app"
183+
184+ codesign:skip:
185+ summary: Skips codesigning when cross-compiling
186+ internal: true
187+ cmds:
188+ - 'echo "Skipping codesign (not available on {{OS}}). Sign the .app on macOS before distribution."'
189+
190+ run:
191+ cmds:
192+ - wails3 tool prepare-macos-output --dir "{{.BIN_DIR}}"
193+ - test -n "{{.BIN_DIR}}" && test -n "{{.APP_NAME}}" && rm -rf -- "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
194+ - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS"
195+ - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
196+ - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
197+ - |
198+ if [ -f build/darwin/Assets.car ]; then
199+ cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
200+ fi
201+ - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS"
202+ - cp "build/darwin/Info.dev.plist" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist"
203+ - xattr -cr "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
204+ - codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
205+ - codesign --verify --deep --strict "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
206+ - '"{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}"'
207+
208+ sign:
209+ summary: Signs the application bundle with Developer ID
210+ desc: |
211+ Signs the .app bundle for distribution.
212+ Uses signing identity from `wails3 setup` (stored in ~/.config/wails/defaults.yaml).
213+ Override with: task darwin:sign -- --identity "Developer ID Application: ..."
214+ deps:
215+ - task: package
216+ cmds:
217+ - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" {{.CLI_ARGS}}
218+
219+ sign:notarize:
220+ summary: Signs and notarizes the application bundle
221+ desc: |
222+ Signs the .app bundle and submits it for notarization.
223+ Uses signing identity and keychain profile from `wails3 setup`.
224+
225+ First-time setup:
226+ wails3 setup
227+ deps:
228+ - task: package
229+ cmds:
230+ - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --notarize {{.CLI_ARGS}}
new file mode 100644
@@ -0,0 +1,230 @@
1+version: '3'
2+
3+includes:
4+ common: ../Taskfile.yml
5+
6+vars:
7+ # Docker image for cross-compilation (used when building on non-macOS)
8+ CROSS_IMAGE: wails-cross
9+
10+tasks:
11+ build:
12+ summary: Builds the application
13+ cmds:
14+ - task: '{{if eq OS "darwin"}}build:native{{else}}build:docker{{end}}'
15+ vars:
16+ ARCH: '{{.ARCH}}'
17+ DEV: '{{.DEV}}'
18+ OUTPUT: '{{.OUTPUT}}'
19+ EXTRA_TAGS: '{{.EXTRA_TAGS}}'
20+ OBFUSCATED: '{{.OBFUSCATED}}'
21+ GARBLE_ARGS: '{{.GARBLE_ARGS}}'
22+ vars:
23+ DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
24+ OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
25+
26+ build:native:
27+ summary: Builds the application natively on macOS
28+ internal: true
29+ deps:
30+ - task: common:go:mod:tidy
31+ - task: common:build:frontend
32+ vars:
33+ BUILD_FLAGS:
34+ ref: .BUILD_FLAGS
35+ OBFUSCATED:
36+ ref: .OBFUSCATED
37+ DEV:
38+ ref: .DEV
39+ - task: common:generate:icons
40+ preconditions:
41+ - sh: '{{if eq .OBFUSCATED "true"}}command -v garble >/dev/null 2>&1{{else}}true{{end}}'
42+ msg: "garble is required for obfuscated builds. Install it with: go install mvdan.cc/garble@v0.16.0 (requires Go 1.24+). See https://github.com/burrowers/garble/releases for version/toolchain compatibility."
43+ cmds:
44+ - wails3 tool prepare-macos-output --dir "{{.BIN_DIR}}"
45+ - '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o "{{.OUTPUT}}"'
46+ vars:
47+ BUILD_FLAGS: '{{if eq .DEV "true"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
48+ DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
49+ OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
50+ env:
51+ GOOS: darwin
52+ CGO_ENABLED: 1
53+ GOARCH: '{{.ARCH | default ARCH}}'
54+ CGO_CFLAGS: "-mmacosx-version-min=12.0"
55+ CGO_LDFLAGS: "-mmacosx-version-min=12.0"
56+ MACOSX_DEPLOYMENT_TARGET: "12.0"
57+
58+ build:docker:
59+ summary: Cross-compiles for macOS using Docker (for Linux/Windows hosts)
60+ internal: true
61+ deps:
62+ - task: common:build:frontend
63+ vars:
64+ OBFUSCATED:
65+ ref: .OBFUSCATED
66+ - task: common:generate:icons
67+ preconditions:
68+ - sh: docker info > /dev/null 2>&1
69+ msg: "Docker is required for cross-compilation. Please install Docker."
70+ - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
71+ msg: |
72+ Docker image '{{.CROSS_IMAGE}}' not found.
73+ Build it first: wails3 task setup:docker
74+ cmds:
75+ - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.DOCKER_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{if eq .OBFUSCATED "true"}}-e OBFUSCATED=true{{end}} {{if .GARBLE_ARGS}}-e GARBLE_ARGS="{{.GARBLE_ARGS}}"{{end}} {{.CROSS_IMAGE}} darwin {{.DOCKER_ARCH}}
76+ - cmd: docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
77+ platforms: [linux, darwin]
78+ - mkdir -p {{.BIN_DIR}}
79+ - mv "bin/{{.APP_NAME}}-darwin-{{.DOCKER_ARCH}}" "{{.OUTPUT}}"
80+ vars:
81+ DOCKER_ARCH: '{{if eq .ARCH "arm64"}}arm64{{else if eq .ARCH "amd64"}}amd64{{else}}arm64{{end}}'
82+ DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
83+ OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
84+ # Generate Docker volume mounts: Go module cache + go.mod replace directives
85+ # Uses wails3 tool docker-mounts for cross-platform compatibility (Windows/Linux/macOS)
86+ DOCKER_MOUNTS:
87+ sh: 'wails3 tool docker-mounts'
88+
89+ build:universal:
90+ summary: Builds darwin universal binary (arm64 + amd64)
91+ deps:
92+ - task: build
93+ vars:
94+ ARCH: amd64
95+ OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64"
96+ - task: build
97+ vars:
98+ ARCH: arm64
99+ OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
100+ cmds:
101+ - task: '{{if eq OS "darwin"}}build:universal:lipo:native{{else}}build:universal:lipo:go{{end}}'
102+
103+ build:universal:lipo:native:
104+ summary: Creates universal binary using native lipo (macOS)
105+ internal: true
106+ cmds:
107+ - lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
108+ - rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
109+
110+ build:universal:lipo:go:
111+ summary: Creates universal binary using wails3 tool lipo (Linux/Windows)
112+ internal: true
113+ cmds:
114+ - wails3 tool lipo -output "{{.BIN_DIR}}/{{.APP_NAME}}" -input "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" -input "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
115+ - rm -f "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
116+
117+ package:
118+ summary: Packages the application into a `.app` bundle
119+ deps:
120+ - task: build
121+ cmds:
122+ - task: create:app:bundle
123+
124+ package:universal:
125+ summary: Packages darwin universal binary (arm64 + amd64)
126+ deps:
127+ - task: build:universal
128+ cmds:
129+ - task: create:app:bundle
130+
131+ package:dmg:
132+ summary: Packages the application into a styled `.dmg`
133+ deps:
134+ - task: package
135+ cmds:
136+ - task: create:dmg
137+
138+ create:dmg:
139+ summary: Creates a configurable `.dmg` from the `.app` bundle
140+ platforms: [darwin]
141+ cmds:
142+ - >-
143+ wails3 tool package --format dmg --name "{{.APP_NAME}}" --out "{{.BIN_DIR}}"
144+ --background "{{.DMG_BACKGROUND}}"
145+ --volume-icon "{{.DMG_VOLUME_ICON}}"
146+ --file-icon "{{.DMG_FILE_ICON}}"
147+ --window-width "{{.DMG_WINDOW_WIDTH}}"
148+ --window-height "{{.DMG_WINDOW_HEIGHT}}"
149+ {{if .DMG_FILES}}--files "{{.DMG_FILES}}"{{end}}
150+ vars:
151+ DMG_BACKGROUND: '{{.DMG_BACKGROUND | default "build/darwin/dmg-background.png"}}'
152+ DMG_VOLUME_ICON: '{{.DMG_VOLUME_ICON | default "build/darwin/icons.icns"}}'
153+ DMG_FILE_ICON: '{{.DMG_FILE_ICON | default "build/darwin/dmg-file-icon.icns"}}'
154+ DMG_WINDOW_WIDTH: '{{.DMG_WINDOW_WIDTH | default "540"}}'
155+ DMG_WINDOW_HEIGHT: '{{.DMG_WINDOW_HEIGHT | default "380"}}'
156+ DMG_FILES: '{{.DMG_FILES | default ""}}'
157+
158+
159+ create:app:bundle:
160+ summary: Creates an `.app` bundle
161+ cmds:
162+ # A reused bundle can acquire Finder metadata in synced folders. Build a
163+ # fresh bundle so codesign never has to replace a contaminated signature.
164+ - test -n "{{.BIN_DIR}}" && test -n "{{.APP_NAME}}" && rm -rf -- "{{.BIN_DIR}}/{{.APP_NAME}}.app"
165+ - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS"
166+ - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
167+ - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
168+ - |
169+ if [ -f build/darwin/Assets.car ]; then
170+ cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
171+ fi
172+ - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS"
173+ - cp build/darwin/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents"
174+ - task: '{{if eq OS "darwin"}}codesign:adhoc{{else}}codesign:skip{{end}}'
175+
176+ codesign:adhoc:
177+ summary: Ad-hoc signs the app bundle (macOS only)
178+ internal: true
179+ cmds:
180+ - xattr -cr "{{.BIN_DIR}}/{{.APP_NAME}}.app"
181+ - codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app"
182+ - codesign --verify --deep --strict "{{.BIN_DIR}}/{{.APP_NAME}}.app"
183+
184+ codesign:skip:
185+ summary: Skips codesigning when cross-compiling
186+ internal: true
187+ cmds:
188+ - 'echo "Skipping codesign (not available on {{OS}}). Sign the .app on macOS before distribution."'
189+
190+ run:
191+ cmds:
192+ - wails3 tool prepare-macos-output --dir "{{.BIN_DIR}}"
193+ - test -n "{{.BIN_DIR}}" && test -n "{{.APP_NAME}}" && rm -rf -- "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
194+ - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS"
195+ - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
196+ - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
197+ - |
198+ if [ -f build/darwin/Assets.car ]; then
199+ cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
200+ fi
201+ - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS"
202+ - cp "build/darwin/Info.dev.plist" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist"
203+ - xattr -cr "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
204+ - codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
205+ - codesign --verify --deep --strict "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
206+ - '"{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}"'
207+
208+ sign:
209+ summary: Signs the application bundle with Developer ID
210+ desc: |
211+ Signs the .app bundle for distribution.
212+ Uses signing identity from `wails3 setup` (stored in ~/.config/wails/defaults.yaml).
213+ Override with: task darwin:sign -- --identity "Developer ID Application: ..."
214+ deps:
215+ - task: package
216+ cmds:
217+ - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" {{.CLI_ARGS}}
218+
219+ sign:notarize:
220+ summary: Signs and notarizes the application bundle
221+ desc: |
222+ Signs the .app bundle and submits it for notarization.
223+ Uses signing identity and keychain profile from `wails3 setup`.
224+
225+ First-time setup:
226+ wails3 setup
227+ deps:
228+ - task: package
229+ cmds:
230+ - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --notarize {{.CLI_ARGS}}
added build/darwin/dmg-background.png +0 -0
new file mode 100644
Binary files /dev/null and b/build/darwin/dmg-background.png differ
new file mode 100644
Binary files /dev/null and b/build/darwin/dmg-background.png differBinary files /dev/null and b/build/darwin/dmg-background.png differ
added build/darwin/dmg-file-icon.icns +0 -0
new file mode 100644
Binary files /dev/null and b/build/darwin/dmg-file-icon.icns differ
new file mode 100644
Binary files /dev/null and b/build/darwin/dmg-file-icon.icns differBinary files /dev/null and b/build/darwin/dmg-file-icon.icns differ
added build/darwin/dmg-file-icon.png +0 -0
new file mode 100644
Binary files /dev/null and b/build/darwin/dmg-file-icon.png differ
new file mode 100644
Binary files /dev/null and b/build/darwin/dmg-file-icon.png differBinary files /dev/null and b/build/darwin/dmg-file-icon.png differ
added build/darwin/icons.icns +0 -0
new file mode 100644
Binary files /dev/null and b/build/darwin/icons.icns differ
new file mode 100644
Binary files /dev/null and b/build/darwin/icons.icns differBinary files /dev/null and b/build/darwin/icons.icns differ
added build/docker/Dockerfile.cross +225 -0
new file mode 100644
@@ -0,0 +1,225 @@
1+# Cross-compile Wails v3 apps to any platform
2+#
3+# Darwin: Zig + macOS SDK
4+# Linux: Native GCC when host matches target, Zig for cross-arch
5+# Windows: Zig + bundled mingw
6+#
7+# Usage:
8+# docker build -t wails-cross -f Dockerfile.cross .
9+# docker run --rm -v $(pwd):/app wails-cross darwin arm64
10+# docker run --rm -v $(pwd):/app wails-cross darwin amd64
11+# docker run --rm -v $(pwd):/app wails-cross linux amd64
12+# docker run --rm -v $(pwd):/app wails-cross linux arm64
13+# docker run --rm -v $(pwd):/app wails-cross windows amd64
14+# docker run --rm -v $(pwd):/app wails-cross windows arm64
15+
16+# Debian 13 provides the GTK >= 4.14 API baseline used by the default Linux backend.
17+FROM golang:1.26-trixie
18+
19+ARG TARGETARCH
20+ARG GARBLE_VERSION=v0.16.0
21+
22+# Install base tools, GCC, and GTK/WebKit dev packages
23+RUN apt-get update && apt-get install -y --no-install-recommends \
24+ curl xz-utils python3 nodejs npm pkg-config gcc libc6-dev \
25+ libgtk-3-dev libwebkit2gtk-4.1-dev \
26+ libgtk-4-dev libwebkitgtk-6.0-dev \
27+ && pkg-config --atleast-version=4.14 gtk4 \
28+ && pkg-config --exists webkitgtk-6.0 \
29+ && pkg-config --exists gtk+-3.0 \
30+ && pkg-config --exists webkit2gtk-4.1 \
31+ && rm -rf /var/lib/apt/lists/*
32+
33+RUN go install mvdan.cc/garble@${GARBLE_VERSION}
34+
35+# Install Zig - automatically selects correct binary for host architecture.
36+# The SHA-256 is fetched from Zig's official download manifest and verified
37+# before extraction to protect against MITM or corrupted downloads.
38+ARG ZIG_VERSION=0.14.0
39+RUN ZIG_ARCH=$(case "${TARGETARCH}" in arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
40+ ZIG_FILE="zig-linux-${ZIG_ARCH}-${ZIG_VERSION}.tar.xz" && \
41+ curl -fSL "https://ziglang.org/download/${ZIG_VERSION}/${ZIG_FILE}" -o "/tmp/${ZIG_FILE}" && \
42+ EXPECTED=$(curl -fsSL "https://ziglang.org/download/index.json" | \
43+ python3 -c "import json,sys; d=json.load(sys.stdin); print(d[sys.argv[1]][sys.argv[2]+'-linux']['shasum'])" \
44+ "${ZIG_VERSION}" "${ZIG_ARCH}") && \
45+ echo "${EXPECTED} /tmp/${ZIG_FILE}" | sha256sum -c - && \
46+ tar -xJ -f "/tmp/${ZIG_FILE}" -C /opt && \
47+ rm "/tmp/${ZIG_FILE}" && \
48+ ln -s /opt/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}/zig /usr/local/bin/zig
49+
50+# Download macOS SDK (required for darwin targets)
51+ARG MACOS_SDK_VERSION=14.5
52+RUN curl -L "https://github.com/joseluisq/macosx-sdks/releases/download/${MACOS_SDK_VERSION}/MacOSX${MACOS_SDK_VERSION}.sdk.tar.xz" \
53+ | tar -xJ -C /opt \
54+ && mv /opt/MacOSX${MACOS_SDK_VERSION}.sdk /opt/macos-sdk
55+
56+ENV MACOS_SDK_PATH=/opt/macos-sdk
57+
58+# Create Zig CC wrappers for cross-compilation targets
59+# Darwin and Windows use Zig; Linux uses native GCC (run with --platform for cross-arch)
60+
61+# Darwin arm64
62+COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-arm64
63+#!/bin/sh
64+ARGS=""
65+SKIP_NEXT=0
66+for arg in "$@"; do
67+ if [ $SKIP_NEXT -eq 1 ]; then
68+ SKIP_NEXT=0
69+ continue
70+ fi
71+ case "$arg" in
72+ -target) SKIP_NEXT=1 ;;
73+ -mmacosx-version-min=*) ;;
74+ *) ARGS="$ARGS $arg" ;;
75+ esac
76+done
77+exec zig cc -fno-sanitize=all -target aarch64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
78+ZIGWRAP
79+RUN chmod +x /usr/local/bin/zcc-darwin-arm64
80+
81+# Darwin amd64
82+COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-amd64
83+#!/bin/sh
84+ARGS=""
85+SKIP_NEXT=0
86+for arg in "$@"; do
87+ if [ $SKIP_NEXT -eq 1 ]; then
88+ SKIP_NEXT=0
89+ continue
90+ fi
91+ case "$arg" in
92+ -target) SKIP_NEXT=1 ;;
93+ -mmacosx-version-min=*) ;;
94+ *) ARGS="$ARGS $arg" ;;
95+ esac
96+done
97+exec zig cc -fno-sanitize=all -target x86_64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
98+ZIGWRAP
99+RUN chmod +x /usr/local/bin/zcc-darwin-amd64
100+
101+# Windows amd64 - uses Zig's bundled mingw
102+COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-amd64
103+#!/bin/sh
104+ARGS=""
105+SKIP_NEXT=0
106+for arg in "$@"; do
107+ if [ $SKIP_NEXT -eq 1 ]; then
108+ SKIP_NEXT=0
109+ continue
110+ fi
111+ case "$arg" in
112+ -target) SKIP_NEXT=1 ;;
113+ -Wl,*) ;;
114+ *) ARGS="$ARGS $arg" ;;
115+ esac
116+done
117+exec zig cc -target x86_64-windows-gnu $ARGS
118+ZIGWRAP
119+RUN chmod +x /usr/local/bin/zcc-windows-amd64
120+
121+# Windows arm64 - uses Zig's bundled mingw
122+COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-arm64
123+#!/bin/sh
124+ARGS=""
125+SKIP_NEXT=0
126+for arg in "$@"; do
127+ if [ $SKIP_NEXT -eq 1 ]; then
128+ SKIP_NEXT=0
129+ continue
130+ fi
131+ case "$arg" in
132+ -target) SKIP_NEXT=1 ;;
133+ -Wl,*) ;;
134+ *) ARGS="$ARGS $arg" ;;
135+ esac
136+done
137+exec zig cc -target aarch64-windows-gnu $ARGS
138+ZIGWRAP
139+RUN chmod +x /usr/local/bin/zcc-windows-arm64
140+
141+# Build script
142+COPY <<'SCRIPT' /usr/local/bin/build.sh
143+#!/bin/sh
144+set -e
145+
146+OS=${1:-darwin}
147+ARCH=${2:-arm64}
148+
149+case "${OS}-${ARCH}" in
150+ darwin-arm64|darwin-aarch64)
151+ export CC=zcc-darwin-arm64
152+ export GOARCH=arm64
153+ export GOOS=darwin
154+ ;;
155+ darwin-amd64|darwin-x86_64)
156+ export CC=zcc-darwin-amd64
157+ export GOARCH=amd64
158+ export GOOS=darwin
159+ ;;
160+ linux-arm64|linux-aarch64)
161+ export CC=gcc
162+ export GOARCH=arm64
163+ export GOOS=linux
164+ ;;
165+ linux-amd64|linux-x86_64)
166+ export CC=gcc
167+ export GOARCH=amd64
168+ export GOOS=linux
169+ ;;
170+ windows-arm64|windows-aarch64)
171+ export CC=zcc-windows-arm64
172+ export GOARCH=arm64
173+ export GOOS=windows
174+ ;;
175+ windows-amd64|windows-x86_64)
176+ export CC=zcc-windows-amd64
177+ export GOARCH=amd64
178+ export GOOS=windows
179+ ;;
180+ *)
181+ echo "Usage: <os> <arch>"
182+ echo " os: darwin, linux, windows"
183+ echo " arch: amd64, arm64"
184+ exit 1
185+ ;;
186+esac
187+
188+export CGO_ENABLED=1
189+export CGO_CFLAGS="-w"
190+
191+# Build frontend if exists and not already built (host may have built it)
192+if [ -d "frontend" ] && [ -f "frontend/package.json" ] && [ ! -d "frontend/dist" ]; then
193+ (cd frontend && npm install --silent && npm run build --silent)
194+fi
195+
196+# Build
197+APP=${APP_NAME:-$(basename $(pwd))}
198+mkdir -p bin
199+
200+EXT=""
201+LDFLAGS="-s -w"
202+if [ "$GOOS" = "windows" ]; then
203+ EXT=".exe"
204+ LDFLAGS="-s -w -H windowsgui"
205+fi
206+
207+TAGS="production"
208+if [ -n "$EXTRA_TAGS" ]; then
209+ TAGS="${TAGS},${EXTRA_TAGS}"
210+fi
211+
212+COMPILER="go build"
213+if [ "$OBFUSCATED" = "true" ]; then
214+ COMPILER="garble ${GARBLE_ARGS} build"
215+ TAGS="${TAGS},wails_obfuscated"
216+fi
217+
218+${COMPILER} -tags "$TAGS" -trimpath -buildvcs=false -ldflags="$LDFLAGS" -o bin/${APP}-${GOOS}-${GOARCH}${EXT} .
219+echo "Built: bin/${APP}-${GOOS}-${GOARCH}${EXT}"
220+SCRIPT
221+RUN chmod +x /usr/local/bin/build.sh
222+
223+WORKDIR /app
224+ENTRYPOINT ["/usr/local/bin/build.sh"]
225+CMD ["darwin", "arm64"]
new file mode 100644
@@ -0,0 +1,225 @@
1+# Cross-compile Wails v3 apps to any platform
2+#
3+# Darwin: Zig + macOS SDK
4+# Linux: Native GCC when host matches target, Zig for cross-arch
5+# Windows: Zig + bundled mingw
6+#
7+# Usage:
8+# docker build -t wails-cross -f Dockerfile.cross .
9+# docker run --rm -v $(pwd):/app wails-cross darwin arm64
10+# docker run --rm -v $(pwd):/app wails-cross darwin amd64
11+# docker run --rm -v $(pwd):/app wails-cross linux amd64
12+# docker run --rm -v $(pwd):/app wails-cross linux arm64
13+# docker run --rm -v $(pwd):/app wails-cross windows amd64
14+# docker run --rm -v $(pwd):/app wails-cross windows arm64
15+
16+# Debian 13 provides the GTK >= 4.14 API baseline used by the default Linux backend.
17+FROM golang:1.26-trixie
18+
19+ARG TARGETARCH
20+ARG GARBLE_VERSION=v0.16.0
21+
22+# Install base tools, GCC, and GTK/WebKit dev packages
23+RUN apt-get update && apt-get install -y --no-install-recommends \
24+ curl xz-utils python3 nodejs npm pkg-config gcc libc6-dev \
25+ libgtk-3-dev libwebkit2gtk-4.1-dev \
26+ libgtk-4-dev libwebkitgtk-6.0-dev \
27+ && pkg-config --atleast-version=4.14 gtk4 \
28+ && pkg-config --exists webkitgtk-6.0 \
29+ && pkg-config --exists gtk+-3.0 \
30+ && pkg-config --exists webkit2gtk-4.1 \
31+ && rm -rf /var/lib/apt/lists/*
32+
33+RUN go install mvdan.cc/garble@${GARBLE_VERSION}
34+
35+# Install Zig - automatically selects correct binary for host architecture.
36+# The SHA-256 is fetched from Zig's official download manifest and verified
37+# before extraction to protect against MITM or corrupted downloads.
38+ARG ZIG_VERSION=0.14.0
39+RUN ZIG_ARCH=$(case "${TARGETARCH}" in arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
40+ ZIG_FILE="zig-linux-${ZIG_ARCH}-${ZIG_VERSION}.tar.xz" && \
41+ curl -fSL "https://ziglang.org/download/${ZIG_VERSION}/${ZIG_FILE}" -o "/tmp/${ZIG_FILE}" && \
42+ EXPECTED=$(curl -fsSL "https://ziglang.org/download/index.json" | \
43+ python3 -c "import json,sys; d=json.load(sys.stdin); print(d[sys.argv[1]][sys.argv[2]+'-linux']['shasum'])" \
44+ "${ZIG_VERSION}" "${ZIG_ARCH}") && \
45+ echo "${EXPECTED} /tmp/${ZIG_FILE}" | sha256sum -c - && \
46+ tar -xJ -f "/tmp/${ZIG_FILE}" -C /opt && \
47+ rm "/tmp/${ZIG_FILE}" && \
48+ ln -s /opt/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}/zig /usr/local/bin/zig
49+
50+# Download macOS SDK (required for darwin targets)
51+ARG MACOS_SDK_VERSION=14.5
52+RUN curl -L "https://github.com/joseluisq/macosx-sdks/releases/download/${MACOS_SDK_VERSION}/MacOSX${MACOS_SDK_VERSION}.sdk.tar.xz" \
53+ | tar -xJ -C /opt \
54+ && mv /opt/MacOSX${MACOS_SDK_VERSION}.sdk /opt/macos-sdk
55+
56+ENV MACOS_SDK_PATH=/opt/macos-sdk
57+
58+# Create Zig CC wrappers for cross-compilation targets
59+# Darwin and Windows use Zig; Linux uses native GCC (run with --platform for cross-arch)
60+
61+# Darwin arm64
62+COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-arm64
63+#!/bin/sh
64+ARGS=""
65+SKIP_NEXT=0
66+for arg in "$@"; do
67+ if [ $SKIP_NEXT -eq 1 ]; then
68+ SKIP_NEXT=0
69+ continue
70+ fi
71+ case "$arg" in
72+ -target) SKIP_NEXT=1 ;;
73+ -mmacosx-version-min=*) ;;
74+ *) ARGS="$ARGS $arg" ;;
75+ esac
76+done
77+exec zig cc -fno-sanitize=all -target aarch64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
78+ZIGWRAP
79+RUN chmod +x /usr/local/bin/zcc-darwin-arm64
80+
81+# Darwin amd64
82+COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-amd64
83+#!/bin/sh
84+ARGS=""
85+SKIP_NEXT=0
86+for arg in "$@"; do
87+ if [ $SKIP_NEXT -eq 1 ]; then
88+ SKIP_NEXT=0
89+ continue
90+ fi
91+ case "$arg" in
92+ -target) SKIP_NEXT=1 ;;
93+ -mmacosx-version-min=*) ;;
94+ *) ARGS="$ARGS $arg" ;;
95+ esac
96+done
97+exec zig cc -fno-sanitize=all -target x86_64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
98+ZIGWRAP
99+RUN chmod +x /usr/local/bin/zcc-darwin-amd64
100+
101+# Windows amd64 - uses Zig's bundled mingw
102+COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-amd64
103+#!/bin/sh
104+ARGS=""
105+SKIP_NEXT=0
106+for arg in "$@"; do
107+ if [ $SKIP_NEXT -eq 1 ]; then
108+ SKIP_NEXT=0
109+ continue
110+ fi
111+ case "$arg" in
112+ -target) SKIP_NEXT=1 ;;
113+ -Wl,*) ;;
114+ *) ARGS="$ARGS $arg" ;;
115+ esac
116+done
117+exec zig cc -target x86_64-windows-gnu $ARGS
118+ZIGWRAP
119+RUN chmod +x /usr/local/bin/zcc-windows-amd64
120+
121+# Windows arm64 - uses Zig's bundled mingw
122+COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-arm64
123+#!/bin/sh
124+ARGS=""
125+SKIP_NEXT=0
126+for arg in "$@"; do
127+ if [ $SKIP_NEXT -eq 1 ]; then
128+ SKIP_NEXT=0
129+ continue
130+ fi
131+ case "$arg" in
132+ -target) SKIP_NEXT=1 ;;
133+ -Wl,*) ;;
134+ *) ARGS="$ARGS $arg" ;;
135+ esac
136+done
137+exec zig cc -target aarch64-windows-gnu $ARGS
138+ZIGWRAP
139+RUN chmod +x /usr/local/bin/zcc-windows-arm64
140+
141+# Build script
142+COPY <<'SCRIPT' /usr/local/bin/build.sh
143+#!/bin/sh
144+set -e
145+
146+OS=${1:-darwin}
147+ARCH=${2:-arm64}
148+
149+case "${OS}-${ARCH}" in
150+ darwin-arm64|darwin-aarch64)
151+ export CC=zcc-darwin-arm64
152+ export GOARCH=arm64
153+ export GOOS=darwin
154+ ;;
155+ darwin-amd64|darwin-x86_64)
156+ export CC=zcc-darwin-amd64
157+ export GOARCH=amd64
158+ export GOOS=darwin
159+ ;;
160+ linux-arm64|linux-aarch64)
161+ export CC=gcc
162+ export GOARCH=arm64
163+ export GOOS=linux
164+ ;;
165+ linux-amd64|linux-x86_64)
166+ export CC=gcc
167+ export GOARCH=amd64
168+ export GOOS=linux
169+ ;;
170+ windows-arm64|windows-aarch64)
171+ export CC=zcc-windows-arm64
172+ export GOARCH=arm64
173+ export GOOS=windows
174+ ;;
175+ windows-amd64|windows-x86_64)
176+ export CC=zcc-windows-amd64
177+ export GOARCH=amd64
178+ export GOOS=windows
179+ ;;
180+ *)
181+ echo "Usage: <os> <arch>"
182+ echo " os: darwin, linux, windows"
183+ echo " arch: amd64, arm64"
184+ exit 1
185+ ;;
186+esac
187+
188+export CGO_ENABLED=1
189+export CGO_CFLAGS="-w"
190+
191+# Build frontend if exists and not already built (host may have built it)
192+if [ -d "frontend" ] && [ -f "frontend/package.json" ] && [ ! -d "frontend/dist" ]; then
193+ (cd frontend && npm install --silent && npm run build --silent)
194+fi
195+
196+# Build
197+APP=${APP_NAME:-$(basename $(pwd))}
198+mkdir -p bin
199+
200+EXT=""
201+LDFLAGS="-s -w"
202+if [ "$GOOS" = "windows" ]; then
203+ EXT=".exe"
204+ LDFLAGS="-s -w -H windowsgui"
205+fi
206+
207+TAGS="production"
208+if [ -n "$EXTRA_TAGS" ]; then
209+ TAGS="${TAGS},${EXTRA_TAGS}"
210+fi
211+
212+COMPILER="go build"
213+if [ "$OBFUSCATED" = "true" ]; then
214+ COMPILER="garble ${GARBLE_ARGS} build"
215+ TAGS="${TAGS},wails_obfuscated"
216+fi
217+
218+${COMPILER} -tags "$TAGS" -trimpath -buildvcs=false -ldflags="$LDFLAGS" -o bin/${APP}-${GOOS}-${GOARCH}${EXT} .
219+echo "Built: bin/${APP}-${GOOS}-${GOARCH}${EXT}"
220+SCRIPT
221+RUN chmod +x /usr/local/bin/build.sh
222+
223+WORKDIR /app
224+ENTRYPOINT ["/usr/local/bin/build.sh"]
225+CMD ["darwin", "arm64"]
added build/docker/Dockerfile.server +67 -0
new file mode 100644
@@ -0,0 +1,67 @@
1+# Wails Server Mode Dockerfile
2+# Multi-stage build for a minimal image.
3+#
4+# Defaults produce a pure-Go, fully static binary on a distroless/static base:
5+# the smallest, most secure image, and correct for the default (CGO-free) app.
6+#
7+# If your app needs CGO, override the build args, e.g.:
8+# docker build --build-arg CGO_ENABLED=1 \
9+# --build-arg GO_IMAGE=golang:bookworm \
10+# --build-arg RUNTIME_IMAGE=gcr.io/distroless/base-debian12 \
11+# -f build/docker/Dockerfile.server .
12+# CGO needs a C toolchain in the builder and a libc in the runtime image; keep
13+# the builder and runtime libc compatible (a glibc builder -> a glibc runtime).
14+# The Taskfile passes these through: task build:docker CGO_ENABLED=1 GO_IMAGE=... RUNTIME_IMAGE=...
15+
16+ARG GO_IMAGE=golang:alpine
17+ARG RUNTIME_IMAGE=gcr.io/distroless/static-debian12
18+ARG CGO_ENABLED=0
19+
20+# Build stage
21+FROM ${GO_IMAGE} AS builder
22+ARG CGO_ENABLED
23+
24+WORKDIR /app
25+
26+# Install build dependencies: git always; a C compiler only when CGO is enabled.
27+# Supports both apk (alpine) and apt (debian/bookworm) base images.
28+RUN if command -v apk >/dev/null 2>&1; then \
29+ apk add --no-cache git $([ "$CGO_ENABLED" = "1" ] && echo gcc musl-dev); \
30+ else \
31+ apt-get update && apt-get install -y --no-install-recommends git $([ "$CGO_ENABLED" = "1" ] && echo gcc libc6-dev) && rm -rf /var/lib/apt/lists/*; \
32+ fi
33+
34+# Copy source code
35+COPY . .
36+
37+# Remove local replace directive if present (for production builds)
38+RUN sed -i '/^replace/d' go.mod || true
39+
40+# Download dependencies
41+RUN go mod tidy
42+
43+# Build the production server binary.
44+# - server,production tags: HTTP server mode with production code paths (no dev
45+# logger/asset middleware), matching the desktop production build.
46+# - CGO_ENABLED defaults to 0 for a fully static binary (distroless/static);
47+# override with --build-arg CGO_ENABLED=1 (see header) for CGO apps.
48+RUN CGO_ENABLED=${CGO_ENABLED} go build -tags server,production -trimpath -buildvcs=false -ldflags="-s -w" -o server .
49+
50+# Runtime stage - minimal image
51+FROM ${RUNTIME_IMAGE}
52+
53+# Copy the binary
54+COPY --from=builder /app/server /server
55+
56+# Copy frontend assets
57+COPY --from=builder /app/frontend/dist /frontend/dist
58+
59+# Expose the default port
60+EXPOSE 8080
61+
62+# Bind to all interfaces (required for Docker)
63+# Can be overridden at runtime with -e WAILS_SERVER_HOST=...
64+ENV WAILS_SERVER_HOST=0.0.0.0
65+
66+# Run the server
67+ENTRYPOINT ["/server"]
new file mode 100644
@@ -0,0 +1,67 @@
1+# Wails Server Mode Dockerfile
2+# Multi-stage build for a minimal image.
3+#
4+# Defaults produce a pure-Go, fully static binary on a distroless/static base:
5+# the smallest, most secure image, and correct for the default (CGO-free) app.
6+#
7+# If your app needs CGO, override the build args, e.g.:
8+# docker build --build-arg CGO_ENABLED=1 \
9+# --build-arg GO_IMAGE=golang:bookworm \
10+# --build-arg RUNTIME_IMAGE=gcr.io/distroless/base-debian12 \
11+# -f build/docker/Dockerfile.server .
12+# CGO needs a C toolchain in the builder and a libc in the runtime image; keep
13+# the builder and runtime libc compatible (a glibc builder -> a glibc runtime).
14+# The Taskfile passes these through: task build:docker CGO_ENABLED=1 GO_IMAGE=... RUNTIME_IMAGE=...
15+
16+ARG GO_IMAGE=golang:alpine
17+ARG RUNTIME_IMAGE=gcr.io/distroless/static-debian12
18+ARG CGO_ENABLED=0
19+
20+# Build stage
21+FROM ${GO_IMAGE} AS builder
22+ARG CGO_ENABLED
23+
24+WORKDIR /app
25+
26+# Install build dependencies: git always; a C compiler only when CGO is enabled.
27+# Supports both apk (alpine) and apt (debian/bookworm) base images.
28+RUN if command -v apk >/dev/null 2>&1; then \
29+ apk add --no-cache git $([ "$CGO_ENABLED" = "1" ] && echo gcc musl-dev); \
30+ else \
31+ apt-get update && apt-get install -y --no-install-recommends git $([ "$CGO_ENABLED" = "1" ] && echo gcc libc6-dev) && rm -rf /var/lib/apt/lists/*; \
32+ fi
33+
34+# Copy source code
35+COPY . .
36+
37+# Remove local replace directive if present (for production builds)
38+RUN sed -i '/^replace/d' go.mod || true
39+
40+# Download dependencies
41+RUN go mod tidy
42+
43+# Build the production server binary.
44+# - server,production tags: HTTP server mode with production code paths (no dev
45+# logger/asset middleware), matching the desktop production build.
46+# - CGO_ENABLED defaults to 0 for a fully static binary (distroless/static);
47+# override with --build-arg CGO_ENABLED=1 (see header) for CGO apps.
48+RUN CGO_ENABLED=${CGO_ENABLED} go build -tags server,production -trimpath -buildvcs=false -ldflags="-s -w" -o server .
49+
50+# Runtime stage - minimal image
51+FROM ${RUNTIME_IMAGE}
52+
53+# Copy the binary
54+COPY --from=builder /app/server /server
55+
56+# Copy frontend assets
57+COPY --from=builder /app/frontend/dist /frontend/dist
58+
59+# Expose the default port
60+EXPOSE 8080
61+
62+# Bind to all interfaces (required for Docker)
63+# Can be overridden at runtime with -e WAILS_SERVER_HOST=...
64+ENV WAILS_SERVER_HOST=0.0.0.0
65+
66+# Run the server
67+ENTRYPOINT ["/server"]
added build/linux/Taskfile.yml +220 -0
new file mode 100644
@@ -0,0 +1,220 @@
1+version: '3'
2+
3+includes:
4+ common: ../Taskfile.yml
5+
6+vars:
7+ # Signing configuration - edit these values for your project
8+ # PGP_KEY: "path/to/signing-key.asc"
9+ # SIGN_ROLE: "builder" # Options: origin, maint, archive, builder
10+ #
11+ # Password is stored securely in system keychain. Run: wails3 setup signing
12+
13+ # Docker image for cross-compilation (used when building on non-Linux or no CC available)
14+ CROSS_IMAGE: wails-cross
15+
16+tasks:
17+ build:
18+ summary: Builds the application for Linux
19+ cmds:
20+ # Linux requires CGO - use Docker when:
21+ # 1. Cross-compiling from non-Linux, OR
22+ # 2. No C compiler is available, OR
23+ # 3. Target architecture differs from host architecture (cross-arch compilation)
24+ - task: '{{if and (eq OS "linux") (eq .HAS_CC "true") (eq .TARGET_ARCH ARCH)}}build:native{{else}}build:docker{{end}}'
25+ vars:
26+ ARCH: '{{.ARCH}}'
27+ DEV: '{{.DEV}}'
28+ OUTPUT: '{{.OUTPUT}}'
29+ EXTRA_TAGS: '{{.EXTRA_TAGS}}'
30+ OBFUSCATED: '{{.OBFUSCATED}}'
31+ GARBLE_ARGS: '{{.GARBLE_ARGS}}'
32+ vars:
33+ DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
34+ OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
35+ # Determine target architecture (defaults to host ARCH if not specified)
36+ TARGET_ARCH: '{{.ARCH | default ARCH}}'
37+ # Check if a C compiler is available (gcc or clang) — cross-platform via wails3 tool
38+ HAS_CC:
39+ sh: 'wails3 tool has "gcc|clang"'
40+
41+ build:native:
42+ summary: Builds the application natively on Linux
43+ internal: true
44+ deps:
45+ - task: common:go:mod:tidy
46+ - task: common:build:frontend
47+ vars:
48+ BUILD_FLAGS:
49+ ref: .BUILD_FLAGS
50+ OBFUSCATED:
51+ ref: .OBFUSCATED
52+ DEV:
53+ ref: .DEV
54+ - task: common:generate:icons
55+ - task: generate:dotdesktop
56+ preconditions:
57+ - sh: '{{if eq .OBFUSCATED "true"}}command -v garble >/dev/null 2>&1{{else}}true{{end}}'
58+ msg: "garble is required for obfuscated builds. Install it with: go install mvdan.cc/garble@v0.16.0 (requires Go 1.24+). See https://github.com/burrowers/garble/releases for version/toolchain compatibility."
59+ cmds:
60+ - '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o {{.OUTPUT}}'
61+ vars:
62+ BUILD_FLAGS: '{{if eq .DEV "true"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
63+ DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
64+ OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
65+ env:
66+ GOOS: linux
67+ CGO_ENABLED: 1
68+ GOARCH: '{{.ARCH | default ARCH}}'
69+
70+ build:docker:
71+ summary: Builds for Linux using Docker (for non-Linux hosts or when no C compiler available)
72+ internal: true
73+ deps:
74+ - task: common:build:frontend
75+ vars:
76+ OBFUSCATED:
77+ ref: .OBFUSCATED
78+ - task: common:generate:icons
79+ - task: generate:dotdesktop
80+ preconditions:
81+ - sh: docker info > /dev/null 2>&1
82+ msg: "Docker is required for cross-compilation to Linux. Please install Docker."
83+ - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
84+ msg: |
85+ Docker image '{{.CROSS_IMAGE}}' not found.
86+ Build it first: wails3 task setup:docker
87+ cmds:
88+ - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.DOCKER_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{if eq .OBFUSCATED "true"}}-e OBFUSCATED=true{{end}} {{if .GARBLE_ARGS}}-e GARBLE_ARGS="{{.GARBLE_ARGS}}"{{end}} "{{.CROSS_IMAGE}}" linux {{.DOCKER_ARCH}}
89+ - cmd: docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
90+ platforms: [linux, darwin]
91+ - mkdir -p {{.BIN_DIR}}
92+ - mv "bin/{{.APP_NAME}}-linux-{{.DOCKER_ARCH}}" "{{.OUTPUT}}"
93+ vars:
94+ DOCKER_ARCH: '{{.ARCH | default "amd64"}}'
95+ DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
96+ OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
97+ # Generate Docker volume mounts: Go module cache + go.mod replace directives
98+ # Uses wails3 tool docker-mounts for cross-platform compatibility (Windows/Linux/macOS)
99+ DOCKER_MOUNTS:
100+ sh: 'wails3 tool docker-mounts'
101+
102+ package:
103+ summary: Packages the application for Linux
104+ deps:
105+ - task: build
106+ cmds:
107+ - task: create:appimage
108+ - task: create:deb
109+ - task: create:rpm
110+ - task: create:aur
111+
112+ create:appimage:
113+ summary: Creates an AppImage
114+ dir: build/linux/appimage
115+ deps:
116+ - task: build
117+ - task: generate:dotdesktop
118+ cmds:
119+ - cp "{{.APP_BINARY}}" "{{.APP_NAME}}"
120+ - cp ../../appicon.png "{{.APP_NAME}}.png"
121+ - wails3 generate appimage -binary "{{.APP_NAME}}" -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build
122+ vars:
123+ APP_NAME: '{{.APP_NAME}}'
124+ APP_BINARY: '../../../bin/{{.APP_NAME}}'
125+ ICON: '{{.APP_NAME}}.png'
126+ DESKTOP_FILE: '../{{.APP_NAME}}.desktop'
127+ OUTPUT_DIR: '../../../bin'
128+
129+ create:deb:
130+ summary: Creates a deb package
131+ deps:
132+ - task: build
133+ cmds:
134+ - task: generate:dotdesktop
135+ - task: generate:deb
136+
137+ create:rpm:
138+ summary: Creates a rpm package
139+ deps:
140+ - task: build
141+ cmds:
142+ - task: generate:dotdesktop
143+ - task: generate:rpm
144+
145+ create:aur:
146+ summary: Creates a arch linux packager package
147+ deps:
148+ - task: build
149+ cmds:
150+ - task: generate:dotdesktop
151+ - task: generate:aur
152+
153+ generate:deb:
154+ summary: Creates a deb package
155+ cmds:
156+ - wails3 tool package -name "{{.APP_NAME}}" -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
157+
158+ generate:rpm:
159+ summary: Creates a rpm package
160+ cmds:
161+ - wails3 tool package -name "{{.APP_NAME}}" -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
162+
163+ generate:aur:
164+ summary: Creates a arch linux packager package
165+ cmds:
166+ - wails3 tool package -name "{{.APP_NAME}}" -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
167+
168+ generate:dotdesktop:
169+ summary: Generates a `.desktop` file
170+ dir: build
171+ cmds:
172+ - mkdir -p {{.ROOT_DIR}}/build/linux/appimage
173+ - wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile "{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop" -categories "{{.CATEGORIES}}"
174+ vars:
175+ APP_NAME: '{{.APP_NAME}}'
176+ EXEC: '{{.APP_NAME}}'
177+ ICON: '{{.APP_NAME}}'
178+ CATEGORIES: 'Development;'
179+ OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop'
180+
181+ run:
182+ cmds:
183+ - '{{.BIN_DIR}}/{{.APP_NAME}}'
184+
185+ sign:deb:
186+ summary: Signs the DEB package
187+ desc: |
188+ Signs the .deb package with a PGP key.
189+ Set PGP_KEY in the vars section to override the key configured globally via
190+ `wails3 setup` (~/.config/wails/defaults.yaml).
191+ Password is retrieved from system keychain (run: wails3 setup signing)
192+ deps:
193+ - task: create:deb
194+ cmds:
195+ # PGP_KEY is optional: if unset, `wails3 tool sign` falls back to the key
196+ # configured globally via `wails3 setup`.
197+ - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.deb" {{if .PGP_KEY}}--pgp-key "{{.PGP_KEY}}"{{end}} {{if .SIGN_ROLE}}--role "{{.SIGN_ROLE}}"{{end}}
198+
199+ sign:rpm:
200+ summary: Signs the RPM package
201+ desc: |
202+ Signs the .rpm package with a PGP key.
203+ Set PGP_KEY in the vars section to override the key configured globally via
204+ `wails3 setup` (~/.config/wails/defaults.yaml).
205+ Password is retrieved from system keychain (run: wails3 setup signing)
206+ deps:
207+ - task: create:rpm
208+ cmds:
209+ - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.rpm" {{if .PGP_KEY}}--pgp-key "{{.PGP_KEY}}"{{end}}
210+
211+ sign:packages:
212+ summary: Signs all Linux packages (DEB and RPM)
213+ desc: |
214+ Signs both .deb and .rpm packages with a PGP key.
215+ Set PGP_KEY in the vars section to override the key configured globally via
216+ `wails3 setup` (~/.config/wails/defaults.yaml).
217+ Password is retrieved from system keychain (run: wails3 setup signing)
218+ cmds:
219+ - task: sign:deb
220+ - task: sign:rpm
new file mode 100644
@@ -0,0 +1,220 @@
1+version: '3'
2+
3+includes:
4+ common: ../Taskfile.yml
5+
6+vars:
7+ # Signing configuration - edit these values for your project
8+ # PGP_KEY: "path/to/signing-key.asc"
9+ # SIGN_ROLE: "builder" # Options: origin, maint, archive, builder
10+ #
11+ # Password is stored securely in system keychain. Run: wails3 setup signing
12+
13+ # Docker image for cross-compilation (used when building on non-Linux or no CC available)
14+ CROSS_IMAGE: wails-cross
15+
16+tasks:
17+ build:
18+ summary: Builds the application for Linux
19+ cmds:
20+ # Linux requires CGO - use Docker when:
21+ # 1. Cross-compiling from non-Linux, OR
22+ # 2. No C compiler is available, OR
23+ # 3. Target architecture differs from host architecture (cross-arch compilation)
24+ - task: '{{if and (eq OS "linux") (eq .HAS_CC "true") (eq .TARGET_ARCH ARCH)}}build:native{{else}}build:docker{{end}}'
25+ vars:
26+ ARCH: '{{.ARCH}}'
27+ DEV: '{{.DEV}}'
28+ OUTPUT: '{{.OUTPUT}}'
29+ EXTRA_TAGS: '{{.EXTRA_TAGS}}'
30+ OBFUSCATED: '{{.OBFUSCATED}}'
31+ GARBLE_ARGS: '{{.GARBLE_ARGS}}'
32+ vars:
33+ DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
34+ OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
35+ # Determine target architecture (defaults to host ARCH if not specified)
36+ TARGET_ARCH: '{{.ARCH | default ARCH}}'
37+ # Check if a C compiler is available (gcc or clang) — cross-platform via wails3 tool
38+ HAS_CC:
39+ sh: 'wails3 tool has "gcc|clang"'
40+
41+ build:native:
42+ summary: Builds the application natively on Linux
43+ internal: true
44+ deps:
45+ - task: common:go:mod:tidy
46+ - task: common:build:frontend
47+ vars:
48+ BUILD_FLAGS:
49+ ref: .BUILD_FLAGS
50+ OBFUSCATED:
51+ ref: .OBFUSCATED
52+ DEV:
53+ ref: .DEV
54+ - task: common:generate:icons
55+ - task: generate:dotdesktop
56+ preconditions:
57+ - sh: '{{if eq .OBFUSCATED "true"}}command -v garble >/dev/null 2>&1{{else}}true{{end}}'
58+ msg: "garble is required for obfuscated builds. Install it with: go install mvdan.cc/garble@v0.16.0 (requires Go 1.24+). See https://github.com/burrowers/garble/releases for version/toolchain compatibility."
59+ cmds:
60+ - '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o {{.OUTPUT}}'
61+ vars:
62+ BUILD_FLAGS: '{{if eq .DEV "true"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
63+ DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
64+ OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
65+ env:
66+ GOOS: linux
67+ CGO_ENABLED: 1
68+ GOARCH: '{{.ARCH | default ARCH}}'
69+
70+ build:docker:
71+ summary: Builds for Linux using Docker (for non-Linux hosts or when no C compiler available)
72+ internal: true
73+ deps:
74+ - task: common:build:frontend
75+ vars:
76+ OBFUSCATED:
77+ ref: .OBFUSCATED
78+ - task: common:generate:icons
79+ - task: generate:dotdesktop
80+ preconditions:
81+ - sh: docker info > /dev/null 2>&1
82+ msg: "Docker is required for cross-compilation to Linux. Please install Docker."
83+ - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
84+ msg: |
85+ Docker image '{{.CROSS_IMAGE}}' not found.
86+ Build it first: wails3 task setup:docker
87+ cmds:
88+ - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.DOCKER_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{if eq .OBFUSCATED "true"}}-e OBFUSCATED=true{{end}} {{if .GARBLE_ARGS}}-e GARBLE_ARGS="{{.GARBLE_ARGS}}"{{end}} "{{.CROSS_IMAGE}}" linux {{.DOCKER_ARCH}}
89+ - cmd: docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
90+ platforms: [linux, darwin]
91+ - mkdir -p {{.BIN_DIR}}
92+ - mv "bin/{{.APP_NAME}}-linux-{{.DOCKER_ARCH}}" "{{.OUTPUT}}"
93+ vars:
94+ DOCKER_ARCH: '{{.ARCH | default "amd64"}}'
95+ DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
96+ OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
97+ # Generate Docker volume mounts: Go module cache + go.mod replace directives
98+ # Uses wails3 tool docker-mounts for cross-platform compatibility (Windows/Linux/macOS)
99+ DOCKER_MOUNTS:
100+ sh: 'wails3 tool docker-mounts'
101+
102+ package:
103+ summary: Packages the application for Linux
104+ deps:
105+ - task: build
106+ cmds:
107+ - task: create:appimage
108+ - task: create:deb
109+ - task: create:rpm
110+ - task: create:aur
111+
112+ create:appimage:
113+ summary: Creates an AppImage
114+ dir: build/linux/appimage
115+ deps:
116+ - task: build
117+ - task: generate:dotdesktop
118+ cmds:
119+ - cp "{{.APP_BINARY}}" "{{.APP_NAME}}"
120+ - cp ../../appicon.png "{{.APP_NAME}}.png"
121+ - wails3 generate appimage -binary "{{.APP_NAME}}" -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build
122+ vars:
123+ APP_NAME: '{{.APP_NAME}}'
124+ APP_BINARY: '../../../bin/{{.APP_NAME}}'
125+ ICON: '{{.APP_NAME}}.png'
126+ DESKTOP_FILE: '../{{.APP_NAME}}.desktop'
127+ OUTPUT_DIR: '../../../bin'
128+
129+ create:deb:
130+ summary: Creates a deb package
131+ deps:
132+ - task: build
133+ cmds:
134+ - task: generate:dotdesktop
135+ - task: generate:deb
136+
137+ create:rpm:
138+ summary: Creates a rpm package
139+ deps:
140+ - task: build
141+ cmds:
142+ - task: generate:dotdesktop
143+ - task: generate:rpm
144+
145+ create:aur:
146+ summary: Creates a arch linux packager package
147+ deps:
148+ - task: build
149+ cmds:
150+ - task: generate:dotdesktop
151+ - task: generate:aur
152+
153+ generate:deb:
154+ summary: Creates a deb package
155+ cmds:
156+ - wails3 tool package -name "{{.APP_NAME}}" -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
157+
158+ generate:rpm:
159+ summary: Creates a rpm package
160+ cmds:
161+ - wails3 tool package -name "{{.APP_NAME}}" -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
162+
163+ generate:aur:
164+ summary: Creates a arch linux packager package
165+ cmds:
166+ - wails3 tool package -name "{{.APP_NAME}}" -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
167+
168+ generate:dotdesktop:
169+ summary: Generates a `.desktop` file
170+ dir: build
171+ cmds:
172+ - mkdir -p {{.ROOT_DIR}}/build/linux/appimage
173+ - wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile "{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop" -categories "{{.CATEGORIES}}"
174+ vars:
175+ APP_NAME: '{{.APP_NAME}}'
176+ EXEC: '{{.APP_NAME}}'
177+ ICON: '{{.APP_NAME}}'
178+ CATEGORIES: 'Development;'
179+ OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop'
180+
181+ run:
182+ cmds:
183+ - '{{.BIN_DIR}}/{{.APP_NAME}}'
184+
185+ sign:deb:
186+ summary: Signs the DEB package
187+ desc: |
188+ Signs the .deb package with a PGP key.
189+ Set PGP_KEY in the vars section to override the key configured globally via
190+ `wails3 setup` (~/.config/wails/defaults.yaml).
191+ Password is retrieved from system keychain (run: wails3 setup signing)
192+ deps:
193+ - task: create:deb
194+ cmds:
195+ # PGP_KEY is optional: if unset, `wails3 tool sign` falls back to the key
196+ # configured globally via `wails3 setup`.
197+ - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.deb" {{if .PGP_KEY}}--pgp-key "{{.PGP_KEY}}"{{end}} {{if .SIGN_ROLE}}--role "{{.SIGN_ROLE}}"{{end}}
198+
199+ sign:rpm:
200+ summary: Signs the RPM package
201+ desc: |
202+ Signs the .rpm package with a PGP key.
203+ Set PGP_KEY in the vars section to override the key configured globally via
204+ `wails3 setup` (~/.config/wails/defaults.yaml).
205+ Password is retrieved from system keychain (run: wails3 setup signing)
206+ deps:
207+ - task: create:rpm
208+ cmds:
209+ - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.rpm" {{if .PGP_KEY}}--pgp-key "{{.PGP_KEY}}"{{end}}
210+
211+ sign:packages:
212+ summary: Signs all Linux packages (DEB and RPM)
213+ desc: |
214+ Signs both .deb and .rpm packages with a PGP key.
215+ Set PGP_KEY in the vars section to override the key configured globally via
216+ `wails3 setup` (~/.config/wails/defaults.yaml).
217+ Password is retrieved from system keychain (run: wails3 setup signing)
218+ cmds:
219+ - task: sign:deb
220+ - task: sign:rpm
added build/linux/appimage/build.sh +35 -0
new file mode 100644
@@ -0,0 +1,35 @@
1+#!/usr/bin/env bash
2+# Copyright (c) 2018-Present Lea Anthony
3+# SPDX-License-Identifier: MIT
4+
5+# Fail script on any error
6+set -euxo pipefail
7+
8+# Define variables
9+APP_DIR="${APP_NAME}.AppDir"
10+
11+# Create AppDir structure
12+mkdir -p "${APP_DIR}/usr/bin"
13+cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/"
14+cp "${ICON_PATH}" "${APP_DIR}/"
15+cp "${DESKTOP_FILE}" "${APP_DIR}/"
16+
17+if [[ $(uname -m) == *x86_64* ]]; then
18+ # Download linuxdeploy and make it executable
19+ wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
20+ chmod +x linuxdeploy-x86_64.AppImage
21+
22+ # Run linuxdeploy to bundle the application
23+ ./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage
24+else
25+ # Download linuxdeploy and make it executable (arm64)
26+ wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage
27+ chmod +x linuxdeploy-aarch64.AppImage
28+
29+ # Run linuxdeploy to bundle the application (arm64)
30+ ./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage
31+fi
32+
33+# Rename the generated AppImage
34+mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage"
35+
new file mode 100644
@@ -0,0 +1,35 @@
1+#!/usr/bin/env bash
2+# Copyright (c) 2018-Present Lea Anthony
3+# SPDX-License-Identifier: MIT
4+
5+# Fail script on any error
6+set -euxo pipefail
7+
8+# Define variables
9+APP_DIR="${APP_NAME}.AppDir"
10+
11+# Create AppDir structure
12+mkdir -p "${APP_DIR}/usr/bin"
13+cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/"
14+cp "${ICON_PATH}" "${APP_DIR}/"
15+cp "${DESKTOP_FILE}" "${APP_DIR}/"
16+
17+if [[ $(uname -m) == *x86_64* ]]; then
18+ # Download linuxdeploy and make it executable
19+ wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
20+ chmod +x linuxdeploy-x86_64.AppImage
21+
22+ # Run linuxdeploy to bundle the application
23+ ./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage
24+else
25+ # Download linuxdeploy and make it executable (arm64)
26+ wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage
27+ chmod +x linuxdeploy-aarch64.AppImage
28+
29+ # Run linuxdeploy to bundle the application (arm64)
30+ ./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage
31+fi
32+
33+# Rename the generated AppImage
34+mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage"
35+
added build/linux/desktop +13 -0
new file mode 100644
@@ -0,0 +1,13 @@
1+[Desktop Entry]
2+Version=1.0
3+Name=Sidekicks
4+Comment=Converse with AI agents in channels
5+# The Exec line includes %u to pass the URL to the application
6+Exec=/usr/local/bin/sidekicks %u
7+Terminal=false
8+Type=Application
9+Icon=sidekicks
10+Categories=Utility;
11+StartupWMClass=sidekicks
12+
13+
new file mode 100644
@@ -0,0 +1,13 @@
1+[Desktop Entry]
2+Version=1.0
3+Name=Sidekicks
4+Comment=Converse with AI agents in channels
5+# The Exec line includes %u to pass the URL to the application
6+Exec=/usr/local/bin/sidekicks %u
7+Terminal=false
8+Type=Application
9+Icon=sidekicks
10+Categories=Utility;
11+StartupWMClass=sidekicks
12+
13+
added build/linux/nfpm/nfpm.yaml +80 -0
new file mode 100644
@@ -0,0 +1,80 @@
1+# Feel free to remove those if you don't want/need to use them.
2+# Make sure to check the documentation at https://nfpm.goreleaser.com
3+#
4+# The lines below are called `modelines`. See `:help modeline`
5+
6+name: "sidekicks"
7+arch: ${GOARCH}
8+platform: "linux"
9+version: "0.0.1"
10+section: "default"
11+priority: "extra"
12+maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
13+description: "Converse with AI agents in channels"
14+vendor: "Philippe Charrière"
15+homepage: "https://wails.io"
16+license: "MIT"
17+release: "1"
18+
19+contents:
20+ - src: "./bin/sidekicks"
21+ dst: "/usr/local/bin/sidekicks"
22+ - src: "./build/appicon.png"
23+ dst: "/usr/share/icons/hicolor/128x128/apps/sidekicks.png"
24+ - src: "./build/linux/sidekicks.desktop"
25+ dst: "/usr/share/applications/sidekicks.desktop"
26+
27+# Default dependencies for the GTK4 + WebKitGTK 6.0 stack (Ubuntu 24.04+ / Debian 13+)
28+depends:
29+ - libgtk-4-1
30+ - libwebkitgtk-6.0-4
31+
32+# Distribution-specific overrides for different package formats
33+overrides:
34+ # RPM packages for Fedora / RHEL / AlmaLinux / Rocky Linux
35+ rpm:
36+ depends:
37+ - gtk4
38+ - webkitgtk6.0
39+
40+ # Arch Linux packages
41+ archlinux:
42+ depends:
43+ - gtk4
44+ - webkitgtk-6.0
45+
46+# scripts section to ensure desktop database is updated after install
47+scripts:
48+ postinstall: "./build/linux/nfpm/scripts/postinstall.sh"
49+ # You can also add preremove, postremove if needed
50+ # preremove: "./build/linux/nfpm/scripts/preremove.sh"
51+ # postremove: "./build/linux/nfpm/scripts/postremove.sh"
52+
53+# If you build your app with -tags gtk3 (legacy WebKit2GTK 4.1 stack — supported through v3.0.x, removed in v3.1),
54+# replace the depends/overrides above with these:
55+#
56+# depends:
57+# - libgtk-3-0
58+# - libwebkit2gtk-4.1-0
59+# overrides:
60+# rpm:
61+# depends:
62+# - gtk3
63+# - webkit2gtk4.1
64+# archlinux:
65+# depends:
66+# - gtk3
67+# - webkit2gtk-4.1
68+#
69+# replaces:
70+# - foobar
71+# provides:
72+# - bar
73+# recommends:
74+# - whatever
75+# suggests:
76+# - something-else
77+# conflicts:
78+# - not-foo
79+# - not-bar
80+# changelog: "changelog.yaml"
new file mode 100644
@@ -0,0 +1,80 @@
1+# Feel free to remove those if you don't want/need to use them.
2+# Make sure to check the documentation at https://nfpm.goreleaser.com
3+#
4+# The lines below are called `modelines`. See `:help modeline`
5+
6+name: "sidekicks"
7+arch: ${GOARCH}
8+platform: "linux"
9+version: "0.0.1"
10+section: "default"
11+priority: "extra"
12+maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
13+description: "Converse with AI agents in channels"
14+vendor: "Philippe Charrière"
15+homepage: "https://wails.io"
16+license: "MIT"
17+release: "1"
18+
19+contents:
20+ - src: "./bin/sidekicks"
21+ dst: "/usr/local/bin/sidekicks"
22+ - src: "./build/appicon.png"
23+ dst: "/usr/share/icons/hicolor/128x128/apps/sidekicks.png"
24+ - src: "./build/linux/sidekicks.desktop"
25+ dst: "/usr/share/applications/sidekicks.desktop"
26+
27+# Default dependencies for the GTK4 + WebKitGTK 6.0 stack (Ubuntu 24.04+ / Debian 13+)
28+depends:
29+ - libgtk-4-1
30+ - libwebkitgtk-6.0-4
31+
32+# Distribution-specific overrides for different package formats
33+overrides:
34+ # RPM packages for Fedora / RHEL / AlmaLinux / Rocky Linux
35+ rpm:
36+ depends:
37+ - gtk4
38+ - webkitgtk6.0
39+
40+ # Arch Linux packages
41+ archlinux:
42+ depends:
43+ - gtk4
44+ - webkitgtk-6.0
45+
46+# scripts section to ensure desktop database is updated after install
47+scripts:
48+ postinstall: "./build/linux/nfpm/scripts/postinstall.sh"
49+ # You can also add preremove, postremove if needed
50+ # preremove: "./build/linux/nfpm/scripts/preremove.sh"
51+ # postremove: "./build/linux/nfpm/scripts/postremove.sh"
52+
53+# If you build your app with -tags gtk3 (legacy WebKit2GTK 4.1 stack — supported through v3.0.x, removed in v3.1),
54+# replace the depends/overrides above with these:
55+#
56+# depends:
57+# - libgtk-3-0
58+# - libwebkit2gtk-4.1-0
59+# overrides:
60+# rpm:
61+# depends:
62+# - gtk3
63+# - webkit2gtk4.1
64+# archlinux:
65+# depends:
66+# - gtk3
67+# - webkit2gtk-4.1
68+#
69+# replaces:
70+# - foobar
71+# provides:
72+# - bar
73+# recommends:
74+# - whatever
75+# suggests:
76+# - something-else
77+# conflicts:
78+# - not-foo
79+# - not-bar
80+# changelog: "changelog.yaml"
added build/linux/nfpm/scripts/postinstall.sh +21 -0
new file mode 100644
@@ -0,0 +1,21 @@
1+#!/bin/sh
2+
3+# Update desktop database for .desktop file changes
4+# This makes the application appear in application menus and registers its capabilities.
5+if command -v update-desktop-database >/dev/null 2>&1; then
6+ echo "Updating desktop database..."
7+ update-desktop-database -q /usr/share/applications
8+else
9+ echo "Warning: update-desktop-database command not found. Desktop file may not be immediately recognized." >&2
10+fi
11+
12+# Update MIME database for custom URL schemes (x-scheme-handler)
13+# This ensures the system knows how to handle your custom protocols.
14+if command -v update-mime-database >/dev/null 2>&1; then
15+ echo "Updating MIME database..."
16+ update-mime-database -n /usr/share/mime
17+else
18+ echo "Warning: update-mime-database command not found. Custom URL schemes may not be immediately recognized." >&2
19+fi
20+
21+exit 0
new file mode 100644
@@ -0,0 +1,21 @@
1+#!/bin/sh
2+
3+# Update desktop database for .desktop file changes
4+# This makes the application appear in application menus and registers its capabilities.
5+if command -v update-desktop-database >/dev/null 2>&1; then
6+ echo "Updating desktop database..."
7+ update-desktop-database -q /usr/share/applications
8+else
9+ echo "Warning: update-desktop-database command not found. Desktop file may not be immediately recognized." >&2
10+fi
11+
12+# Update MIME database for custom URL schemes (x-scheme-handler)
13+# This ensures the system knows how to handle your custom protocols.
14+if command -v update-mime-database >/dev/null 2>&1; then
15+ echo "Updating MIME database..."
16+ update-mime-database -n /usr/share/mime
17+else
18+ echo "Warning: update-mime-database command not found. Custom URL schemes may not be immediately recognized." >&2
19+fi
20+
21+exit 0
added build/linux/nfpm/scripts/postremove.sh +1 -0
new file mode 100644
@@ -0,0 +1 @@
1+#!/bin/bash
new file mode 100644
@@ -0,0 +1 @@
1+#!/bin/bash
added build/linux/nfpm/scripts/preinstall.sh +1 -0
new file mode 100644
@@ -0,0 +1 @@
1+#!/bin/bash
new file mode 100644
@@ -0,0 +1 @@
1+#!/bin/bash
added build/linux/nfpm/scripts/preremove.sh +1 -0
new file mode 100644
@@ -0,0 +1 @@
1+#!/bin/bash
new file mode 100644
@@ -0,0 +1 @@
1+#!/bin/bash
added build/windows/Taskfile.yml +195 -0
new file mode 100644
@@ -0,0 +1,195 @@
1+version: '3'
2+
3+includes:
4+ common: ../Taskfile.yml
5+
6+vars:
7+ # Signing configuration - edit these values for your project
8+ # SIGN_CERTIFICATE: "path/to/certificate.pfx"
9+ # SIGN_THUMBPRINT: "certificate-thumbprint" # Alternative to SIGN_CERTIFICATE
10+ # TIMESTAMP_SERVER: "http://timestamp.digicert.com"
11+ #
12+ # Password is stored securely in system keychain. Run: wails3 setup signing
13+
14+ # Docker image for cross-compilation with CGO (used when CGO_ENABLED=1 on non-Windows)
15+ CROSS_IMAGE: wails-cross
16+
17+tasks:
18+ build:
19+ summary: Builds the application for Windows
20+ cmds:
21+ # Auto-detect CGO: if CGO_ENABLED=1, use Docker; otherwise use native Go cross-compile
22+ - task: '{{if and (ne OS "windows") (eq .CGO_ENABLED "1")}}build:docker{{else}}build:native{{end}}'
23+ vars:
24+ ARCH: '{{.ARCH}}'
25+ DEV: '{{.DEV}}'
26+ EXTRA_TAGS: '{{.EXTRA_TAGS}}'
27+ OBFUSCATED: '{{.OBFUSCATED}}'
28+ GARBLE_ARGS: '{{.GARBLE_ARGS}}'
29+ vars:
30+ # Default to CGO_ENABLED=0 if not explicitly set
31+ CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}'
32+
33+ build:native:
34+ summary: Builds the application using native Go cross-compilation
35+ internal: true
36+ deps:
37+ - task: common:go:mod:tidy
38+ - task: common:build:frontend
39+ vars:
40+ BUILD_FLAGS:
41+ ref: .BUILD_FLAGS
42+ OBFUSCATED:
43+ ref: .OBFUSCATED
44+ DEV:
45+ ref: .DEV
46+ - task: common:generate:icons
47+ preconditions:
48+ - sh: '{{if eq .OBFUSCATED "true"}}command -v garble >/dev/null 2>&1{{else}}true{{end}}'
49+ msg: "garble is required for obfuscated builds. Install it with: go install mvdan.cc/garble@v0.16.0 (requires Go 1.24+). See https://github.com/burrowers/garble/releases for version/toolchain compatibility."
50+ cmds:
51+ - task: generate:syso
52+ vars:
53+ ARCH: '{{.ARCH}}'
54+ - '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}.exe"'
55+ - cmd: powershell Remove-item *.syso
56+ platforms: [windows]
57+ - cmd: rm -f *.syso
58+ platforms: [linux, darwin]
59+ vars:
60+ BUILD_FLAGS: '{{if eq .DEV "true"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{end}}'
61+ env:
62+ GOOS: windows
63+ CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}'
64+ GOARCH: '{{.ARCH | default ARCH}}'
65+
66+ build:docker:
67+ summary: Cross-compiles for Windows using Docker with Zig (for CGO builds on non-Windows)
68+ internal: true
69+ deps:
70+ - task: common:build:frontend
71+ vars:
72+ OBFUSCATED:
73+ ref: .OBFUSCATED
74+ - task: common:generate:icons
75+ preconditions:
76+ - sh: docker info > /dev/null 2>&1
77+ msg: "Docker is required for CGO cross-compilation. Please install Docker."
78+ - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
79+ msg: |
80+ Docker image '{{.CROSS_IMAGE}}' not found.
81+ Build it first: wails3 task setup:docker
82+ cmds:
83+ - task: generate:syso
84+ vars:
85+ ARCH: '{{.ARCH}}'
86+ - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.DOCKER_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{if eq .OBFUSCATED "true"}}-e OBFUSCATED=true{{end}} {{if .GARBLE_ARGS}}-e GARBLE_ARGS="{{.GARBLE_ARGS}}"{{end}} {{.CROSS_IMAGE}} windows {{.DOCKER_ARCH}}
87+ - cmd: docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
88+ platforms: [linux, darwin]
89+ - rm -f *.syso
90+ vars:
91+ DOCKER_ARCH: '{{.ARCH | default "amd64"}}'
92+ # Generate Docker volume mounts: Go module cache + go.mod replace directives
93+ # Uses wails3 tool docker-mounts for cross-platform compatibility (Windows/Linux/macOS)
94+ DOCKER_MOUNTS:
95+ sh: 'wails3 tool docker-mounts'
96+
97+ package:
98+ summary: Packages the application
99+ cmds:
100+ - task: '{{if eq (.FORMAT | default "nsis") "msix"}}create:msix:package{{else}}create:nsis:installer{{end}}'
101+ vars:
102+ INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}'
103+ vars:
104+ FORMAT: '{{.FORMAT | default "nsis"}}'
105+ INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}'
106+
107+ generate:syso:
108+ summary: Generates Windows `.syso` file
109+ dir: build
110+ cmds:
111+ - wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso
112+ vars:
113+ ARCH: '{{.ARCH | default ARCH}}'
114+
115+ create:nsis:installer:
116+ summary: Creates an NSIS installer
117+ dir: build/windows/nsis
118+ deps:
119+ - task: build
120+ preconditions:
121+ - sh: '[ "{{.INSTALL_SCOPE}}" = "user" ] || [ "{{.INSTALL_SCOPE}}" = "machine" ]'
122+ msg: "INSTALL_SCOPE must be 'user' or 'machine', got '{{.INSTALL_SCOPE}}'"
123+ cmds:
124+ # Create the Microsoft WebView2 bootstrapper if it doesn't exist
125+ - wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis"
126+ - |
127+ {{if eq OS "windows"}}
128+ makensis {{if eq .INSTALL_SCOPE "user"}}-DWAILS_INSTALL_SCOPE=user -DREQUEST_EXECUTION_LEVEL=user {{end}}-DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi
129+ {{else}}
130+ makensis {{if eq .INSTALL_SCOPE "user"}}-DWAILS_INSTALL_SCOPE=user -DREQUEST_EXECUTION_LEVEL=user {{end}}-DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi
131+ {{end}}
132+ vars:
133+ ARCH: '{{.ARCH | default ARCH}}'
134+ ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
135+ INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}'
136+
137+ create:msix:package:
138+ summary: Creates an MSIX package
139+ deps:
140+ - task: build
141+ cmds:
142+ - |-
143+ wails3 tool msix \
144+ --config "{{.ROOT_DIR}}/build/config.yml" \
145+ --name "{{.APP_NAME}}.exe" \
146+ --executable "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" \
147+ --arch "{{.ARCH}}" \
148+ --out "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}-{{.ARCH}}.msix" \
149+ {{if .CERT_PATH}}--cert "{{.CERT_PATH}}"{{end}} \
150+ {{if .PUBLISHER}}--publisher "{{.PUBLISHER}}"{{end}} \
151+ {{if eq .USE_MSIX_TOOL "true"}}--use-msix-tool{{else}}--use-makeappx{{end}}
152+ vars:
153+ ARCH: '{{.ARCH | default ARCH}}'
154+ CERT_PATH: '{{.CERT_PATH | default ""}}'
155+ PUBLISHER: '{{.PUBLISHER | default ""}}'
156+ USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}'
157+
158+ install:msix:tools:
159+ summary: Installs tools required for MSIX packaging
160+ cmds:
161+ - wails3 tool msix-install-tools
162+
163+ run:
164+ cmds:
165+ - '{{.BIN_DIR}}/{{.APP_NAME}}.exe'
166+
167+ sign:
168+ summary: Signs the Windows executable
169+ desc: |
170+ Signs the .exe with an Authenticode certificate.
171+ Set SIGN_CERTIFICATE or SIGN_THUMBPRINT to override the certificate
172+ configured globally via `wails3 setup` (~/.config/wails/defaults.yaml).
173+ Password is retrieved from system keychain (run: wails3 setup signing)
174+ deps:
175+ - task: build
176+ cmds:
177+ # Certificate vars are optional: if unset, `wails3 tool sign` falls back to
178+ # the certificate configured globally via `wails3 setup`.
179+ - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.exe" {{if .SIGN_CERTIFICATE}}--certificate "{{.SIGN_CERTIFICATE}}"{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint "{{.SIGN_THUMBPRINT}}"{{end}} {{if .TIMESTAMP_SERVER}}--timestamp "{{.TIMESTAMP_SERVER}}"{{end}}
180+
181+ sign:installer:
182+ summary: Signs the NSIS installer
183+ desc: |
184+ Creates and signs the NSIS installer.
185+ Set SIGN_CERTIFICATE or SIGN_THUMBPRINT to override the certificate
186+ configured globally via `wails3 setup` (~/.config/wails/defaults.yaml).
187+ Password is retrieved from system keychain (run: wails3 setup signing)
188+ deps:
189+ - task: create:nsis:installer
190+ vars:
191+ INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}'
192+ vars:
193+ INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}'
194+ cmds:
195+ - wails3 tool sign --input "build/windows/nsis/{{.APP_NAME}}-installer.exe" {{if .SIGN_CERTIFICATE}}--certificate "{{.SIGN_CERTIFICATE}}"{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint "{{.SIGN_THUMBPRINT}}"{{end}} {{if .TIMESTAMP_SERVER}}--timestamp "{{.TIMESTAMP_SERVER}}"{{end}}
new file mode 100644
@@ -0,0 +1,195 @@
1+version: '3'
2+
3+includes:
4+ common: ../Taskfile.yml
5+
6+vars:
7+ # Signing configuration - edit these values for your project
8+ # SIGN_CERTIFICATE: "path/to/certificate.pfx"
9+ # SIGN_THUMBPRINT: "certificate-thumbprint" # Alternative to SIGN_CERTIFICATE
10+ # TIMESTAMP_SERVER: "http://timestamp.digicert.com"
11+ #
12+ # Password is stored securely in system keychain. Run: wails3 setup signing
13+
14+ # Docker image for cross-compilation with CGO (used when CGO_ENABLED=1 on non-Windows)
15+ CROSS_IMAGE: wails-cross
16+
17+tasks:
18+ build:
19+ summary: Builds the application for Windows
20+ cmds:
21+ # Auto-detect CGO: if CGO_ENABLED=1, use Docker; otherwise use native Go cross-compile
22+ - task: '{{if and (ne OS "windows") (eq .CGO_ENABLED "1")}}build:docker{{else}}build:native{{end}}'
23+ vars:
24+ ARCH: '{{.ARCH}}'
25+ DEV: '{{.DEV}}'
26+ EXTRA_TAGS: '{{.EXTRA_TAGS}}'
27+ OBFUSCATED: '{{.OBFUSCATED}}'
28+ GARBLE_ARGS: '{{.GARBLE_ARGS}}'
29+ vars:
30+ # Default to CGO_ENABLED=0 if not explicitly set
31+ CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}'
32+
33+ build:native:
34+ summary: Builds the application using native Go cross-compilation
35+ internal: true
36+ deps:
37+ - task: common:go:mod:tidy
38+ - task: common:build:frontend
39+ vars:
40+ BUILD_FLAGS:
41+ ref: .BUILD_FLAGS
42+ OBFUSCATED:
43+ ref: .OBFUSCATED
44+ DEV:
45+ ref: .DEV
46+ - task: common:generate:icons
47+ preconditions:
48+ - sh: '{{if eq .OBFUSCATED "true"}}command -v garble >/dev/null 2>&1{{else}}true{{end}}'
49+ msg: "garble is required for obfuscated builds. Install it with: go install mvdan.cc/garble@v0.16.0 (requires Go 1.24+). See https://github.com/burrowers/garble/releases for version/toolchain compatibility."
50+ cmds:
51+ - task: generate:syso
52+ vars:
53+ ARCH: '{{.ARCH}}'
54+ - '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}.exe"'
55+ - cmd: powershell Remove-item *.syso
56+ platforms: [windows]
57+ - cmd: rm -f *.syso
58+ platforms: [linux, darwin]
59+ vars:
60+ BUILD_FLAGS: '{{if eq .DEV "true"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{end}}'
61+ env:
62+ GOOS: windows
63+ CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}'
64+ GOARCH: '{{.ARCH | default ARCH}}'
65+
66+ build:docker:
67+ summary: Cross-compiles for Windows using Docker with Zig (for CGO builds on non-Windows)
68+ internal: true
69+ deps:
70+ - task: common:build:frontend
71+ vars:
72+ OBFUSCATED:
73+ ref: .OBFUSCATED
74+ - task: common:generate:icons
75+ preconditions:
76+ - sh: docker info > /dev/null 2>&1
77+ msg: "Docker is required for CGO cross-compilation. Please install Docker."
78+ - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
79+ msg: |
80+ Docker image '{{.CROSS_IMAGE}}' not found.
81+ Build it first: wails3 task setup:docker
82+ cmds:
83+ - task: generate:syso
84+ vars:
85+ ARCH: '{{.ARCH}}'
86+ - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.DOCKER_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{if eq .OBFUSCATED "true"}}-e OBFUSCATED=true{{end}} {{if .GARBLE_ARGS}}-e GARBLE_ARGS="{{.GARBLE_ARGS}}"{{end}} {{.CROSS_IMAGE}} windows {{.DOCKER_ARCH}}
87+ - cmd: docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
88+ platforms: [linux, darwin]
89+ - rm -f *.syso
90+ vars:
91+ DOCKER_ARCH: '{{.ARCH | default "amd64"}}'
92+ # Generate Docker volume mounts: Go module cache + go.mod replace directives
93+ # Uses wails3 tool docker-mounts for cross-platform compatibility (Windows/Linux/macOS)
94+ DOCKER_MOUNTS:
95+ sh: 'wails3 tool docker-mounts'
96+
97+ package:
98+ summary: Packages the application
99+ cmds:
100+ - task: '{{if eq (.FORMAT | default "nsis") "msix"}}create:msix:package{{else}}create:nsis:installer{{end}}'
101+ vars:
102+ INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}'
103+ vars:
104+ FORMAT: '{{.FORMAT | default "nsis"}}'
105+ INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}'
106+
107+ generate:syso:
108+ summary: Generates Windows `.syso` file
109+ dir: build
110+ cmds:
111+ - wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso
112+ vars:
113+ ARCH: '{{.ARCH | default ARCH}}'
114+
115+ create:nsis:installer:
116+ summary: Creates an NSIS installer
117+ dir: build/windows/nsis
118+ deps:
119+ - task: build
120+ preconditions:
121+ - sh: '[ "{{.INSTALL_SCOPE}}" = "user" ] || [ "{{.INSTALL_SCOPE}}" = "machine" ]'
122+ msg: "INSTALL_SCOPE must be 'user' or 'machine', got '{{.INSTALL_SCOPE}}'"
123+ cmds:
124+ # Create the Microsoft WebView2 bootstrapper if it doesn't exist
125+ - wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis"
126+ - |
127+ {{if eq OS "windows"}}
128+ makensis {{if eq .INSTALL_SCOPE "user"}}-DWAILS_INSTALL_SCOPE=user -DREQUEST_EXECUTION_LEVEL=user {{end}}-DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi
129+ {{else}}
130+ makensis {{if eq .INSTALL_SCOPE "user"}}-DWAILS_INSTALL_SCOPE=user -DREQUEST_EXECUTION_LEVEL=user {{end}}-DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi
131+ {{end}}
132+ vars:
133+ ARCH: '{{.ARCH | default ARCH}}'
134+ ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
135+ INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}'
136+
137+ create:msix:package:
138+ summary: Creates an MSIX package
139+ deps:
140+ - task: build
141+ cmds:
142+ - |-
143+ wails3 tool msix \
144+ --config "{{.ROOT_DIR}}/build/config.yml" \
145+ --name "{{.APP_NAME}}.exe" \
146+ --executable "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" \
147+ --arch "{{.ARCH}}" \
148+ --out "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}-{{.ARCH}}.msix" \
149+ {{if .CERT_PATH}}--cert "{{.CERT_PATH}}"{{end}} \
150+ {{if .PUBLISHER}}--publisher "{{.PUBLISHER}}"{{end}} \
151+ {{if eq .USE_MSIX_TOOL "true"}}--use-msix-tool{{else}}--use-makeappx{{end}}
152+ vars:
153+ ARCH: '{{.ARCH | default ARCH}}'
154+ CERT_PATH: '{{.CERT_PATH | default ""}}'
155+ PUBLISHER: '{{.PUBLISHER | default ""}}'
156+ USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}'
157+
158+ install:msix:tools:
159+ summary: Installs tools required for MSIX packaging
160+ cmds:
161+ - wails3 tool msix-install-tools
162+
163+ run:
164+ cmds:
165+ - '{{.BIN_DIR}}/{{.APP_NAME}}.exe'
166+
167+ sign:
168+ summary: Signs the Windows executable
169+ desc: |
170+ Signs the .exe with an Authenticode certificate.
171+ Set SIGN_CERTIFICATE or SIGN_THUMBPRINT to override the certificate
172+ configured globally via `wails3 setup` (~/.config/wails/defaults.yaml).
173+ Password is retrieved from system keychain (run: wails3 setup signing)
174+ deps:
175+ - task: build
176+ cmds:
177+ # Certificate vars are optional: if unset, `wails3 tool sign` falls back to
178+ # the certificate configured globally via `wails3 setup`.
179+ - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.exe" {{if .SIGN_CERTIFICATE}}--certificate "{{.SIGN_CERTIFICATE}}"{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint "{{.SIGN_THUMBPRINT}}"{{end}} {{if .TIMESTAMP_SERVER}}--timestamp "{{.TIMESTAMP_SERVER}}"{{end}}
180+
181+ sign:installer:
182+ summary: Signs the NSIS installer
183+ desc: |
184+ Creates and signs the NSIS installer.
185+ Set SIGN_CERTIFICATE or SIGN_THUMBPRINT to override the certificate
186+ configured globally via `wails3 setup` (~/.config/wails/defaults.yaml).
187+ Password is retrieved from system keychain (run: wails3 setup signing)
188+ deps:
189+ - task: create:nsis:installer
190+ vars:
191+ INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}'
192+ vars:
193+ INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}'
194+ cmds:
195+ - wails3 tool sign --input "build/windows/nsis/{{.APP_NAME}}-installer.exe" {{if .SIGN_CERTIFICATE}}--certificate "{{.SIGN_CERTIFICATE}}"{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint "{{.SIGN_THUMBPRINT}}"{{end}} {{if .TIMESTAMP_SERVER}}--timestamp "{{.TIMESTAMP_SERVER}}"{{end}}
added build/windows/icon.ico +0 -0
new file mode 100644
Binary files /dev/null and b/build/windows/icon.ico differ
new file mode 100644
Binary files /dev/null and b/build/windows/icon.ico differBinary files /dev/null and b/build/windows/icon.ico differ
added build/windows/info.json +15 -0
new file mode 100644
@@ -0,0 +1,15 @@
1+{
2+ "fixed": {
3+ "file_version": "0.0.1"
4+ },
5+ "info": {
6+ "0409": {
7+ "ProductVersion": "0.0.1",
8+ "CompanyName": "Philippe Charrière",
9+ "FileDescription": "Converse with AI agents in channels",
10+ "LegalCopyright": "© 2026 Philippe Charrière",
11+ "ProductName": "Sidekicks",
12+ "Comments": "This is a comment"
13+ }
14+ }
15+}
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,15 @@
1+{
2+ "fixed": {
3+ "file_version": "0.0.1"
4+ },
5+ "info": {
6+ "0409": {
7+ "ProductVersion": "0.0.1",
8+ "CompanyName": "Philippe Charrière",
9+ "FileDescription": "Converse with AI agents in channels",
10+ "LegalCopyright": "© 2026 Philippe Charrière",
11+ "ProductName": "Sidekicks",
12+ "Comments": "This is a comment"
13+ }
14+ }
15+}
\ No newline at end of file\ No newline at end of file
added build/windows/msix/app_manifest.xml +55 -0
new file mode 100644
@@ -0,0 +1,55 @@
1+<?xml version="1.0" encoding="utf-8"?>
2+<Package
3+ xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
4+ xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
5+ xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
6+ xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
7+ xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
8+ IgnorableNamespaces="uap3">
9+
10+ <Identity
11+ Name="com.k33g.sidekicks"
12+ Publisher="CN=Philippe Charrière"
13+ Version="0.0.1.0"
14+ ProcessorArchitecture="arm64" />
15+
16+ <Properties>
17+ <DisplayName>Sidekicks</DisplayName>
18+ <PublisherDisplayName>Philippe Charrière</PublisherDisplayName>
19+ <Description>Converse with AI agents in channels</Description>
20+ <Logo>Assets\StoreLogo.png</Logo>
21+ </Properties>
22+
23+ <Dependencies>
24+ <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
25+ </Dependencies>
26+
27+ <Resources>
28+ <Resource Language="en-us" />
29+ </Resources>
30+
31+ <Applications>
32+ <Application Id="com.k33g.sidekicks" Executable="sidekicks" EntryPoint="Windows.FullTrustApplication">
33+ <uap:VisualElements
34+ DisplayName="Sidekicks"
35+ Description="Converse with AI agents in channels"
36+ BackgroundColor="transparent"
37+ Square150x150Logo="Assets\Square150x150Logo.png"
38+ Square44x44Logo="Assets\Square44x44Logo.png">
39+ <uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
40+ <uap:SplashScreen Image="Assets\SplashScreen.png" />
41+ </uap:VisualElements>
42+
43+ <Extensions>
44+ <desktop:Extension Category="windows.fullTrustProcess" Executable="sidekicks" />
45+
46+
47+ </Extensions>
48+ </Application>
49+ </Applications>
50+
51+ <Capabilities>
52+ <rescap:Capability Name="runFullTrust" />
53+
54+ </Capabilities>
55+</Package>
new file mode 100644
@@ -0,0 +1,55 @@
1+<?xml version="1.0" encoding="utf-8"?>
2+<Package
3+ xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
4+ xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
5+ xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
6+ xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
7+ xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
8+ IgnorableNamespaces="uap3">
9+
10+ <Identity
11+ Name="com.k33g.sidekicks"
12+ Publisher="CN=Philippe Charrière"
13+ Version="0.0.1.0"
14+ ProcessorArchitecture="arm64" />
15+
16+ <Properties>
17+ <DisplayName>Sidekicks</DisplayName>
18+ <PublisherDisplayName>Philippe Charrière</PublisherDisplayName>
19+ <Description>Converse with AI agents in channels</Description>
20+ <Logo>Assets\StoreLogo.png</Logo>
21+ </Properties>
22+
23+ <Dependencies>
24+ <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
25+ </Dependencies>
26+
27+ <Resources>
28+ <Resource Language="en-us" />
29+ </Resources>
30+
31+ <Applications>
32+ <Application Id="com.k33g.sidekicks" Executable="sidekicks" EntryPoint="Windows.FullTrustApplication">
33+ <uap:VisualElements
34+ DisplayName="Sidekicks"
35+ Description="Converse with AI agents in channels"
36+ BackgroundColor="transparent"
37+ Square150x150Logo="Assets\Square150x150Logo.png"
38+ Square44x44Logo="Assets\Square44x44Logo.png">
39+ <uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
40+ <uap:SplashScreen Image="Assets\SplashScreen.png" />
41+ </uap:VisualElements>
42+
43+ <Extensions>
44+ <desktop:Extension Category="windows.fullTrustProcess" Executable="sidekicks" />
45+
46+
47+ </Extensions>
48+ </Application>
49+ </Applications>
50+
51+ <Capabilities>
52+ <rescap:Capability Name="runFullTrust" />
53+
54+ </Capabilities>
55+</Package>
added build/windows/msix/template.xml +54 -0
new file mode 100644
@@ -0,0 +1,54 @@
1+<?xml version="1.0" encoding="utf-8"?>
2+<MsixPackagingToolTemplate
3+ xmlns="http://schemas.microsoft.com/msix/packaging/msixpackagingtool/template/2022">
4+ <Settings
5+ AllowTelemetry="false"
6+ ApplyACLsToPackageFiles="true"
7+ GenerateCommandLineFile="true"
8+ AllowPromptForPassword="false">
9+ </Settings>
10+ <Installer
11+ Path="sidekicks"
12+ Arguments=""
13+ InstallLocation="C:\Program Files\Philippe Charrière\Sidekicks">
14+ </Installer>
15+ <PackageInformation
16+ PackageName="Sidekicks"
17+ PackageDisplayName="Sidekicks"
18+ PublisherName="CN=Philippe Charrière"
19+ PublisherDisplayName="Philippe Charrière"
20+ Version="0.0.1.0"
21+ PackageDescription="Converse with AI agents in channels">
22+ <Capabilities>
23+ <Capability Name="runFullTrust" />
24+
25+ </Capabilities>
26+ <Applications>
27+ <Application
28+ Id="com.k33g.sidekicks"
29+ Description="Converse with AI agents in channels"
30+ DisplayName="Sidekicks"
31+ ExecutableName="sidekicks"
32+ EntryPoint="Windows.FullTrustApplication">
33+
34+ </Application>
35+ </Applications>
36+ <Resources>
37+ <Resource Language="en-us" />
38+ </Resources>
39+ <Dependencies>
40+ <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
41+ </Dependencies>
42+ <Properties>
43+ <Framework>false</Framework>
44+ <DisplayName>Sidekicks</DisplayName>
45+ <PublisherDisplayName>Philippe Charrière</PublisherDisplayName>
46+ <Description>Converse with AI agents in channels</Description>
47+ <Logo>Assets\AppIcon.png</Logo>
48+ </Properties>
49+ </PackageInformation>
50+ <SaveLocation PackagePath="sidekicks.msix" />
51+ <PackageIntegrity>
52+ <CertificatePath></CertificatePath>
53+ </PackageIntegrity>
54+</MsixPackagingToolTemplate>
new file mode 100644
@@ -0,0 +1,54 @@
1+<?xml version="1.0" encoding="utf-8"?>
2+<MsixPackagingToolTemplate
3+ xmlns="http://schemas.microsoft.com/msix/packaging/msixpackagingtool/template/2022">
4+ <Settings
5+ AllowTelemetry="false"
6+ ApplyACLsToPackageFiles="true"
7+ GenerateCommandLineFile="true"
8+ AllowPromptForPassword="false">
9+ </Settings>
10+ <Installer
11+ Path="sidekicks"
12+ Arguments=""
13+ InstallLocation="C:\Program Files\Philippe Charrière\Sidekicks">
14+ </Installer>
15+ <PackageInformation
16+ PackageName="Sidekicks"
17+ PackageDisplayName="Sidekicks"
18+ PublisherName="CN=Philippe Charrière"
19+ PublisherDisplayName="Philippe Charrière"
20+ Version="0.0.1.0"
21+ PackageDescription="Converse with AI agents in channels">
22+ <Capabilities>
23+ <Capability Name="runFullTrust" />
24+
25+ </Capabilities>
26+ <Applications>
27+ <Application
28+ Id="com.k33g.sidekicks"
29+ Description="Converse with AI agents in channels"
30+ DisplayName="Sidekicks"
31+ ExecutableName="sidekicks"
32+ EntryPoint="Windows.FullTrustApplication">
33+
34+ </Application>
35+ </Applications>
36+ <Resources>
37+ <Resource Language="en-us" />
38+ </Resources>
39+ <Dependencies>
40+ <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
41+ </Dependencies>
42+ <Properties>
43+ <Framework>false</Framework>
44+ <DisplayName>Sidekicks</DisplayName>
45+ <PublisherDisplayName>Philippe Charrière</PublisherDisplayName>
46+ <Description>Converse with AI agents in channels</Description>
47+ <Logo>Assets\AppIcon.png</Logo>
48+ </Properties>
49+ </PackageInformation>
50+ <SaveLocation PackagePath="sidekicks.msix" />
51+ <PackageIntegrity>
52+ <CertificatePath></CertificatePath>
53+ </PackageIntegrity>
54+</MsixPackagingToolTemplate>
added build/windows/nsis/project.nsi +119 -0
new file mode 100644
@@ -0,0 +1,119 @@
1+Unicode true
2+
3+####
4+## Please note: Template replacements don't work in this file. They are provided with default defines like
5+## mentioned underneath.
6+## If the keyword is not defined, "wails_tools.nsh" will populate them.
7+## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually
8+## from outside of Wails for debugging and development of the installer.
9+##
10+## For development first make a wails nsis build to populate the "wails_tools.nsh":
11+## > wails build --target windows/amd64 --nsis
12+## Then you can call makensis on this file with specifying the path to your binary:
13+## For a AMD64 only installer:
14+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
15+## For a ARM64 only installer:
16+## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
17+## For a installer with both architectures:
18+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
19+####
20+## The following information is taken from the wails_tools.nsh file, but they can be overwritten here.
21+####
22+## !define INFO_PROJECTNAME "my-project" # Default "sidekicks"
23+## !define INFO_COMPANYNAME "My Company" # Default "Philippe Charrière"
24+## !define INFO_PRODUCTNAME "My Product Name" # Default "Sidekicks"
25+## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.0.1"
26+## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© 2026 Philippe Charrière"
27+###
28+## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
29+## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
30+####
31+## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
32+## !define WAILS_INSTALL_SCOPE "user" # Default "machine" - set to "user" for per-user install ($LOCALAPPDATA) without UAC prompt
33+####
34+## Include the wails tools
35+####
36+!include "wails_tools.nsh"
37+
38+# The version information for this two must consist of 4 parts
39+VIProductVersion "${INFO_PRODUCTVERSION}.0"
40+VIFileVersion "${INFO_PRODUCTVERSION}.0"
41+
42+VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
43+VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
44+VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
45+VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
46+VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
47+VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
48+
49+# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
50+ManifestDPIAware true
51+
52+!include "MUI.nsh"
53+
54+!define MUI_ICON "..\icon.ico"
55+!define MUI_UNICON "..\icon.ico"
56+# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
57+!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
58+!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
59+
60+!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
61+# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
62+!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
63+!insertmacro MUI_PAGE_INSTFILES # Installing page.
64+!insertmacro MUI_PAGE_FINISH # Finished installation page.
65+
66+!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page
67+
68+!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
69+
70+## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
71+#!uninstfinalize 'signtool --file "%1"'
72+#!finalize 'signtool --file "%1"'
73+
74+Name "${INFO_PRODUCTNAME}"
75+OutFile "..\..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
76+!if "${WAILS_INSTALL_SCOPE}" == "user"
77+ InstallDir "$LOCALAPPDATA\Programs\${INFO_PRODUCTNAME}"
78+!else
79+ InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}"
80+!endif
81+ShowInstDetails show # This will always show the installation details.
82+
83+Function .onInit
84+ !insertmacro wails.checkArchitecture
85+FunctionEnd
86+
87+Section
88+ !insertmacro wails.setShellContext
89+
90+ !insertmacro wails.webview2runtime
91+
92+ SetOutPath $INSTDIR
93+
94+ !insertmacro wails.files
95+
96+ CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
97+ CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
98+
99+ !insertmacro wails.associateFiles
100+ !insertmacro wails.associateCustomProtocols
101+
102+ !insertmacro wails.writeUninstaller
103+SectionEnd
104+
105+Section "uninstall"
106+ !insertmacro wails.setShellContext
107+
108+ RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
109+
110+ RMDir /r $INSTDIR
111+
112+ Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
113+ Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
114+
115+ !insertmacro wails.unassociateFiles
116+ !insertmacro wails.unassociateCustomProtocols
117+
118+ !insertmacro wails.deleteUninstaller
119+SectionEnd
new file mode 100644
@@ -0,0 +1,119 @@
1+Unicode true
2+
3+####
4+## Please note: Template replacements don't work in this file. They are provided with default defines like
5+## mentioned underneath.
6+## If the keyword is not defined, "wails_tools.nsh" will populate them.
7+## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually
8+## from outside of Wails for debugging and development of the installer.
9+##
10+## For development first make a wails nsis build to populate the "wails_tools.nsh":
11+## > wails build --target windows/amd64 --nsis
12+## Then you can call makensis on this file with specifying the path to your binary:
13+## For a AMD64 only installer:
14+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
15+## For a ARM64 only installer:
16+## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
17+## For a installer with both architectures:
18+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
19+####
20+## The following information is taken from the wails_tools.nsh file, but they can be overwritten here.
21+####
22+## !define INFO_PROJECTNAME "my-project" # Default "sidekicks"
23+## !define INFO_COMPANYNAME "My Company" # Default "Philippe Charrière"
24+## !define INFO_PRODUCTNAME "My Product Name" # Default "Sidekicks"
25+## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.0.1"
26+## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© 2026 Philippe Charrière"
27+###
28+## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
29+## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
30+####
31+## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
32+## !define WAILS_INSTALL_SCOPE "user" # Default "machine" - set to "user" for per-user install ($LOCALAPPDATA) without UAC prompt
33+####
34+## Include the wails tools
35+####
36+!include "wails_tools.nsh"
37+
38+# The version information for this two must consist of 4 parts
39+VIProductVersion "${INFO_PRODUCTVERSION}.0"
40+VIFileVersion "${INFO_PRODUCTVERSION}.0"
41+
42+VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
43+VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
44+VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
45+VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
46+VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
47+VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
48+
49+# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
50+ManifestDPIAware true
51+
52+!include "MUI.nsh"
53+
54+!define MUI_ICON "..\icon.ico"
55+!define MUI_UNICON "..\icon.ico"
56+# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
57+!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
58+!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
59+
60+!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
61+# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
62+!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
63+!insertmacro MUI_PAGE_INSTFILES # Installing page.
64+!insertmacro MUI_PAGE_FINISH # Finished installation page.
65+
66+!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page
67+
68+!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
69+
70+## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
71+#!uninstfinalize 'signtool --file "%1"'
72+#!finalize 'signtool --file "%1"'
73+
74+Name "${INFO_PRODUCTNAME}"
75+OutFile "..\..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
76+!if "${WAILS_INSTALL_SCOPE}" == "user"
77+ InstallDir "$LOCALAPPDATA\Programs\${INFO_PRODUCTNAME}"
78+!else
79+ InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}"
80+!endif
81+ShowInstDetails show # This will always show the installation details.
82+
83+Function .onInit
84+ !insertmacro wails.checkArchitecture
85+FunctionEnd
86+
87+Section
88+ !insertmacro wails.setShellContext
89+
90+ !insertmacro wails.webview2runtime
91+
92+ SetOutPath $INSTDIR
93+
94+ !insertmacro wails.files
95+
96+ CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
97+ CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
98+
99+ !insertmacro wails.associateFiles
100+ !insertmacro wails.associateCustomProtocols
101+
102+ !insertmacro wails.writeUninstaller
103+SectionEnd
104+
105+Section "uninstall"
106+ !insertmacro wails.setShellContext
107+
108+ RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
109+
110+ RMDir /r $INSTDIR
111+
112+ Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
113+ Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
114+
115+ !insertmacro wails.unassociateFiles
116+ !insertmacro wails.unassociateCustomProtocols
117+
118+ !insertmacro wails.deleteUninstaller
119+SectionEnd
added build/windows/nsis/wails_tools.nsh +261 -0
new file mode 100644
@@ -0,0 +1,261 @@
1+# DO NOT EDIT - Generated automatically by `wails build`
2+
3+!include "x64.nsh"
4+!include "WinVer.nsh"
5+!include "FileFunc.nsh"
6+
7+!ifndef INFO_PROJECTNAME
8+ !define INFO_PROJECTNAME "sidekicks"
9+!endif
10+!ifndef INFO_COMPANYNAME
11+ !define INFO_COMPANYNAME "Philippe Charrière"
12+!endif
13+!ifndef INFO_PRODUCTNAME
14+ !define INFO_PRODUCTNAME "Sidekicks"
15+!endif
16+!ifndef INFO_PRODUCTVERSION
17+ !define INFO_PRODUCTVERSION "0.0.1"
18+!endif
19+!ifndef INFO_COPYRIGHT
20+ !define INFO_COPYRIGHT "© 2026 Philippe Charrière"
21+!endif
22+!ifndef PRODUCT_EXECUTABLE
23+ !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
24+!endif
25+!ifndef UNINST_KEY_NAME
26+ !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
27+!endif
28+!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
29+
30+!ifndef WAILS_INSTALL_SCOPE
31+ !define WAILS_INSTALL_SCOPE "machine"
32+!endif
33+
34+!ifndef REQUEST_EXECUTION_LEVEL
35+ !if "${WAILS_INSTALL_SCOPE}" == "user"
36+ !define REQUEST_EXECUTION_LEVEL "user"
37+ !else
38+ !define REQUEST_EXECUTION_LEVEL "admin"
39+ !endif
40+!endif
41+
42+RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
43+
44+!ifdef ARG_WAILS_AMD64_BINARY
45+ !define SUPPORTS_AMD64
46+!endif
47+
48+!ifdef ARG_WAILS_ARM64_BINARY
49+ !define SUPPORTS_ARM64
50+!endif
51+
52+!ifdef SUPPORTS_AMD64
53+ !ifdef SUPPORTS_ARM64
54+ !define ARCH "amd64_arm64"
55+ !else
56+ !define ARCH "amd64"
57+ !endif
58+!else
59+ !ifdef SUPPORTS_ARM64
60+ !define ARCH "arm64"
61+ !else
62+ !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
63+ !endif
64+!endif
65+
66+!macro wails.checkArchitecture
67+ !ifndef WAILS_WIN10_REQUIRED
68+ !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
69+ !endif
70+
71+ !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
72+ !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
73+ !endif
74+
75+ ${If} ${AtLeastWin10}
76+ !ifdef SUPPORTS_AMD64
77+ ${if} ${IsNativeAMD64}
78+ Goto ok
79+ ${EndIf}
80+ !endif
81+
82+ !ifdef SUPPORTS_ARM64
83+ ${if} ${IsNativeARM64}
84+ Goto ok
85+ ${EndIf}
86+ !endif
87+
88+ IfSilent silentArch notSilentArch
89+ silentArch:
90+ SetErrorLevel 65
91+ Abort
92+ notSilentArch:
93+ MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
94+ Quit
95+ ${else}
96+ IfSilent silentWin notSilentWin
97+ silentWin:
98+ SetErrorLevel 64
99+ Abort
100+ notSilentWin:
101+ MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
102+ Quit
103+ ${EndIf}
104+
105+ ok:
106+!macroend
107+
108+!macro wails.files
109+ !ifdef SUPPORTS_AMD64
110+ ${if} ${IsNativeAMD64}
111+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
112+ ${EndIf}
113+ !endif
114+
115+ !ifdef SUPPORTS_ARM64
116+ ${if} ${IsNativeARM64}
117+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
118+ ${EndIf}
119+ !endif
120+!macroend
121+
122+!macro wails.writeUninstaller
123+ WriteUninstaller "$INSTDIR\uninstall.exe"
124+
125+ SetRegView 64
126+ !if "${WAILS_INSTALL_SCOPE}" == "user"
127+ WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
128+ WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
129+ WriteRegStr HKCU "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
130+ WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
131+ WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
132+ WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
133+
134+ ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
135+ IntFmt $0 "0x%08X" $0
136+ WriteRegDWORD HKCU "${UNINST_KEY}" "EstimatedSize" "$0"
137+ !else
138+ WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
139+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
140+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
141+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
142+ WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
143+ WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
144+
145+ ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
146+ IntFmt $0 "0x%08X" $0
147+ WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
148+ !endif
149+!macroend
150+
151+!macro wails.deleteUninstaller
152+ Delete "$INSTDIR\uninstall.exe"
153+
154+ SetRegView 64
155+ !if "${WAILS_INSTALL_SCOPE}" == "user"
156+ DeleteRegKey HKCU "${UNINST_KEY}"
157+ !else
158+ DeleteRegKey HKLM "${UNINST_KEY}"
159+ !endif
160+!macroend
161+
162+!macro wails.setShellContext
163+ ${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
164+ SetShellVarContext all
165+ ${else}
166+ SetShellVarContext current
167+ ${EndIf}
168+!macroend
169+
170+# Install webview2 by launching the bootstrapper
171+# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
172+!macro wails.webview2runtime
173+ !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
174+ !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
175+ !endif
176+
177+ SetRegView 64
178+ # If the admin key exists and is not empty then webview2 is already installed
179+ ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
180+ ${If} $0 != ""
181+ Goto ok
182+ ${EndIf}
183+
184+ ${If} ${REQUEST_EXECUTION_LEVEL} == "user"
185+ # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
186+ ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
187+ ${If} $0 != ""
188+ Goto ok
189+ ${EndIf}
190+ ${EndIf}
191+
192+ SetDetailsPrint both
193+ DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
194+ SetDetailsPrint listonly
195+
196+ InitPluginsDir
197+ CreateDirectory "$pluginsdir\webview2bootstrapper"
198+ SetOutPath "$pluginsdir\webview2bootstrapper"
199+ File "MicrosoftEdgeWebview2Setup.exe"
200+ ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
201+
202+ SetDetailsPrint both
203+ ok:
204+!macroend
205+
206+# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
207+!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
208+ ; Backup the previously associated file class
209+ ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
210+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
211+
212+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
213+
214+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
215+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
216+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
217+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
218+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
219+!macroend
220+
221+!macro APP_UNASSOCIATE EXT FILECLASS
222+ ; Backup the previously associated file class
223+ ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
224+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
225+
226+ DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
227+!macroend
228+
229+!macro wails.associateFiles
230+ ; Create file associations
231+
232+!macroend
233+
234+!macro wails.unassociateFiles
235+ ; Delete app associations
236+
237+!macroend
238+
239+!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
240+ DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
241+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
242+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
243+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
244+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
245+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
246+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
247+!macroend
248+
249+!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
250+ DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
251+!macroend
252+
253+!macro wails.associateCustomProtocols
254+ ; Create custom protocols associations
255+
256+!macroend
257+
258+!macro wails.unassociateCustomProtocols
259+ ; Delete app custom protocol associations
260+
261+!macroend
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,261 @@
1+# DO NOT EDIT - Generated automatically by `wails build`
2+
3+!include "x64.nsh"
4+!include "WinVer.nsh"
5+!include "FileFunc.nsh"
6+
7+!ifndef INFO_PROJECTNAME
8+ !define INFO_PROJECTNAME "sidekicks"
9+!endif
10+!ifndef INFO_COMPANYNAME
11+ !define INFO_COMPANYNAME "Philippe Charrière"
12+!endif
13+!ifndef INFO_PRODUCTNAME
14+ !define INFO_PRODUCTNAME "Sidekicks"
15+!endif
16+!ifndef INFO_PRODUCTVERSION
17+ !define INFO_PRODUCTVERSION "0.0.1"
18+!endif
19+!ifndef INFO_COPYRIGHT
20+ !define INFO_COPYRIGHT "© 2026 Philippe Charrière"
21+!endif
22+!ifndef PRODUCT_EXECUTABLE
23+ !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
24+!endif
25+!ifndef UNINST_KEY_NAME
26+ !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
27+!endif
28+!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
29+
30+!ifndef WAILS_INSTALL_SCOPE
31+ !define WAILS_INSTALL_SCOPE "machine"
32+!endif
33+
34+!ifndef REQUEST_EXECUTION_LEVEL
35+ !if "${WAILS_INSTALL_SCOPE}" == "user"
36+ !define REQUEST_EXECUTION_LEVEL "user"
37+ !else
38+ !define REQUEST_EXECUTION_LEVEL "admin"
39+ !endif
40+!endif
41+
42+RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
43+
44+!ifdef ARG_WAILS_AMD64_BINARY
45+ !define SUPPORTS_AMD64
46+!endif
47+
48+!ifdef ARG_WAILS_ARM64_BINARY
49+ !define SUPPORTS_ARM64
50+!endif
51+
52+!ifdef SUPPORTS_AMD64
53+ !ifdef SUPPORTS_ARM64
54+ !define ARCH "amd64_arm64"
55+ !else
56+ !define ARCH "amd64"
57+ !endif
58+!else
59+ !ifdef SUPPORTS_ARM64
60+ !define ARCH "arm64"
61+ !else
62+ !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
63+ !endif
64+!endif
65+
66+!macro wails.checkArchitecture
67+ !ifndef WAILS_WIN10_REQUIRED
68+ !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
69+ !endif
70+
71+ !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
72+ !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
73+ !endif
74+
75+ ${If} ${AtLeastWin10}
76+ !ifdef SUPPORTS_AMD64
77+ ${if} ${IsNativeAMD64}
78+ Goto ok
79+ ${EndIf}
80+ !endif
81+
82+ !ifdef SUPPORTS_ARM64
83+ ${if} ${IsNativeARM64}
84+ Goto ok
85+ ${EndIf}
86+ !endif
87+
88+ IfSilent silentArch notSilentArch
89+ silentArch:
90+ SetErrorLevel 65
91+ Abort
92+ notSilentArch:
93+ MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
94+ Quit
95+ ${else}
96+ IfSilent silentWin notSilentWin
97+ silentWin:
98+ SetErrorLevel 64
99+ Abort
100+ notSilentWin:
101+ MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
102+ Quit
103+ ${EndIf}
104+
105+ ok:
106+!macroend
107+
108+!macro wails.files
109+ !ifdef SUPPORTS_AMD64
110+ ${if} ${IsNativeAMD64}
111+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
112+ ${EndIf}
113+ !endif
114+
115+ !ifdef SUPPORTS_ARM64
116+ ${if} ${IsNativeARM64}
117+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
118+ ${EndIf}
119+ !endif
120+!macroend
121+
122+!macro wails.writeUninstaller
123+ WriteUninstaller "$INSTDIR\uninstall.exe"
124+
125+ SetRegView 64
126+ !if "${WAILS_INSTALL_SCOPE}" == "user"
127+ WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
128+ WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
129+ WriteRegStr HKCU "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
130+ WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
131+ WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
132+ WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
133+
134+ ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
135+ IntFmt $0 "0x%08X" $0
136+ WriteRegDWORD HKCU "${UNINST_KEY}" "EstimatedSize" "$0"
137+ !else
138+ WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
139+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
140+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
141+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
142+ WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
143+ WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
144+
145+ ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
146+ IntFmt $0 "0x%08X" $0
147+ WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
148+ !endif
149+!macroend
150+
151+!macro wails.deleteUninstaller
152+ Delete "$INSTDIR\uninstall.exe"
153+
154+ SetRegView 64
155+ !if "${WAILS_INSTALL_SCOPE}" == "user"
156+ DeleteRegKey HKCU "${UNINST_KEY}"
157+ !else
158+ DeleteRegKey HKLM "${UNINST_KEY}"
159+ !endif
160+!macroend
161+
162+!macro wails.setShellContext
163+ ${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
164+ SetShellVarContext all
165+ ${else}
166+ SetShellVarContext current
167+ ${EndIf}
168+!macroend
169+
170+# Install webview2 by launching the bootstrapper
171+# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
172+!macro wails.webview2runtime
173+ !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
174+ !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
175+ !endif
176+
177+ SetRegView 64
178+ # If the admin key exists and is not empty then webview2 is already installed
179+ ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
180+ ${If} $0 != ""
181+ Goto ok
182+ ${EndIf}
183+
184+ ${If} ${REQUEST_EXECUTION_LEVEL} == "user"
185+ # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
186+ ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
187+ ${If} $0 != ""
188+ Goto ok
189+ ${EndIf}
190+ ${EndIf}
191+
192+ SetDetailsPrint both
193+ DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
194+ SetDetailsPrint listonly
195+
196+ InitPluginsDir
197+ CreateDirectory "$pluginsdir\webview2bootstrapper"
198+ SetOutPath "$pluginsdir\webview2bootstrapper"
199+ File "MicrosoftEdgeWebview2Setup.exe"
200+ ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
201+
202+ SetDetailsPrint both
203+ ok:
204+!macroend
205+
206+# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
207+!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
208+ ; Backup the previously associated file class
209+ ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
210+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
211+
212+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
213+
214+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
215+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
216+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
217+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
218+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
219+!macroend
220+
221+!macro APP_UNASSOCIATE EXT FILECLASS
222+ ; Backup the previously associated file class
223+ ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
224+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
225+
226+ DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
227+!macroend
228+
229+!macro wails.associateFiles
230+ ; Create file associations
231+
232+!macroend
233+
234+!macro wails.unassociateFiles
235+ ; Delete app associations
236+
237+!macroend
238+
239+!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
240+ DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
241+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
242+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
243+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
244+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
245+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
246+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
247+!macroend
248+
249+!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
250+ DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
251+!macroend
252+
253+!macro wails.associateCustomProtocols
254+ ; Create custom protocols associations
255+
256+!macroend
257+
258+!macro wails.unassociateCustomProtocols
259+ ; Delete app custom protocol associations
260+
261+!macroend
\ No newline at end of file\ No newline at end of file
added build/windows/wails.exe.manifest +22 -0
new file mode 100644
@@ -0,0 +1,22 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
3+ <assemblyIdentity type="win32" name="com.k33g.sidekicks" version="0.0.1" processorArchitecture="*"/>
4+ <dependency>
5+ <dependentAssembly>
6+ <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
7+ </dependentAssembly>
8+ </dependency>
9+ <asmv3:application>
10+ <asmv3:windowsSettings>
11+ <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
12+ <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
13+ </asmv3:windowsSettings>
14+ </asmv3:application>
15+ <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
16+ <security>
17+ <requestedPrivileges>
18+ <requestedExecutionLevel level="asInvoker" uiAccess="false"/>
19+ </requestedPrivileges>
20+ </security>
21+ </trustInfo>
22+</assembly>
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,22 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
3+ <assemblyIdentity type="win32" name="com.k33g.sidekicks" version="0.0.1" processorArchitecture="*"/>
4+ <dependency>
5+ <dependentAssembly>
6+ <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
7+ </dependentAssembly>
8+ </dependency>
9+ <asmv3:application>
10+ <asmv3:windowsSettings>
11+ <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
12+ <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
13+ </asmv3:windowsSettings>
14+ </asmv3:application>
15+ <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
16+ <security>
17+ <requestedPrivileges>
18+ <requestedExecutionLevel level="asInvoker" uiAccess="false"/>
19+ </requestedPrivileges>
20+ </security>
21+ </trustInfo>
22+</assembly>
\ No newline at end of file\ No newline at end of file
modified docs/adr/0003-architecture.md +14 -5
@@ -1,6 +1,6 @@
11 # ADR 0003 — Architecture, data model and events
22
3-- **Status**: accepted
3+- **Status**: accepted — amended on 2026-10-29 by milestone M1 (see "Amendment 1")
44 - **Date**: 2026-10-29
55 - **Issue**: [#5 — Définir l'architecture et le modèle de données](../../.tickets/issues/0005-definir-l-architecture-et-le-modele-de-donnees.yaml)
66 - **Builds on**: [ADR 0001 — ACP](0001-acp.md), [ADR 0002 — UI stack](0002-ui-stack.md), [configuration spec](../en/reference/configuration.md)
@@ -13,12 +13,13 @@ Several features overlap: an agent process serves several channels, the same ACP
1313
1414 ![Packages and dependencies](../diagrams/packages.png)
1515
16-Source: [docs/diagrams/packages.drawio](../diagrams/packages.drawio). On 2026-10-29 only `schema` exists; the others are the target of M1–M4 and the diagram is updated whenever a package is added, removed or re-wired.
16+Source: [docs/diagrams/packages.drawio](../diagrams/packages.drawio). Blue boxes exist (end of M1), grey ones are planned; the diagram is updated whenever a package is added, removed or re-wired.
1717
1818 | Package | Responsibility | Depends on |
1919 | --- | --- | --- |
20-| `cmd/sidekicks` | `main`: flags (`--config`), wiring, `application.Run()`. No logic. | `internal/app`, Wails |
21-| `internal/app` | The Wails services bound to the frontend (`ChannelService`, `AgentService`, `SettingsService`, `WorkspaceService`) and the events sent to it. Translates between UI calls and the domain; **the only package the UI sees**. | domain packages, Wails |
20+| `main.go` (repository root) | `main`: flags (`--config`), Wails application, services, events registration, window. No logic. | `internal/app`, `internal/events`, Wails |
21+| `internal/app` | The services bound to the frontend (`AgentService`, `ConfigService` in M1; `ChannelService`, `WorkspaceService` later) and the glue between configuration and agents. Translates between UI calls and the domain; **the only package the UI sees**. Does not import Wails. | domain packages, `internal/events` |
22+| `internal/events` | Event names, payload types and the `Emitter` interface the domain emits through. | — |
2223 | `internal/channels` | Channels, members, messages, routing of a user message to the agents it addresses, `@mention` and `/agent:command` parsing, context re-injection. | `agents`, `store` |
2324 | `internal/agents` | One manager owning every agent process: lazy start, idle stop, `maxRunningAgents`, state machine, stderr logs. Opens ACP sessions on behalf of channels. | `acp`, `config` |
2425 | `internal/acp` | Wraps `coder/acp-go-sdk`: connection, `initialize`, sessions, prompt turns, permission requests. Exposes Sidekicks types; **SDK types never leave it**. | ACP SDK |
@@ -107,8 +108,16 @@ UI ─ ChannelService.Send(channel, "@garfield /idioms Il pleut") ─▶ interna
107108 ─▶ store: Message saved, lastSeenMessageId updated ─▶ message:finished
108109 ```
109110
111+## Amendment 1 — what M1 changed (2026-10-29)
112+
113+- **The main package is at the repository root, not in `cmd/sidekicks`.** The Taskfiles Wails generates build `.` and generate the bindings from it on every platform; moving `main` would mean editing about ten generated commands that `wails3 update build-assets` overwrites. The root package stays a thin `main.go`.
114+- **Only `main.go` imports Wails.** `internal/app` exposes plain Go services and emits through `internal/events.Emitter`; `main.go` binds the services and plugs the Wails event manager in. `internal/app` is therefore tested without a window or cgo.
115+- **`internal/events`** is a new package: the event names and payloads below, the `Emitter` interface, and a `Recorder` for tests.
116+- **When the limit is reached**, starting an agent first stops the least recently used *idle* agent; only when every running agent is busy is the start refused.
117+- **Events of M1**: `config:changed`, `agent:state`, `agent:log`. The others of the catalogue arrive with their features.
118+
110119 ## Consequences
111120
112-- M1 creates `cmd/sidekicks`, `internal/app`, `internal/config`, `internal/acp`, `internal/agents` in this layout (issues #8–#13).
121+- M1 creates `main.go`, `internal/app`, `internal/events`, `internal/config`, `internal/acp`, `internal/agents` (issues #8–#13; see Amendment 1).
113122 - The diagram and this table are part of the definition of done of any change that adds or re-wires a package.
114123 - Pebble v2 (`github.com/cockroachdb/pebble/v2`) is the store; its use is confined to `internal/store`.
@@ -1,6 +1,6 @@
1 # ADR 0003 — Architecture, data model and events1 # ADR 0003 — Architecture, data model and events
2 2
3-- **Status**: accepted3+- **Status**: accepted — amended on 2026-10-29 by milestone M1 (see "Amendment 1")
4 - **Date**: 2026-10-294 - **Date**: 2026-10-29
5 - **Issue**: [#5 — Définir l'architecture et le modèle de données](../../.tickets/issues/0005-definir-l-architecture-et-le-modele-de-donnees.yaml)5 - **Issue**: [#5 — Définir l'architecture et le modèle de données](../../.tickets/issues/0005-definir-l-architecture-et-le-modele-de-donnees.yaml)
6 - **Builds on**: [ADR 0001 — ACP](0001-acp.md), [ADR 0002 — UI stack](0002-ui-stack.md), [configuration spec](../en/reference/configuration.md)6 - **Builds on**: [ADR 0001 — ACP](0001-acp.md), [ADR 0002 — UI stack](0002-ui-stack.md), [configuration spec](../en/reference/configuration.md)
@@ -13,12 +13,13 @@ Several features overlap: an agent process serves several channels, the same ACP
13 13
14 ![Packages and dependencies](../diagrams/packages.png)14 ![Packages and dependencies](../diagrams/packages.png)
15 15
16-Source: [docs/diagrams/packages.drawio](../diagrams/packages.drawio). On 2026-10-29 only `schema` exists; the others are the target of M1–M4 and the diagram is updated whenever a package is added, removed or re-wired.16+Source: [docs/diagrams/packages.drawio](../diagrams/packages.drawio). Blue boxes exist (end of M1), grey ones are planned; the diagram is updated whenever a package is added, removed or re-wired.
17 17
18 | Package | Responsibility | Depends on |18 | Package | Responsibility | Depends on |
19 | --- | --- | --- |19 | --- | --- | --- |
20-| `cmd/sidekicks` | `main`: flags (`--config`), wiring, `application.Run()`. No logic. | `internal/app`, Wails |20+| `main.go` (repository root) | `main`: flags (`--config`), Wails application, services, events registration, window. No logic. | `internal/app`, `internal/events`, Wails |
21-| `internal/app` | The Wails services bound to the frontend (`ChannelService`, `AgentService`, `SettingsService`, `WorkspaceService`) and the events sent to it. Translates between UI calls and the domain; **the only package the UI sees**. | domain packages, Wails |21+| `internal/app` | The services bound to the frontend (`AgentService`, `ConfigService` in M1; `ChannelService`, `WorkspaceService` later) and the glue between configuration and agents. Translates between UI calls and the domain; **the only package the UI sees**. Does not import Wails. | domain packages, `internal/events` |
22+| `internal/events` | Event names, payload types and the `Emitter` interface the domain emits through. | — |
22 | `internal/channels` | Channels, members, messages, routing of a user message to the agents it addresses, `@mention` and `/agent:command` parsing, context re-injection. | `agents`, `store` |23 | `internal/channels` | Channels, members, messages, routing of a user message to the agents it addresses, `@mention` and `/agent:command` parsing, context re-injection. | `agents`, `store` |
23 | `internal/agents` | One manager owning every agent process: lazy start, idle stop, `maxRunningAgents`, state machine, stderr logs. Opens ACP sessions on behalf of channels. | `acp`, `config` |24 | `internal/agents` | One manager owning every agent process: lazy start, idle stop, `maxRunningAgents`, state machine, stderr logs. Opens ACP sessions on behalf of channels. | `acp`, `config` |
24 | `internal/acp` | Wraps `coder/acp-go-sdk`: connection, `initialize`, sessions, prompt turns, permission requests. Exposes Sidekicks types; **SDK types never leave it**. | ACP SDK |25 | `internal/acp` | Wraps `coder/acp-go-sdk`: connection, `initialize`, sessions, prompt turns, permission requests. Exposes Sidekicks types; **SDK types never leave it**. | ACP SDK |
@@ -107,8 +108,16 @@ UI ─ ChannelService.Send(channel, "@garfield /idioms Il pleut") ─▶ interna
107 ─▶ store: Message saved, lastSeenMessageId updated ─▶ message:finished108 ─▶ store: Message saved, lastSeenMessageId updated ─▶ message:finished
108 ```109 ```
109 110
111+## Amendment 1 — what M1 changed (2026-10-29)
112+
113+- **The main package is at the repository root, not in `cmd/sidekicks`.** The Taskfiles Wails generates build `.` and generate the bindings from it on every platform; moving `main` would mean editing about ten generated commands that `wails3 update build-assets` overwrites. The root package stays a thin `main.go`.
114+- **Only `main.go` imports Wails.** `internal/app` exposes plain Go services and emits through `internal/events.Emitter`; `main.go` binds the services and plugs the Wails event manager in. `internal/app` is therefore tested without a window or cgo.
115+- **`internal/events`** is a new package: the event names and payloads below, the `Emitter` interface, and a `Recorder` for tests.
116+- **When the limit is reached**, starting an agent first stops the least recently used *idle* agent; only when every running agent is busy is the start refused.
117+- **Events of M1**: `config:changed`, `agent:state`, `agent:log`. The others of the catalogue arrive with their features.
118+
110 ## Consequences119 ## Consequences
111 120
112-- M1 creates `cmd/sidekicks`, `internal/app`, `internal/config`, `internal/acp`, `internal/agents` in this layout (issues #8–#13).121+- M1 creates `main.go`, `internal/app`, `internal/events`, `internal/config`, `internal/acp`, `internal/agents` (issues #8–#13; see Amendment 1).
113 - The diagram and this table are part of the definition of done of any change that adds or re-wires a package.122 - The diagram and this table are part of the definition of done of any change that adds or re-wires a package.
114 - Pebble v2 (`github.com/cockroachdb/pebble/v2`) is the store; its use is confined to `internal/store`.123 - Pebble v2 (`github.com/cockroachdb/pebble/v2`) is the store; its use is confined to `internal/store`.
modified docs/diagrams/packages.drawio +27 -25
@@ -1,10 +1,10 @@
11 <mxfile host="app.diagrams.net">
22 <diagram id="packages" name="Sidekicks — packages and dependencies">
3- <mxGraphModel dx="1200" dy="780" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1200" pageHeight="780" background="#F5F7FC" math="0" shadow="0">
3+ <mxGraphModel dx="1200" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1200" pageHeight="800" background="#F5F7FC" math="0" shadow="0">
44 <root>
55 <mxCell id="0"/>
66 <mxCell id="1" parent="0"/>
7- <mxCell id="title" value="&lt;b style=&quot;font-size:25px;color:#1A237E&quot;&gt;Sidekicks — packages and dependencies&lt;/b&gt;&lt;br&gt;&lt;i style=&quot;font-size:14px;color:#546E7A&quot;&gt;arrows are imports · the frontend only talks to internal/app · nothing outside internal/acp sees the ACP SDK&lt;/i&gt;" style="text;html=1;align=center;verticalAlign=middle;strokeColor=none;fillColor=none;" vertex="1" parent="1"><mxGeometry x="150.0" y="12" width="900" height="56" as="geometry"/></mxCell>
7+ <mxCell id="title" value="&lt;b style=&quot;font-size:25px;color:#1A237E&quot;&gt;Sidekicks — packages and dependencies&lt;/b&gt;&lt;br&gt;&lt;i style=&quot;font-size:14px;color:#546E7A&quot;&gt;arrows are imports · blue: exists (M1) · grey: planned · only main.go imports Wails&lt;/i&gt;" style="text;html=1;align=center;verticalAlign=middle;strokeColor=none;fillColor=none;" vertex="1" parent="1"><mxGeometry x="150.0" y="12" width="900" height="56" as="geometry"/></mxCell>
88 <mxCell id="c5" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=block;startFill=1;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="290" y="157" as="sourcePoint"/><mxPoint x="475" y="157" as="targetPoint"/></mxGeometry></mxCell>
99 <mxCell id="c7" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="910" y="157" as="sourcePoint"/><mxPoint x="725" y="157" as="targetPoint"/></mxGeometry></mxCell>
1010 <mxCell id="c13" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="600" y="215" as="sourcePoint"/><mxPoint x="165" y="300" as="targetPoint"/><Array as="points"><mxPoint x="600" y="255"/><mxPoint x="165" y="255"/></Array></mxGeometry></mxCell>
@@ -13,32 +13,34 @@
1313 <mxCell id="c16" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="600" y="215" as="sourcePoint"/><mxPoint x="1035" y="300" as="targetPoint"/><Array as="points"><mxPoint x="600" y="255"/><mxPoint x="1035" y="255"/></Array></mxGeometry></mxCell>
1414 <mxCell id="c17" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="290" y="355" as="sourcePoint"/><mxPoint x="330" y="355" as="targetPoint"/></mxGeometry></mxCell>
1515 <mxCell id="c18" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="580" y="355" as="sourcePoint"/><mxPoint x="620" y="355" as="targetPoint"/></mxGeometry></mxCell>
16- <mxCell id="c22" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="165" y="410" as="sourcePoint"/><mxPoint x="165" y="480" as="targetPoint"/></mxGeometry></mxCell>
17- <mxCell id="c23" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="455" y="410" as="sourcePoint"/><mxPoint x="455" y="480" as="targetPoint"/></mxGeometry></mxCell>
18- <mxCell id="c24" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="745" y="410" as="sourcePoint"/><mxPoint x="745" y="480" as="targetPoint"/></mxGeometry></mxCell>
19- <mxCell id="c29" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="165" y="585" as="sourcePoint"/><mxPoint x="165" y="655" as="targetPoint"/></mxGeometry></mxCell>
20- <mxCell id="c30" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="455" y="585" as="sourcePoint"/><mxPoint x="455" y="655" as="targetPoint"/></mxGeometry></mxCell>
21- <mxCell id="c32" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="745" y="585" as="sourcePoint"/><mxPoint x="745" y="655" as="targetPoint"/></mxGeometry></mxCell>
22- <mxCell id="c33" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="1035" y="410" as="sourcePoint"/><mxPoint x="1035" y="655" as="targetPoint"/></mxGeometry></mxCell>
23- <mxCell id="c1" value="💻&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;frontend/&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;Svelte 5 · panels · Markdown&lt;br&gt;CodeMirror · xterm.js&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E3F2FD;gradientColor=#BBDEFB;strokeColor=#1565C0;strokeWidth=2;shadow=1;fontSize=20;fontColor=#0D47A1;" vertex="1" parent="1"><mxGeometry x="40" y="100" width="250" height="115" as="geometry"/></mxCell>
24- <mxCell id="c2" value="🔀&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/app&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;Wails services bound to the UI&lt;br&gt;typed events to the UI&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#F3E5F5;gradientColor=#E1BEE7;strokeColor=#6A1B9A;strokeWidth=3;shadow=1;fontSize=20;fontColor=#4A148C;" vertex="1" parent="1"><mxGeometry x="475" y="100" width="250" height="115" as="geometry"/></mxCell>
16+ <mxCell id="c23" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="165" y="410" as="sourcePoint"/><mxPoint x="165" y="490" as="targetPoint"/></mxGeometry></mxCell>
17+ <mxCell id="c24" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="455" y="410" as="sourcePoint"/><mxPoint x="455" y="490" as="targetPoint"/></mxGeometry></mxCell>
18+ <mxCell id="c25" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="745" y="410" as="sourcePoint"/><mxPoint x="745" y="490" as="targetPoint"/></mxGeometry></mxCell>
19+ <mxCell id="c26" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="560" y="410" as="sourcePoint"/><mxPoint x="1035" y="490" as="targetPoint"/><Array as="points"><mxPoint x="560" y="450"/><mxPoint x="1035" y="450"/></Array></mxGeometry></mxCell>
20+ <mxCell id="c32" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;dashed=1;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="165" y="595" as="sourcePoint"/><mxPoint x="165" y="670" as="targetPoint"/></mxGeometry></mxCell>
21+ <mxCell id="c33" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="455" y="595" as="sourcePoint"/><mxPoint x="455" y="670" as="targetPoint"/></mxGeometry></mxCell>
22+ <mxCell id="c35" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="745" y="595" as="sourcePoint"/><mxPoint x="745" y="670" as="targetPoint"/></mxGeometry></mxCell>
23+ <mxCell id="c1" value="💻&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;frontend/&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;Svelte 5 · panels · state&lt;br&gt;generated TS bindings&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E3F2FD;gradientColor=#BBDEFB;strokeColor=#1565C0;strokeWidth=2;shadow=1;fontSize=20;fontColor=#0D47A1;" vertex="1" parent="1"><mxGeometry x="40" y="100" width="250" height="115" as="geometry"/></mxCell>
24+ <mxCell id="c2" value="🔀&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/app&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;AgentService · ConfigService&lt;br&gt;config ↔ agents glue&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#F3E5F5;gradientColor=#E1BEE7;strokeColor=#6A1B9A;strokeWidth=3;shadow=1;fontSize=20;fontColor=#4A148C;" vertex="1" parent="1"><mxGeometry x="475" y="100" width="250" height="115" as="geometry"/></mxCell>
2525 <mxCell id="c3" value="&lt;b&gt;★ only API of the UI&lt;/b&gt;" style="rounded=1;arcSize=50;html=1;fillColor=#6A1B9A;strokeColor=none;fontColor=#FFFFFF;fontSize=11;" vertex="1" parent="1"><mxGeometry x="577.0" y="88" width="158.0" height="24" as="geometry"/></mxCell>
26- <mxCell id="c4" value="⚙️&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;cmd/sidekicks&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;main: reads flags, wires&lt;br&gt;the services, runs Wails&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=20;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="910" y="100" width="250" height="115" as="geometry"/></mxCell>
26+ <mxCell id="c4" value="⚙️&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;main.go (root)&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;flags · Wails app · window&lt;br&gt;binds services and events&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#FFFDE7;gradientColor=#FFF9C4;strokeColor=#F9A825;strokeWidth=2;shadow=1;fontSize=20;fontColor=#E65100;" vertex="1" parent="1"><mxGeometry x="910" y="100" width="250" height="115" as="geometry"/></mxCell>
2727 <mxCell id="c6" value="&lt;b style=&quot;font-size:12.5px;color:#263238&quot;&gt;bindings · events&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;fillColor=#FFFFFF;strokeColor=none;rounded=1;" vertex="1" parent="1"><mxGeometry x="317.0" y="145.0" width="130" height="24" as="geometry"/></mxCell>
2828 <mxCell id="c8" value="&lt;b style=&quot;font-size:12.5px;color:#263238&quot;&gt;creates&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;fillColor=#FFFFFF;strokeColor=none;rounded=1;" vertex="1" parent="1"><mxGeometry x="782.0" y="145.0" width="70" height="24" as="geometry"/></mxCell>
29- <mxCell id="c9" value="💬&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/channels&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;channels, members, routing&lt;br&gt;@mentions · /agent:command&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="40" y="300" width="250" height="110" as="geometry"/></mxCell>
30- <mxCell id="c10" value="🤖&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/agents&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;agent processes: lazy start,&lt;br&gt;idle stop, limit, logs&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="330" y="300" width="250" height="110" as="geometry"/></mxCell>
31- <mxCell id="c11" value="📋&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/config&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;load · validate · watch&lt;br&gt;save with comments&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="620" y="300" width="250" height="110" as="geometry"/></mxCell>
32- <mxCell id="c12" value="🧰&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/workspace&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;files + exec: local,&lt;br&gt;container, sbx&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="910" y="300" width="250" height="110" as="geometry"/></mxCell>
33- <mxCell id="c19" value="📥&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/store&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;Pebble: channels, messages,&lt;br&gt;session ids&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="40" y="480" width="250" height="105" as="geometry"/></mxCell>
34- <mxCell id="c20" value="🔧&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/acp&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;ACP v1 client wrapper&lt;br&gt;SDK types stay inside&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="330" y="480" width="250" height="105" as="geometry"/></mxCell>
35- <mxCell id="c21" value="🛡️&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;schema&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;config JSON Schema&lt;br&gt;ValidateConfig()&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="620" y="480" width="250" height="105" as="geometry"/></mxCell>
36- <mxCell id="c25" value="&lt;b&gt;cockroachdb/pebble/v2&lt;/b&gt;" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=13;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="40" y="655" width="250" height="50" as="geometry"/></mxCell>
37- <mxCell id="c26" value="&lt;b&gt;coder/acp-go-sdk → mm -acp&lt;/b&gt;" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=13;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="330" y="655" width="250" height="50" as="geometry"/></mxCell>
38- <mxCell id="c27" value="&lt;b&gt;jsonschema/v6 · yaml/v3&lt;/b&gt;" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=13;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="620" y="655" width="250" height="50" as="geometry"/></mxCell>
39- <mxCell id="c28" value="&lt;b&gt;creack/pty · docker · sbx&lt;/b&gt;" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=13;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="910" y="655" width="250" height="50" as="geometry"/></mxCell>
40- <mxCell id="c31" value="&lt;b style=&quot;font-size:12.5px;color:#263238&quot;&gt;stdio&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;fillColor=#FFFFFF;strokeColor=none;rounded=1;" vertex="1" parent="1"><mxGeometry x="475.0" y="608.0" width="50" height="24" as="geometry"/></mxCell>
41- <mxCell id="c34" value="Third-party modules in grey. Wails v3 is imported by cmd/sidekicks and internal/app only." style="text;html=1;align=center;verticalAlign=middle;strokeColor=none;fillColor=none;fontSize=12.5;fontColor=#455A64;fontStyle=2;" vertex="1" parent="1"><mxGeometry x="255.125" y="737.5" width="689.75" height="18.5" as="geometry"/></mxCell>
29+ <mxCell id="c9" value="💬&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/channels&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;planned · M3&lt;br&gt;channels, routing, @ and /&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=20;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="40" y="300" width="250" height="110" as="geometry"/></mxCell>
30+ <mxCell id="c10" value="🤖&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/agents&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;lazy start · LRU limit&lt;br&gt;idle stop · states · logs&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="330" y="300" width="250" height="110" as="geometry"/></mxCell>
31+ <mxCell id="c11" value="📋&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/config&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;load · validate · locate&lt;br&gt;default file · save · watch&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="620" y="300" width="250" height="110" as="geometry"/></mxCell>
32+ <mxCell id="c12" value="🧰&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/workspace&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;planned · M4&lt;br&gt;files + exec: local, container, sbx&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=20;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="910" y="300" width="250" height="110" as="geometry"/></mxCell>
33+ <mxCell id="c19" value="📥&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/store&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;planned · M3&lt;br&gt;Pebble: channels, messages&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=20;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="40" y="490" width="250" height="105" as="geometry"/></mxCell>
34+ <mxCell id="c20" value="🔧&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/acp&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;ACP v1 client over stdio&lt;br&gt;SDK types stay inside&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="330" y="490" width="250" height="105" as="geometry"/></mxCell>
35+ <mxCell id="c21" value="🛡️&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;schema&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;config JSON Schema&lt;br&gt;ValidateConfig()&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="620" y="490" width="250" height="105" as="geometry"/></mxCell>
36+ <mxCell id="c22" value="📋&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/events&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;event names + payloads&lt;br&gt;Emitter interface&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="910" y="490" width="250" height="105" as="geometry"/></mxCell>
37+ <mxCell id="c27" value="&lt;b style=&quot;font-size:12.5px;color:#263238&quot;&gt;emits&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;fillColor=#FFFFFF;strokeColor=none;rounded=1;" vertex="1" parent="1"><mxGeometry x="920.0" y="438.0" width="60" height="24" as="geometry"/></mxCell>
38+ <mxCell id="c28" value="&lt;b&gt;cockroachdb/pebble/v2&lt;/b&gt;" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=13;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="40" y="670" width="250" height="50" as="geometry"/></mxCell>
39+ <mxCell id="c29" value="&lt;b&gt;coder/acp-go-sdk → mm -acp&lt;/b&gt;" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=13;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="330" y="670" width="250" height="50" as="geometry"/></mxCell>
40+ <mxCell id="c30" value="&lt;b&gt;jsonschema/v6 · yaml/v3&lt;/b&gt;" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=13;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="620" y="670" width="250" height="50" as="geometry"/></mxCell>
41+ <mxCell id="c31" value="&lt;b&gt;fsnotify (config watch)&lt;/b&gt;" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=13;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="910" y="670" width="250" height="50" as="geometry"/></mxCell>
42+ <mxCell id="c34" value="&lt;b style=&quot;font-size:12.5px;color:#263238&quot;&gt;stdio&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;fillColor=#FFFFFF;strokeColor=none;rounded=1;" vertex="1" parent="1"><mxGeometry x="475.0" y="620.0" width="50" height="24" as="geometry"/></mxCell>
43+ <mxCell id="c36" value="internal/app also emits through internal/events; internal/config imports schema and fsnotify. Third-party modules in grey chips." style="text;html=1;align=center;verticalAlign=middle;strokeColor=none;fillColor=none;fontSize=12.5;fontColor=#455A64;fontStyle=2;" vertex="1" parent="1"><mxGeometry x="104.0" y="752.5" width="992.0" height="18.5" as="geometry"/></mxCell>
4244 </root>
4345 </mxGraphModel>
4446 </diagram>
@@ -1,10 +1,10 @@
1 <mxfile host="app.diagrams.net">1 <mxfile host="app.diagrams.net">
2 <diagram id="packages" name="Sidekicks — packages and dependencies">2 <diagram id="packages" name="Sidekicks — packages and dependencies">
3- <mxGraphModel dx="1200" dy="780" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1200" pageHeight="780" background="#F5F7FC" math="0" shadow="0">3+ <mxGraphModel dx="1200" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1200" pageHeight="800" background="#F5F7FC" math="0" shadow="0">
4 <root>4 <root>
5 <mxCell id="0"/>5 <mxCell id="0"/>
6 <mxCell id="1" parent="0"/>6 <mxCell id="1" parent="0"/>
7- <mxCell id="title" value="&lt;b style=&quot;font-size:25px;color:#1A237E&quot;&gt;Sidekicks — packages and dependencies&lt;/b&gt;&lt;br&gt;&lt;i style=&quot;font-size:14px;color:#546E7A&quot;&gt;arrows are imports · the frontend only talks to internal/app · nothing outside internal/acp sees the ACP SDK&lt;/i&gt;" style="text;html=1;align=center;verticalAlign=middle;strokeColor=none;fillColor=none;" vertex="1" parent="1"><mxGeometry x="150.0" y="12" width="900" height="56" as="geometry"/></mxCell>7+ <mxCell id="title" value="&lt;b style=&quot;font-size:25px;color:#1A237E&quot;&gt;Sidekicks — packages and dependencies&lt;/b&gt;&lt;br&gt;&lt;i style=&quot;font-size:14px;color:#546E7A&quot;&gt;arrows are imports · blue: exists (M1) · grey: planned · only main.go imports Wails&lt;/i&gt;" style="text;html=1;align=center;verticalAlign=middle;strokeColor=none;fillColor=none;" vertex="1" parent="1"><mxGeometry x="150.0" y="12" width="900" height="56" as="geometry"/></mxCell>
8 <mxCell id="c5" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=block;startFill=1;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="290" y="157" as="sourcePoint"/><mxPoint x="475" y="157" as="targetPoint"/></mxGeometry></mxCell>8 <mxCell id="c5" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=block;startFill=1;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="290" y="157" as="sourcePoint"/><mxPoint x="475" y="157" as="targetPoint"/></mxGeometry></mxCell>
9 <mxCell id="c7" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="910" y="157" as="sourcePoint"/><mxPoint x="725" y="157" as="targetPoint"/></mxGeometry></mxCell>9 <mxCell id="c7" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="910" y="157" as="sourcePoint"/><mxPoint x="725" y="157" as="targetPoint"/></mxGeometry></mxCell>
10 <mxCell id="c13" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="600" y="215" as="sourcePoint"/><mxPoint x="165" y="300" as="targetPoint"/><Array as="points"><mxPoint x="600" y="255"/><mxPoint x="165" y="255"/></Array></mxGeometry></mxCell>10 <mxCell id="c13" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="600" y="215" as="sourcePoint"/><mxPoint x="165" y="300" as="targetPoint"/><Array as="points"><mxPoint x="600" y="255"/><mxPoint x="165" y="255"/></Array></mxGeometry></mxCell>
@@ -13,32 +13,34 @@
13 <mxCell id="c16" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="600" y="215" as="sourcePoint"/><mxPoint x="1035" y="300" as="targetPoint"/><Array as="points"><mxPoint x="600" y="255"/><mxPoint x="1035" y="255"/></Array></mxGeometry></mxCell>13 <mxCell id="c16" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="600" y="215" as="sourcePoint"/><mxPoint x="1035" y="300" as="targetPoint"/><Array as="points"><mxPoint x="600" y="255"/><mxPoint x="1035" y="255"/></Array></mxGeometry></mxCell>
14 <mxCell id="c17" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="290" y="355" as="sourcePoint"/><mxPoint x="330" y="355" as="targetPoint"/></mxGeometry></mxCell>14 <mxCell id="c17" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="290" y="355" as="sourcePoint"/><mxPoint x="330" y="355" as="targetPoint"/></mxGeometry></mxCell>
15 <mxCell id="c18" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="580" y="355" as="sourcePoint"/><mxPoint x="620" y="355" as="targetPoint"/></mxGeometry></mxCell>15 <mxCell id="c18" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="580" y="355" as="sourcePoint"/><mxPoint x="620" y="355" as="targetPoint"/></mxGeometry></mxCell>
16- <mxCell id="c22" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="165" y="410" as="sourcePoint"/><mxPoint x="165" y="480" as="targetPoint"/></mxGeometry></mxCell>16+ <mxCell id="c23" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="165" y="410" as="sourcePoint"/><mxPoint x="165" y="490" as="targetPoint"/></mxGeometry></mxCell>
17- <mxCell id="c23" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="455" y="410" as="sourcePoint"/><mxPoint x="455" y="480" as="targetPoint"/></mxGeometry></mxCell>17+ <mxCell id="c24" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="455" y="410" as="sourcePoint"/><mxPoint x="455" y="490" as="targetPoint"/></mxGeometry></mxCell>
18- <mxCell id="c24" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="745" y="410" as="sourcePoint"/><mxPoint x="745" y="480" as="targetPoint"/></mxGeometry></mxCell>18+ <mxCell id="c25" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="745" y="410" as="sourcePoint"/><mxPoint x="745" y="490" as="targetPoint"/></mxGeometry></mxCell>
19- <mxCell id="c29" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="165" y="585" as="sourcePoint"/><mxPoint x="165" y="655" as="targetPoint"/></mxGeometry></mxCell>19+ <mxCell id="c26" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="560" y="410" as="sourcePoint"/><mxPoint x="1035" y="490" as="targetPoint"/><Array as="points"><mxPoint x="560" y="450"/><mxPoint x="1035" y="450"/></Array></mxGeometry></mxCell>
20- <mxCell id="c30" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="455" y="585" as="sourcePoint"/><mxPoint x="455" y="655" as="targetPoint"/></mxGeometry></mxCell>20+ <mxCell id="c32" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;dashed=1;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="165" y="595" as="sourcePoint"/><mxPoint x="165" y="670" as="targetPoint"/></mxGeometry></mxCell>
21- <mxCell id="c32" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="745" y="585" as="sourcePoint"/><mxPoint x="745" y="655" as="targetPoint"/></mxGeometry></mxCell>21+ <mxCell id="c33" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="455" y="595" as="sourcePoint"/><mxPoint x="455" y="670" as="targetPoint"/></mxGeometry></mxCell>
22- <mxCell id="c33" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="1035" y="410" as="sourcePoint"/><mxPoint x="1035" y="655" as="targetPoint"/></mxGeometry></mxCell>22+ <mxCell id="c35" value="" style="edgeStyle=none;rounded=0;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;startArrow=none;" edge="1" parent="1"><mxGeometry relative="1" as="geometry"><mxPoint x="745" y="595" as="sourcePoint"/><mxPoint x="745" y="670" as="targetPoint"/></mxGeometry></mxCell>
23- <mxCell id="c1" value="💻&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;frontend/&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;Svelte 5 · panels · Markdown&lt;br&gt;CodeMirror · xterm.js&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E3F2FD;gradientColor=#BBDEFB;strokeColor=#1565C0;strokeWidth=2;shadow=1;fontSize=20;fontColor=#0D47A1;" vertex="1" parent="1"><mxGeometry x="40" y="100" width="250" height="115" as="geometry"/></mxCell>23+ <mxCell id="c1" value="💻&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;frontend/&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;Svelte 5 · panels · state&lt;br&gt;generated TS bindings&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E3F2FD;gradientColor=#BBDEFB;strokeColor=#1565C0;strokeWidth=2;shadow=1;fontSize=20;fontColor=#0D47A1;" vertex="1" parent="1"><mxGeometry x="40" y="100" width="250" height="115" as="geometry"/></mxCell>
24- <mxCell id="c2" value="🔀&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/app&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;Wails services bound to the UI&lt;br&gt;typed events to the UI&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#F3E5F5;gradientColor=#E1BEE7;strokeColor=#6A1B9A;strokeWidth=3;shadow=1;fontSize=20;fontColor=#4A148C;" vertex="1" parent="1"><mxGeometry x="475" y="100" width="250" height="115" as="geometry"/></mxCell>24+ <mxCell id="c2" value="🔀&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/app&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;AgentService · ConfigService&lt;br&gt;config ↔ agents glue&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#F3E5F5;gradientColor=#E1BEE7;strokeColor=#6A1B9A;strokeWidth=3;shadow=1;fontSize=20;fontColor=#4A148C;" vertex="1" parent="1"><mxGeometry x="475" y="100" width="250" height="115" as="geometry"/></mxCell>
25 <mxCell id="c3" value="&lt;b&gt;★ only API of the UI&lt;/b&gt;" style="rounded=1;arcSize=50;html=1;fillColor=#6A1B9A;strokeColor=none;fontColor=#FFFFFF;fontSize=11;" vertex="1" parent="1"><mxGeometry x="577.0" y="88" width="158.0" height="24" as="geometry"/></mxCell>25 <mxCell id="c3" value="&lt;b&gt;★ only API of the UI&lt;/b&gt;" style="rounded=1;arcSize=50;html=1;fillColor=#6A1B9A;strokeColor=none;fontColor=#FFFFFF;fontSize=11;" vertex="1" parent="1"><mxGeometry x="577.0" y="88" width="158.0" height="24" as="geometry"/></mxCell>
26- <mxCell id="c4" value="⚙️&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;cmd/sidekicks&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;main: reads flags, wires&lt;br&gt;the services, runs Wails&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=20;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="910" y="100" width="250" height="115" as="geometry"/></mxCell>26+ <mxCell id="c4" value="⚙️&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;main.go (root)&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;flags · Wails app · window&lt;br&gt;binds services and events&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#FFFDE7;gradientColor=#FFF9C4;strokeColor=#F9A825;strokeWidth=2;shadow=1;fontSize=20;fontColor=#E65100;" vertex="1" parent="1"><mxGeometry x="910" y="100" width="250" height="115" as="geometry"/></mxCell>
27 <mxCell id="c6" value="&lt;b style=&quot;font-size:12.5px;color:#263238&quot;&gt;bindings · events&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;fillColor=#FFFFFF;strokeColor=none;rounded=1;" vertex="1" parent="1"><mxGeometry x="317.0" y="145.0" width="130" height="24" as="geometry"/></mxCell>27 <mxCell id="c6" value="&lt;b style=&quot;font-size:12.5px;color:#263238&quot;&gt;bindings · events&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;fillColor=#FFFFFF;strokeColor=none;rounded=1;" vertex="1" parent="1"><mxGeometry x="317.0" y="145.0" width="130" height="24" as="geometry"/></mxCell>
28 <mxCell id="c8" value="&lt;b style=&quot;font-size:12.5px;color:#263238&quot;&gt;creates&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;fillColor=#FFFFFF;strokeColor=none;rounded=1;" vertex="1" parent="1"><mxGeometry x="782.0" y="145.0" width="70" height="24" as="geometry"/></mxCell>28 <mxCell id="c8" value="&lt;b style=&quot;font-size:12.5px;color:#263238&quot;&gt;creates&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;fillColor=#FFFFFF;strokeColor=none;rounded=1;" vertex="1" parent="1"><mxGeometry x="782.0" y="145.0" width="70" height="24" as="geometry"/></mxCell>
29- <mxCell id="c9" value="💬&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/channels&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;channels, members, routing&lt;br&gt;@mentions · /agent:command&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="40" y="300" width="250" height="110" as="geometry"/></mxCell>29+ <mxCell id="c9" value="💬&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/channels&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;planned · M3&lt;br&gt;channels, routing, @ and /&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=20;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="40" y="300" width="250" height="110" as="geometry"/></mxCell>
30- <mxCell id="c10" value="🤖&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/agents&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;agent processes: lazy start,&lt;br&gt;idle stop, limit, logs&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="330" y="300" width="250" height="110" as="geometry"/></mxCell>30+ <mxCell id="c10" value="🤖&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/agents&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;lazy start · LRU limit&lt;br&gt;idle stop · states · logs&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="330" y="300" width="250" height="110" as="geometry"/></mxCell>
31- <mxCell id="c11" value="📋&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/config&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;load · validate · watch&lt;br&gt;save with comments&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="620" y="300" width="250" height="110" as="geometry"/></mxCell>31+ <mxCell id="c11" value="📋&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/config&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;load · validate · locate&lt;br&gt;default file · save · watch&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="620" y="300" width="250" height="110" as="geometry"/></mxCell>
32- <mxCell id="c12" value="🧰&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/workspace&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;files + exec: local,&lt;br&gt;container, sbx&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="910" y="300" width="250" height="110" as="geometry"/></mxCell>32+ <mxCell id="c12" value="🧰&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/workspace&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;planned · M4&lt;br&gt;files + exec: local, container, sbx&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=20;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="910" y="300" width="250" height="110" as="geometry"/></mxCell>
33- <mxCell id="c19" value="📥&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/store&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;Pebble: channels, messages,&lt;br&gt;session ids&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="40" y="480" width="250" height="105" as="geometry"/></mxCell>33+ <mxCell id="c19" value="📥&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/store&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;planned · M3&lt;br&gt;Pebble: channels, messages&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=20;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="40" y="490" width="250" height="105" as="geometry"/></mxCell>
34- <mxCell id="c20" value="🔧&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/acp&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;ACP v1 client wrapper&lt;br&gt;SDK types stay inside&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="330" y="480" width="250" height="105" as="geometry"/></mxCell>34+ <mxCell id="c20" value="🔧&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/acp&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;ACP v1 client over stdio&lt;br&gt;SDK types stay inside&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="330" y="490" width="250" height="105" as="geometry"/></mxCell>
35- <mxCell id="c21" value="🛡️&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;schema&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;config JSON Schema&lt;br&gt;ValidateConfig()&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="620" y="480" width="250" height="105" as="geometry"/></mxCell>35+ <mxCell id="c21" value="🛡️&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;schema&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;config JSON Schema&lt;br&gt;ValidateConfig()&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="620" y="490" width="250" height="105" as="geometry"/></mxCell>
36- <mxCell id="c25" value="&lt;b&gt;cockroachdb/pebble/v2&lt;/b&gt;" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=13;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="40" y="655" width="250" height="50" as="geometry"/></mxCell>36+ <mxCell id="c22" value="📋&lt;br&gt;&lt;b style=&quot;font-size:16px&quot;&gt;internal/events&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;font-size:11.5px;font-weight:normal&quot;&gt;event names + payloads&lt;br&gt;Emitter interface&lt;/span&gt;" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=20;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="910" y="490" width="250" height="105" as="geometry"/></mxCell>
37- <mxCell id="c26" value="&lt;b&gt;coder/acp-go-sdk → mm -acp&lt;/b&gt;" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=13;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="330" y="655" width="250" height="50" as="geometry"/></mxCell>37+ <mxCell id="c27" value="&lt;b style=&quot;font-size:12.5px;color:#263238&quot;&gt;emits&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;fillColor=#FFFFFF;strokeColor=none;rounded=1;" vertex="1" parent="1"><mxGeometry x="920.0" y="438.0" width="60" height="24" as="geometry"/></mxCell>
38- <mxCell id="c27" value="&lt;b&gt;jsonschema/v6 · yaml/v3&lt;/b&gt;" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=13;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="620" y="655" width="250" height="50" as="geometry"/></mxCell>38+ <mxCell id="c28" value="&lt;b&gt;cockroachdb/pebble/v2&lt;/b&gt;" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=13;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="40" y="670" width="250" height="50" as="geometry"/></mxCell>
39- <mxCell id="c28" value="&lt;b&gt;creack/pty · docker · sbx&lt;/b&gt;" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=13;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="910" y="655" width="250" height="50" as="geometry"/></mxCell>39+ <mxCell id="c29" value="&lt;b&gt;coder/acp-go-sdk → mm -acp&lt;/b&gt;" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=13;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="330" y="670" width="250" height="50" as="geometry"/></mxCell>
40- <mxCell id="c31" value="&lt;b style=&quot;font-size:12.5px;color:#263238&quot;&gt;stdio&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;fillColor=#FFFFFF;strokeColor=none;rounded=1;" vertex="1" parent="1"><mxGeometry x="475.0" y="608.0" width="50" height="24" as="geometry"/></mxCell>40+ <mxCell id="c30" value="&lt;b&gt;jsonschema/v6 · yaml/v3&lt;/b&gt;" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=13;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="620" y="670" width="250" height="50" as="geometry"/></mxCell>
41- <mxCell id="c34" value="Third-party modules in grey. Wails v3 is imported by cmd/sidekicks and internal/app only." style="text;html=1;align=center;verticalAlign=middle;strokeColor=none;fillColor=none;fontSize=12.5;fontColor=#455A64;fontStyle=2;" vertex="1" parent="1"><mxGeometry x="255.125" y="737.5" width="689.75" height="18.5" as="geometry"/></mxCell>41+ <mxCell id="c31" value="&lt;b&gt;fsnotify (config watch)&lt;/b&gt;" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=13;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="910" y="670" width="250" height="50" as="geometry"/></mxCell>
42+ <mxCell id="c34" value="&lt;b style=&quot;font-size:12.5px;color:#263238&quot;&gt;stdio&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;fillColor=#FFFFFF;strokeColor=none;rounded=1;" vertex="1" parent="1"><mxGeometry x="475.0" y="620.0" width="50" height="24" as="geometry"/></mxCell>
43+ <mxCell id="c36" value="internal/app also emits through internal/events; internal/config imports schema and fsnotify. Third-party modules in grey chips." style="text;html=1;align=center;verticalAlign=middle;strokeColor=none;fillColor=none;fontSize=12.5;fontColor=#455A64;fontStyle=2;" vertex="1" parent="1"><mxGeometry x="104.0" y="752.5" width="992.0" height="18.5" as="geometry"/></mxCell>
42 </root>44 </root>
43 </mxGraphModel>45 </mxGraphModel>
44 </diagram>46 </diagram>
modified docs/diagrams/packages.png +0 -0
Binary files a/docs/diagrams/packages.png and b/docs/diagrams/packages.png differ
Binary files a/docs/diagrams/packages.png and b/docs/diagrams/packages.png differBinary files a/docs/diagrams/packages.png and b/docs/diagrams/packages.png differ
modified docs/diagrams/packages.svg +63 -56
@@ -1,4 +1,4 @@
1-<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="780" viewBox="0 0 1200 780" font-family="DejaVu Sans, Verdana, sans-serif">
1+<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800" viewBox="0 0 1200 800" font-family="DejaVu Sans, Verdana, sans-serif">
22 <defs>
33 <linearGradient id="gBg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#FFFFFF"/><stop offset="100%" stop-color="#EEF2FA"/></linearGradient>
44 <linearGradient id="gBlue" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#E3F2FD"/><stop offset="100%" stop-color="#BBDEFB"/></linearGradient>
@@ -9,9 +9,9 @@
99 <filter id="shadow" x="-30%" y="-30%" width="160%" height="160%"><feDropShadow dx="2" dy="3" stdDeviation="4" flood-color="#000000" flood-opacity="0.25"/></filter>
1010 <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#37474F"/></marker>
1111 </defs>
12- <rect x="0" y="0" width="1200" height="780" fill="url(#gBg)"/>
12+ <rect x="0" y="0" width="1200" height="800" fill="url(#gBg)"/>
1313 <text x="600.0" y="38" text-anchor="middle" font-size="25" font-weight="700" fill="#1A237E">Sidekicks — packages and dependencies</text>
14- <text x="600.0" y="61" text-anchor="middle" font-size="14" font-style="italic" fill="#546E7A">arrows are imports · the frontend only talks to internal/app · nothing outside internal/acp sees the ACP SDK</text>
14+ <text x="600.0" y="61" text-anchor="middle" font-size="14" font-style="italic" fill="#546E7A">arrows are imports · blue: exists (M1) · grey: planned · only main.go imports Wails</text>
1515 <path d="M290,157 L475,157" fill="none" stroke="#37474F" stroke-width="2.2" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
1616 <path d="M910,157 L725,157" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
1717 <path d="M600,215 L600,255 L165,255 L165,300" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
@@ -20,78 +20,85 @@
2020 <path d="M600,215 L600,255 L1035,255 L1035,300" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
2121 <path d="M290,355 L330,355" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
2222 <path d="M580,355 L620,355" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
23- <path d="M165,410 L165,480" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
24- <path d="M455,410 L455,480" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
25- <path d="M745,410 L745,480" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
26- <path d="M165,585 L165,655" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
27- <path d="M455,585 L455,655" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
28- <path d="M745,585 L745,655" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
29- <path d="M1035,410 L1035,655" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
23+ <path d="M165,410 L165,490" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
24+ <path d="M455,410 L455,490" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
25+ <path d="M745,410 L745,490" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
26+ <path d="M560,410 L560,450 L1035,450 L1035,490" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
27+ <path d="M165,595 L165,670" fill="none" stroke="#37474F" stroke-width="2.2" stroke-dasharray="6,4" marker-end="url(#arrow)"/>
28+ <path d="M455,595 L455,670" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
29+ <path d="M745,595 L745,670" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
3030 <g filter="url(#shadow)"><rect x="40" y="100" width="250" height="115" rx="16" fill="url(#gBlue)" stroke="#1565C0" stroke-width="2"/></g>
3131 <text x="165.0" y="137.5" text-anchor="middle" font-size="22">💻</text>
3232 <text x="165.0" y="160.5" text-anchor="middle" font-size="16" font-weight="700" fill="#0D47A1">frontend/</text>
33- <text x="165.0" y="178.5" text-anchor="middle" font-size="11.5" fill="#0D47A1">Svelte 5 · panels · Markdown</text>
34- <text x="165.0" y="195.5" text-anchor="middle" font-size="11.5" fill="#0D47A1">CodeMirror · xterm.js</text>
33+ <text x="165.0" y="178.5" text-anchor="middle" font-size="11.5" fill="#0D47A1">Svelte 5 · panels · state</text>
34+ <text x="165.0" y="195.5" text-anchor="middle" font-size="11.5" fill="#0D47A1">generated TS bindings</text>
3535 <g filter="url(#shadow)"><rect x="475" y="100" width="250" height="115" rx="16" fill="url(#gPurple)" stroke="#6A1B9A" stroke-width="3"/></g>
3636 <text x="600.0" y="137.5" text-anchor="middle" font-size="22">🔀</text>
3737 <text x="600.0" y="160.5" text-anchor="middle" font-size="16" font-weight="700" fill="#4A148C">internal/app</text>
38- <text x="600.0" y="178.5" text-anchor="middle" font-size="11.5" fill="#4A148C">Wails services bound to the UI</text>
39- <text x="600.0" y="195.5" text-anchor="middle" font-size="11.5" fill="#4A148C">typed events to the UI</text>
38+ <text x="600.0" y="178.5" text-anchor="middle" font-size="11.5" fill="#4A148C">AgentService · ConfigService</text>
39+ <text x="600.0" y="195.5" text-anchor="middle" font-size="11.5" fill="#4A148C">config ↔ agents glue</text>
4040 <rect x="577.0" y="88" width="158.0" height="24" rx="12" fill="#6A1B9A"/>
4141 <text x="656.0" y="104" text-anchor="middle" font-size="11.5" font-weight="700" fill="#FFFFFF">★ only API of the UI</text>
42- <g filter="url(#shadow)"><rect x="910" y="100" width="250" height="115" rx="16" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
42+ <g filter="url(#shadow)"><rect x="910" y="100" width="250" height="115" rx="16" fill="url(#gYellow)" stroke="#F9A825" stroke-width="2"/></g>
4343 <text x="1035.0" y="137.5" text-anchor="middle" font-size="22">⚙️</text>
44- <text x="1035.0" y="160.5" text-anchor="middle" font-size="16" font-weight="700" fill="#263238">cmd/sidekicks</text>
45- <text x="1035.0" y="178.5" text-anchor="middle" font-size="11.5" fill="#263238">main: reads flags, wires</text>
46- <text x="1035.0" y="195.5" text-anchor="middle" font-size="11.5" fill="#263238">the services, runs Wails</text>
44+ <text x="1035.0" y="160.5" text-anchor="middle" font-size="16" font-weight="700" fill="#E65100">main.go (root)</text>
45+ <text x="1035.0" y="178.5" text-anchor="middle" font-size="11.5" fill="#E65100">flags · Wails app · window</text>
46+ <text x="1035.0" y="195.5" text-anchor="middle" font-size="11.5" fill="#E65100">binds services and events</text>
4747 <rect x="317.0" y="145.0" width="130" height="24" rx="5" fill="#FFFFFF"/>
4848 <text x="382" y="161.5" text-anchor="middle" font-size="12.5" font-weight="700" fill="#263238">bindings · events</text>
4949 <rect x="782.0" y="145.0" width="70" height="24" rx="5" fill="#FFFFFF"/>
5050 <text x="817" y="161.5" text-anchor="middle" font-size="12.5" font-weight="700" fill="#263238">creates</text>
51- <g filter="url(#shadow)"><rect x="40" y="300" width="250" height="110" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>
51+ <g filter="url(#shadow)"><rect x="40" y="300" width="250" height="110" rx="16" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
5252 <text x="165.0" y="335.0" text-anchor="middle" font-size="22">💬</text>
53- <text x="165.0" y="358.0" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/channels</text>
54- <text x="165.0" y="376.0" text-anchor="middle" font-size="11.5" fill="#01579B">channels, members, routing</text>
55- <text x="165.0" y="393.0" text-anchor="middle" font-size="11.5" fill="#01579B">@mentions · /agent:command</text>
53+ <text x="165.0" y="358.0" text-anchor="middle" font-size="16" font-weight="700" fill="#263238">internal/channels</text>
54+ <text x="165.0" y="376.0" text-anchor="middle" font-size="11.5" fill="#263238">planned · M3</text>
55+ <text x="165.0" y="393.0" text-anchor="middle" font-size="11.5" fill="#263238">channels, routing, @ and /</text>
5656 <g filter="url(#shadow)"><rect x="330" y="300" width="250" height="110" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>
5757 <text x="455.0" y="335.0" text-anchor="middle" font-size="22">🤖</text>
5858 <text x="455.0" y="358.0" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/agents</text>
59- <text x="455.0" y="376.0" text-anchor="middle" font-size="11.5" fill="#01579B">agent processes: lazy start,</text>
60- <text x="455.0" y="393.0" text-anchor="middle" font-size="11.5" fill="#01579B">idle stop, limit, logs</text>
59+ <text x="455.0" y="376.0" text-anchor="middle" font-size="11.5" fill="#01579B">lazy start · LRU limit</text>
60+ <text x="455.0" y="393.0" text-anchor="middle" font-size="11.5" fill="#01579B">idle stop · states · logs</text>
6161 <g filter="url(#shadow)"><rect x="620" y="300" width="250" height="110" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>
6262 <text x="745.0" y="335.0" text-anchor="middle" font-size="22">📋</text>
6363 <text x="745.0" y="358.0" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/config</text>
64- <text x="745.0" y="376.0" text-anchor="middle" font-size="11.5" fill="#01579B">load · validate · watch</text>
65- <text x="745.0" y="393.0" text-anchor="middle" font-size="11.5" fill="#01579B">save with comments</text>
66- <g filter="url(#shadow)"><rect x="910" y="300" width="250" height="110" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>
64+ <text x="745.0" y="376.0" text-anchor="middle" font-size="11.5" fill="#01579B">load · validate · locate</text>
65+ <text x="745.0" y="393.0" text-anchor="middle" font-size="11.5" fill="#01579B">default file · save · watch</text>
66+ <g filter="url(#shadow)"><rect x="910" y="300" width="250" height="110" rx="16" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
6767 <text x="1035.0" y="335.0" text-anchor="middle" font-size="22">🧰</text>
68- <text x="1035.0" y="358.0" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/workspace</text>
69- <text x="1035.0" y="376.0" text-anchor="middle" font-size="11.5" fill="#01579B">files + exec: local,</text>
70- <text x="1035.0" y="393.0" text-anchor="middle" font-size="11.5" fill="#01579B">container, sbx</text>
71- <g filter="url(#shadow)"><rect x="40" y="480" width="250" height="105" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>
72- <text x="165.0" y="512.5" text-anchor="middle" font-size="22">📥</text>
73- <text x="165.0" y="535.5" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/store</text>
74- <text x="165.0" y="553.5" text-anchor="middle" font-size="11.5" fill="#01579B">Pebble: channels, messages,</text>
75- <text x="165.0" y="570.5" text-anchor="middle" font-size="11.5" fill="#01579B">session ids</text>
76- <g filter="url(#shadow)"><rect x="330" y="480" width="250" height="105" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>
77- <text x="455.0" y="512.5" text-anchor="middle" font-size="22">🔧</text>
78- <text x="455.0" y="535.5" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/acp</text>
79- <text x="455.0" y="553.5" text-anchor="middle" font-size="11.5" fill="#01579B">ACP v1 client wrapper</text>
80- <text x="455.0" y="570.5" text-anchor="middle" font-size="11.5" fill="#01579B">SDK types stay inside</text>
81- <g filter="url(#shadow)"><rect x="620" y="480" width="250" height="105" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>
82- <text x="745.0" y="512.5" text-anchor="middle" font-size="22">🛡️</text>
83- <text x="745.0" y="535.5" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">schema</text>
84- <text x="745.0" y="553.5" text-anchor="middle" font-size="11.5" fill="#01579B">config JSON Schema</text>
85- <text x="745.0" y="570.5" text-anchor="middle" font-size="11.5" fill="#01579B">ValidateConfig()</text>
86- <g filter="url(#shadow)"><rect x="40" y="655" width="250" height="50" rx="12" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
87- <text x="165.0" y="685.0" text-anchor="middle" font-size="13" font-weight="700" fill="#263238">cockroachdb/pebble/v2</text>
88- <g filter="url(#shadow)"><rect x="330" y="655" width="250" height="50" rx="12" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
89- <text x="455.0" y="685.0" text-anchor="middle" font-size="13" font-weight="700" fill="#263238">coder/acp-go-sdk → mm -acp</text>
90- <g filter="url(#shadow)"><rect x="620" y="655" width="250" height="50" rx="12" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
91- <text x="745.0" y="685.0" text-anchor="middle" font-size="13" font-weight="700" fill="#263238">jsonschema/v6 · yaml/v3</text>
92- <g filter="url(#shadow)"><rect x="910" y="655" width="250" height="50" rx="12" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
93- <text x="1035.0" y="685.0" text-anchor="middle" font-size="13" font-weight="700" fill="#263238">creack/pty · docker · sbx</text>
94- <rect x="475.0" y="608.0" width="50" height="24" rx="5" fill="#FFFFFF"/>
95- <text x="500" y="624.5" text-anchor="middle" font-size="12.5" font-weight="700" fill="#263238">stdio</text>
96- <text x="600" y="750" text-anchor="middle" font-size="12.5" fill="#455A64" font-style="italic">Third-party modules in grey. Wails v3 is imported by cmd/sidekicks and internal/app only.</text>
68+ <text x="1035.0" y="358.0" text-anchor="middle" font-size="16" font-weight="700" fill="#263238">internal/workspace</text>
69+ <text x="1035.0" y="376.0" text-anchor="middle" font-size="11.5" fill="#263238">planned · M4</text>
70+ <text x="1035.0" y="393.0" text-anchor="middle" font-size="11.5" fill="#263238">files + exec: local, container, sbx</text>
71+ <g filter="url(#shadow)"><rect x="40" y="490" width="250" height="105" rx="16" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
72+ <text x="165.0" y="522.5" text-anchor="middle" font-size="22">📥</text>
73+ <text x="165.0" y="545.5" text-anchor="middle" font-size="16" font-weight="700" fill="#263238">internal/store</text>
74+ <text x="165.0" y="563.5" text-anchor="middle" font-size="11.5" fill="#263238">planned · M3</text>
75+ <text x="165.0" y="580.5" text-anchor="middle" font-size="11.5" fill="#263238">Pebble: channels, messages</text>
76+ <g filter="url(#shadow)"><rect x="330" y="490" width="250" height="105" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>
77+ <text x="455.0" y="522.5" text-anchor="middle" font-size="22">🔧</text>
78+ <text x="455.0" y="545.5" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/acp</text>
79+ <text x="455.0" y="563.5" text-anchor="middle" font-size="11.5" fill="#01579B">ACP v1 client over stdio</text>
80+ <text x="455.0" y="580.5" text-anchor="middle" font-size="11.5" fill="#01579B">SDK types stay inside</text>
81+ <g filter="url(#shadow)"><rect x="620" y="490" width="250" height="105" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>
82+ <text x="745.0" y="522.5" text-anchor="middle" font-size="22">🛡️</text>
83+ <text x="745.0" y="545.5" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">schema</text>
84+ <text x="745.0" y="563.5" text-anchor="middle" font-size="11.5" fill="#01579B">config JSON Schema</text>
85+ <text x="745.0" y="580.5" text-anchor="middle" font-size="11.5" fill="#01579B">ValidateConfig()</text>
86+ <g filter="url(#shadow)"><rect x="910" y="490" width="250" height="105" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>
87+ <text x="1035.0" y="522.5" text-anchor="middle" font-size="22">📋</text>
88+ <text x="1035.0" y="545.5" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/events</text>
89+ <text x="1035.0" y="563.5" text-anchor="middle" font-size="11.5" fill="#01579B">event names + payloads</text>
90+ <text x="1035.0" y="580.5" text-anchor="middle" font-size="11.5" fill="#01579B">Emitter interface</text>
91+ <rect x="920.0" y="438.0" width="60" height="24" rx="5" fill="#FFFFFF"/>
92+ <text x="950" y="454.5" text-anchor="middle" font-size="12.5" font-weight="700" fill="#263238">emits</text>
93+ <g filter="url(#shadow)"><rect x="40" y="670" width="250" height="50" rx="12" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
94+ <text x="165.0" y="700.0" text-anchor="middle" font-size="13" font-weight="700" fill="#263238">cockroachdb/pebble/v2</text>
95+ <g filter="url(#shadow)"><rect x="330" y="670" width="250" height="50" rx="12" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
96+ <text x="455.0" y="700.0" text-anchor="middle" font-size="13" font-weight="700" fill="#263238">coder/acp-go-sdk → mm -acp</text>
97+ <g filter="url(#shadow)"><rect x="620" y="670" width="250" height="50" rx="12" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
98+ <text x="745.0" y="700.0" text-anchor="middle" font-size="13" font-weight="700" fill="#263238">jsonschema/v6 · yaml/v3</text>
99+ <g filter="url(#shadow)"><rect x="910" y="670" width="250" height="50" rx="12" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
100+ <text x="1035.0" y="700.0" text-anchor="middle" font-size="13" font-weight="700" fill="#263238">fsnotify (config watch)</text>
101+ <rect x="475.0" y="620.0" width="50" height="24" rx="5" fill="#FFFFFF"/>
102+ <text x="500" y="636.5" text-anchor="middle" font-size="12.5" font-weight="700" fill="#263238">stdio</text>
103+ <text x="600" y="765" text-anchor="middle" font-size="12.5" fill="#455A64" font-style="italic">internal/app also emits through internal/events; internal/config imports schema and fsnotify. Third-party modules in grey chips.</text>
97104 </svg>
@@ -1,4 +1,4 @@
1-<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="780" viewBox="0 0 1200 780" font-family="DejaVu Sans, Verdana, sans-serif">1+<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800" viewBox="0 0 1200 800" font-family="DejaVu Sans, Verdana, sans-serif">
2 <defs>2 <defs>
3 <linearGradient id="gBg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#FFFFFF"/><stop offset="100%" stop-color="#EEF2FA"/></linearGradient>3 <linearGradient id="gBg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#FFFFFF"/><stop offset="100%" stop-color="#EEF2FA"/></linearGradient>
4 <linearGradient id="gBlue" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#E3F2FD"/><stop offset="100%" stop-color="#BBDEFB"/></linearGradient>4 <linearGradient id="gBlue" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#E3F2FD"/><stop offset="100%" stop-color="#BBDEFB"/></linearGradient>
@@ -9,9 +9,9 @@
9 <filter id="shadow" x="-30%" y="-30%" width="160%" height="160%"><feDropShadow dx="2" dy="3" stdDeviation="4" flood-color="#000000" flood-opacity="0.25"/></filter>9 <filter id="shadow" x="-30%" y="-30%" width="160%" height="160%"><feDropShadow dx="2" dy="3" stdDeviation="4" flood-color="#000000" flood-opacity="0.25"/></filter>
10 <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#37474F"/></marker>10 <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#37474F"/></marker>
11 </defs>11 </defs>
12- <rect x="0" y="0" width="1200" height="780" fill="url(#gBg)"/>12+ <rect x="0" y="0" width="1200" height="800" fill="url(#gBg)"/>
13 <text x="600.0" y="38" text-anchor="middle" font-size="25" font-weight="700" fill="#1A237E">Sidekicks — packages and dependencies</text>13 <text x="600.0" y="38" text-anchor="middle" font-size="25" font-weight="700" fill="#1A237E">Sidekicks — packages and dependencies</text>
14- <text x="600.0" y="61" text-anchor="middle" font-size="14" font-style="italic" fill="#546E7A">arrows are imports · the frontend only talks to internal/app · nothing outside internal/acp sees the ACP SDK</text>14+ <text x="600.0" y="61" text-anchor="middle" font-size="14" font-style="italic" fill="#546E7A">arrows are imports · blue: exists (M1) · grey: planned · only main.go imports Wails</text>
15 <path d="M290,157 L475,157" fill="none" stroke="#37474F" stroke-width="2.2" marker-start="url(#arrow)" marker-end="url(#arrow)"/>15 <path d="M290,157 L475,157" fill="none" stroke="#37474F" stroke-width="2.2" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
16 <path d="M910,157 L725,157" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>16 <path d="M910,157 L725,157" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
17 <path d="M600,215 L600,255 L165,255 L165,300" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>17 <path d="M600,215 L600,255 L165,255 L165,300" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
@@ -20,78 +20,85 @@
20 <path d="M600,215 L600,255 L1035,255 L1035,300" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>20 <path d="M600,215 L600,255 L1035,255 L1035,300" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
21 <path d="M290,355 L330,355" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>21 <path d="M290,355 L330,355" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
22 <path d="M580,355 L620,355" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>22 <path d="M580,355 L620,355" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
23- <path d="M165,410 L165,480" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>23+ <path d="M165,410 L165,490" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
24- <path d="M455,410 L455,480" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>24+ <path d="M455,410 L455,490" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
25- <path d="M745,410 L745,480" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>25+ <path d="M745,410 L745,490" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
26- <path d="M165,585 L165,655" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>26+ <path d="M560,410 L560,450 L1035,450 L1035,490" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
27- <path d="M455,585 L455,655" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>27+ <path d="M165,595 L165,670" fill="none" stroke="#37474F" stroke-width="2.2" stroke-dasharray="6,4" marker-end="url(#arrow)"/>
28- <path d="M745,585 L745,655" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>28+ <path d="M455,595 L455,670" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
29- <path d="M1035,410 L1035,655" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>29+ <path d="M745,595 L745,670" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/>
30 <g filter="url(#shadow)"><rect x="40" y="100" width="250" height="115" rx="16" fill="url(#gBlue)" stroke="#1565C0" stroke-width="2"/></g>30 <g filter="url(#shadow)"><rect x="40" y="100" width="250" height="115" rx="16" fill="url(#gBlue)" stroke="#1565C0" stroke-width="2"/></g>
31 <text x="165.0" y="137.5" text-anchor="middle" font-size="22">💻</text>31 <text x="165.0" y="137.5" text-anchor="middle" font-size="22">💻</text>
32 <text x="165.0" y="160.5" text-anchor="middle" font-size="16" font-weight="700" fill="#0D47A1">frontend/</text>32 <text x="165.0" y="160.5" text-anchor="middle" font-size="16" font-weight="700" fill="#0D47A1">frontend/</text>
33- <text x="165.0" y="178.5" text-anchor="middle" font-size="11.5" fill="#0D47A1">Svelte 5 · panels · Markdown</text>33+ <text x="165.0" y="178.5" text-anchor="middle" font-size="11.5" fill="#0D47A1">Svelte 5 · panels · state</text>
34- <text x="165.0" y="195.5" text-anchor="middle" font-size="11.5" fill="#0D47A1">CodeMirror · xterm.js</text>34+ <text x="165.0" y="195.5" text-anchor="middle" font-size="11.5" fill="#0D47A1">generated TS bindings</text>
35 <g filter="url(#shadow)"><rect x="475" y="100" width="250" height="115" rx="16" fill="url(#gPurple)" stroke="#6A1B9A" stroke-width="3"/></g>35 <g filter="url(#shadow)"><rect x="475" y="100" width="250" height="115" rx="16" fill="url(#gPurple)" stroke="#6A1B9A" stroke-width="3"/></g>
36 <text x="600.0" y="137.5" text-anchor="middle" font-size="22">🔀</text>36 <text x="600.0" y="137.5" text-anchor="middle" font-size="22">🔀</text>
37 <text x="600.0" y="160.5" text-anchor="middle" font-size="16" font-weight="700" fill="#4A148C">internal/app</text>37 <text x="600.0" y="160.5" text-anchor="middle" font-size="16" font-weight="700" fill="#4A148C">internal/app</text>
38- <text x="600.0" y="178.5" text-anchor="middle" font-size="11.5" fill="#4A148C">Wails services bound to the UI</text>38+ <text x="600.0" y="178.5" text-anchor="middle" font-size="11.5" fill="#4A148C">AgentService · ConfigService</text>
39- <text x="600.0" y="195.5" text-anchor="middle" font-size="11.5" fill="#4A148C">typed events to the UI</text>39+ <text x="600.0" y="195.5" text-anchor="middle" font-size="11.5" fill="#4A148C">config ↔ agents glue</text>
40 <rect x="577.0" y="88" width="158.0" height="24" rx="12" fill="#6A1B9A"/>40 <rect x="577.0" y="88" width="158.0" height="24" rx="12" fill="#6A1B9A"/>
41 <text x="656.0" y="104" text-anchor="middle" font-size="11.5" font-weight="700" fill="#FFFFFF">★ only API of the UI</text>41 <text x="656.0" y="104" text-anchor="middle" font-size="11.5" font-weight="700" fill="#FFFFFF">★ only API of the UI</text>
42- <g filter="url(#shadow)"><rect x="910" y="100" width="250" height="115" rx="16" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>42+ <g filter="url(#shadow)"><rect x="910" y="100" width="250" height="115" rx="16" fill="url(#gYellow)" stroke="#F9A825" stroke-width="2"/></g>
43 <text x="1035.0" y="137.5" text-anchor="middle" font-size="22">⚙️</text>43 <text x="1035.0" y="137.5" text-anchor="middle" font-size="22">⚙️</text>
44- <text x="1035.0" y="160.5" text-anchor="middle" font-size="16" font-weight="700" fill="#263238">cmd/sidekicks</text>44+ <text x="1035.0" y="160.5" text-anchor="middle" font-size="16" font-weight="700" fill="#E65100">main.go (root)</text>
45- <text x="1035.0" y="178.5" text-anchor="middle" font-size="11.5" fill="#263238">main: reads flags, wires</text>45+ <text x="1035.0" y="178.5" text-anchor="middle" font-size="11.5" fill="#E65100">flags · Wails app · window</text>
46- <text x="1035.0" y="195.5" text-anchor="middle" font-size="11.5" fill="#263238">the services, runs Wails</text>46+ <text x="1035.0" y="195.5" text-anchor="middle" font-size="11.5" fill="#E65100">binds services and events</text>
47 <rect x="317.0" y="145.0" width="130" height="24" rx="5" fill="#FFFFFF"/>47 <rect x="317.0" y="145.0" width="130" height="24" rx="5" fill="#FFFFFF"/>
48 <text x="382" y="161.5" text-anchor="middle" font-size="12.5" font-weight="700" fill="#263238">bindings · events</text>48 <text x="382" y="161.5" text-anchor="middle" font-size="12.5" font-weight="700" fill="#263238">bindings · events</text>
49 <rect x="782.0" y="145.0" width="70" height="24" rx="5" fill="#FFFFFF"/>49 <rect x="782.0" y="145.0" width="70" height="24" rx="5" fill="#FFFFFF"/>
50 <text x="817" y="161.5" text-anchor="middle" font-size="12.5" font-weight="700" fill="#263238">creates</text>50 <text x="817" y="161.5" text-anchor="middle" font-size="12.5" font-weight="700" fill="#263238">creates</text>
51- <g filter="url(#shadow)"><rect x="40" y="300" width="250" height="110" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>51+ <g filter="url(#shadow)"><rect x="40" y="300" width="250" height="110" rx="16" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
52 <text x="165.0" y="335.0" text-anchor="middle" font-size="22">💬</text>52 <text x="165.0" y="335.0" text-anchor="middle" font-size="22">💬</text>
53- <text x="165.0" y="358.0" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/channels</text>53+ <text x="165.0" y="358.0" text-anchor="middle" font-size="16" font-weight="700" fill="#263238">internal/channels</text>
54- <text x="165.0" y="376.0" text-anchor="middle" font-size="11.5" fill="#01579B">channels, members, routing</text>54+ <text x="165.0" y="376.0" text-anchor="middle" font-size="11.5" fill="#263238">planned · M3</text>
55- <text x="165.0" y="393.0" text-anchor="middle" font-size="11.5" fill="#01579B">@mentions · /agent:command</text>55+ <text x="165.0" y="393.0" text-anchor="middle" font-size="11.5" fill="#263238">channels, routing, @ and /</text>
56 <g filter="url(#shadow)"><rect x="330" y="300" width="250" height="110" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>56 <g filter="url(#shadow)"><rect x="330" y="300" width="250" height="110" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>
57 <text x="455.0" y="335.0" text-anchor="middle" font-size="22">🤖</text>57 <text x="455.0" y="335.0" text-anchor="middle" font-size="22">🤖</text>
58 <text x="455.0" y="358.0" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/agents</text>58 <text x="455.0" y="358.0" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/agents</text>
59- <text x="455.0" y="376.0" text-anchor="middle" font-size="11.5" fill="#01579B">agent processes: lazy start,</text>59+ <text x="455.0" y="376.0" text-anchor="middle" font-size="11.5" fill="#01579B">lazy start · LRU limit</text>
60- <text x="455.0" y="393.0" text-anchor="middle" font-size="11.5" fill="#01579B">idle stop, limit, logs</text>60+ <text x="455.0" y="393.0" text-anchor="middle" font-size="11.5" fill="#01579B">idle stop · states · logs</text>
61 <g filter="url(#shadow)"><rect x="620" y="300" width="250" height="110" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>61 <g filter="url(#shadow)"><rect x="620" y="300" width="250" height="110" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>
62 <text x="745.0" y="335.0" text-anchor="middle" font-size="22">📋</text>62 <text x="745.0" y="335.0" text-anchor="middle" font-size="22">📋</text>
63 <text x="745.0" y="358.0" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/config</text>63 <text x="745.0" y="358.0" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/config</text>
64- <text x="745.0" y="376.0" text-anchor="middle" font-size="11.5" fill="#01579B">load · validate · watch</text>64+ <text x="745.0" y="376.0" text-anchor="middle" font-size="11.5" fill="#01579B">load · validate · locate</text>
65- <text x="745.0" y="393.0" text-anchor="middle" font-size="11.5" fill="#01579B">save with comments</text>65+ <text x="745.0" y="393.0" text-anchor="middle" font-size="11.5" fill="#01579B">default file · save · watch</text>
66- <g filter="url(#shadow)"><rect x="910" y="300" width="250" height="110" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>66+ <g filter="url(#shadow)"><rect x="910" y="300" width="250" height="110" rx="16" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
67 <text x="1035.0" y="335.0" text-anchor="middle" font-size="22">🧰</text>67 <text x="1035.0" y="335.0" text-anchor="middle" font-size="22">🧰</text>
68- <text x="1035.0" y="358.0" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/workspace</text>68+ <text x="1035.0" y="358.0" text-anchor="middle" font-size="16" font-weight="700" fill="#263238">internal/workspace</text>
69- <text x="1035.0" y="376.0" text-anchor="middle" font-size="11.5" fill="#01579B">files + exec: local,</text>69+ <text x="1035.0" y="376.0" text-anchor="middle" font-size="11.5" fill="#263238">planned · M4</text>
70- <text x="1035.0" y="393.0" text-anchor="middle" font-size="11.5" fill="#01579B">container, sbx</text>70+ <text x="1035.0" y="393.0" text-anchor="middle" font-size="11.5" fill="#263238">files + exec: local, container, sbx</text>
71- <g filter="url(#shadow)"><rect x="40" y="480" width="250" height="105" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>71+ <g filter="url(#shadow)"><rect x="40" y="490" width="250" height="105" rx="16" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
72- <text x="165.0" y="512.5" text-anchor="middle" font-size="22">📥</text>72+ <text x="165.0" y="522.5" text-anchor="middle" font-size="22">📥</text>
73- <text x="165.0" y="535.5" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/store</text>73+ <text x="165.0" y="545.5" text-anchor="middle" font-size="16" font-weight="700" fill="#263238">internal/store</text>
74- <text x="165.0" y="553.5" text-anchor="middle" font-size="11.5" fill="#01579B">Pebble: channels, messages,</text>74+ <text x="165.0" y="563.5" text-anchor="middle" font-size="11.5" fill="#263238">planned · M3</text>
75- <text x="165.0" y="570.5" text-anchor="middle" font-size="11.5" fill="#01579B">session ids</text>75+ <text x="165.0" y="580.5" text-anchor="middle" font-size="11.5" fill="#263238">Pebble: channels, messages</text>
76- <g filter="url(#shadow)"><rect x="330" y="480" width="250" height="105" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>76+ <g filter="url(#shadow)"><rect x="330" y="490" width="250" height="105" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>
77- <text x="455.0" y="512.5" text-anchor="middle" font-size="22">🔧</text>77+ <text x="455.0" y="522.5" text-anchor="middle" font-size="22">🔧</text>
78- <text x="455.0" y="535.5" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/acp</text>78+ <text x="455.0" y="545.5" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/acp</text>
79- <text x="455.0" y="553.5" text-anchor="middle" font-size="11.5" fill="#01579B">ACP v1 client wrapper</text>79+ <text x="455.0" y="563.5" text-anchor="middle" font-size="11.5" fill="#01579B">ACP v1 client over stdio</text>
80- <text x="455.0" y="570.5" text-anchor="middle" font-size="11.5" fill="#01579B">SDK types stay inside</text>80+ <text x="455.0" y="580.5" text-anchor="middle" font-size="11.5" fill="#01579B">SDK types stay inside</text>
81- <g filter="url(#shadow)"><rect x="620" y="480" width="250" height="105" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>81+ <g filter="url(#shadow)"><rect x="620" y="490" width="250" height="105" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>
82- <text x="745.0" y="512.5" text-anchor="middle" font-size="22">🛡️</text>82+ <text x="745.0" y="522.5" text-anchor="middle" font-size="22">🛡️</text>
83- <text x="745.0" y="535.5" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">schema</text>83+ <text x="745.0" y="545.5" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">schema</text>
84- <text x="745.0" y="553.5" text-anchor="middle" font-size="11.5" fill="#01579B">config JSON Schema</text>84+ <text x="745.0" y="563.5" text-anchor="middle" font-size="11.5" fill="#01579B">config JSON Schema</text>
85- <text x="745.0" y="570.5" text-anchor="middle" font-size="11.5" fill="#01579B">ValidateConfig()</text>85+ <text x="745.0" y="580.5" text-anchor="middle" font-size="11.5" fill="#01579B">ValidateConfig()</text>
86- <g filter="url(#shadow)"><rect x="40" y="655" width="250" height="50" rx="12" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>86+ <g filter="url(#shadow)"><rect x="910" y="490" width="250" height="105" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/></g>
87- <text x="165.0" y="685.0" text-anchor="middle" font-size="13" font-weight="700" fill="#263238">cockroachdb/pebble/v2</text>87+ <text x="1035.0" y="522.5" text-anchor="middle" font-size="22">📋</text>
88- <g filter="url(#shadow)"><rect x="330" y="655" width="250" height="50" rx="12" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>88+ <text x="1035.0" y="545.5" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">internal/events</text>
89- <text x="455.0" y="685.0" text-anchor="middle" font-size="13" font-weight="700" fill="#263238">coder/acp-go-sdk → mm -acp</text>89+ <text x="1035.0" y="563.5" text-anchor="middle" font-size="11.5" fill="#01579B">event names + payloads</text>
90- <g filter="url(#shadow)"><rect x="620" y="655" width="250" height="50" rx="12" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>90+ <text x="1035.0" y="580.5" text-anchor="middle" font-size="11.5" fill="#01579B">Emitter interface</text>
91- <text x="745.0" y="685.0" text-anchor="middle" font-size="13" font-weight="700" fill="#263238">jsonschema/v6 · yaml/v3</text>91+ <rect x="920.0" y="438.0" width="60" height="24" rx="5" fill="#FFFFFF"/>
92- <g filter="url(#shadow)"><rect x="910" y="655" width="250" height="50" rx="12" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>92+ <text x="950" y="454.5" text-anchor="middle" font-size="12.5" font-weight="700" fill="#263238">emits</text>
93- <text x="1035.0" y="685.0" text-anchor="middle" font-size="13" font-weight="700" fill="#263238">creack/pty · docker · sbx</text>93+ <g filter="url(#shadow)"><rect x="40" y="670" width="250" height="50" rx="12" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
94- <rect x="475.0" y="608.0" width="50" height="24" rx="5" fill="#FFFFFF"/>94+ <text x="165.0" y="700.0" text-anchor="middle" font-size="13" font-weight="700" fill="#263238">cockroachdb/pebble/v2</text>
95- <text x="500" y="624.5" text-anchor="middle" font-size="12.5" font-weight="700" fill="#263238">stdio</text>95+ <g filter="url(#shadow)"><rect x="330" y="670" width="250" height="50" rx="12" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
96- <text x="600" y="750" text-anchor="middle" font-size="12.5" fill="#455A64" font-style="italic">Third-party modules in grey. Wails v3 is imported by cmd/sidekicks and internal/app only.</text>96+ <text x="455.0" y="700.0" text-anchor="middle" font-size="13" font-weight="700" fill="#263238">coder/acp-go-sdk → mm -acp</text>
97+ <g filter="url(#shadow)"><rect x="620" y="670" width="250" height="50" rx="12" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
98+ <text x="745.0" y="700.0" text-anchor="middle" font-size="13" font-weight="700" fill="#263238">jsonschema/v6 · yaml/v3</text>
99+ <g filter="url(#shadow)"><rect x="910" y="670" width="250" height="50" rx="12" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/></g>
100+ <text x="1035.0" y="700.0" text-anchor="middle" font-size="13" font-weight="700" fill="#263238">fsnotify (config watch)</text>
101+ <rect x="475.0" y="620.0" width="50" height="24" rx="5" fill="#FFFFFF"/>
102+ <text x="500" y="636.5" text-anchor="middle" font-size="12.5" font-weight="700" fill="#263238">stdio</text>
103+ <text x="600" y="765" text-anchor="middle" font-size="12.5" fill="#455A64" font-style="italic">internal/app also emits through internal/events; internal/config imports schema and fsnotify. Third-party modules in grey chips.</text>
97 </svg>104 </svg>
modified docs/en/README.md +4 -4
@@ -1,14 +1,14 @@
11 # Sidekicks documentation (English)
22
3-Sidekicks is a desktop application to talk with AI agents in channels, through the Agent Client Protocol, designed for people who are not developers. The project is at its start: on 29 October 2026 the decisions, the configuration format and the test tooling exist; the application does not yet.
3+Sidekicks is a desktop application to talk with AI agents in channels, through the Agent Client Protocol, designed for people who are not developers. Milestone M1 (Foundations) is done: the application opens, reads its configuration, starts and stops agents and shows their state and logs. Conversations arrive with milestone M2.
44
55 This documentation follows [Diátaxis](https://diataxis.fr): pick the section that matches what you need right now.
66
77 | You want to… | Section | Pages |
88 | --- | --- | --- |
9-| **learn** by doing | Tutorials | [Your first ACP exchange with a test agent](tutorials/first-acp-exchange.md) |
10-| **do** a specific task | How-to guides | [Run the tests](how-to/run-the-tests.md) · [Run a test agent](how-to/run-a-test-agent.md) · [Test milestone Initialize on macOS](how-to/test-milestone-initialize-on-macos.md) |
11-| **look up** an exact detail | Reference | [The configuration file](reference/configuration.md) · [scripts/agents.sh](reference/agents-script.md) |
9+| **learn** by doing | Tutorials | [Your first launch of Sidekicks](tutorials/first-launch.md) · [Your first ACP exchange with a test agent](tutorials/first-acp-exchange.md) |
10+| **do** a specific task | How-to guides | [Develop the application](how-to/develop-the-application.md) · [Run the tests](how-to/run-the-tests.md) · [Run a test agent](how-to/run-a-test-agent.md) · [Test milestone M1 on macOS](how-to/test-milestone-m1-on-macos.md) · [Test milestone Initialize on macOS](how-to/test-milestone-initialize-on-macos.md) |
11+| **look up** an exact detail | Reference | [The configuration file](reference/configuration.md) · [Agent states and events](reference/agents-and-events.md) · [Commands](reference/commands.md) · [scripts/agents.sh](reference/agents-script.md) |
1212 | **understand** why | Explanation | [Why ACP](explanation/why-acp.md) · [The interface stack](explanation/ui-stack.md) · [Architecture](explanation/architecture.md) · [Memory and local models](explanation/memory-and-local-models.md) |
1313
1414 ## Design records
@@ -1,14 +1,14 @@
1 # Sidekicks documentation (English)1 # Sidekicks documentation (English)
2 2
3-Sidekicks is a desktop application to talk with AI agents in channels, through the Agent Client Protocol, designed for people who are not developers. The project is at its start: on 29 October 2026 the decisions, the configuration format and the test tooling exist; the application does not yet.3+Sidekicks is a desktop application to talk with AI agents in channels, through the Agent Client Protocol, designed for people who are not developers. Milestone M1 (Foundations) is done: the application opens, reads its configuration, starts and stops agents and shows their state and logs. Conversations arrive with milestone M2.
4 4
5 This documentation follows [Diátaxis](https://diataxis.fr): pick the section that matches what you need right now.5 This documentation follows [Diátaxis](https://diataxis.fr): pick the section that matches what you need right now.
6 6
7 | You want to… | Section | Pages |7 | You want to… | Section | Pages |
8 | --- | --- | --- |8 | --- | --- | --- |
9-| **learn** by doing | Tutorials | [Your first ACP exchange with a test agent](tutorials/first-acp-exchange.md) |9+| **learn** by doing | Tutorials | [Your first launch of Sidekicks](tutorials/first-launch.md) · [Your first ACP exchange with a test agent](tutorials/first-acp-exchange.md) |
10-| **do** a specific task | How-to guides | [Run the tests](how-to/run-the-tests.md) · [Run a test agent](how-to/run-a-test-agent.md) · [Test milestone Initialize on macOS](how-to/test-milestone-initialize-on-macos.md) |10+| **do** a specific task | How-to guides | [Develop the application](how-to/develop-the-application.md) · [Run the tests](how-to/run-the-tests.md) · [Run a test agent](how-to/run-a-test-agent.md) · [Test milestone M1 on macOS](how-to/test-milestone-m1-on-macos.md) · [Test milestone Initialize on macOS](how-to/test-milestone-initialize-on-macos.md) |
11-| **look up** an exact detail | Reference | [The configuration file](reference/configuration.md) · [scripts/agents.sh](reference/agents-script.md) |11+| **look up** an exact detail | Reference | [The configuration file](reference/configuration.md) · [Agent states and events](reference/agents-and-events.md) · [Commands](reference/commands.md) · [scripts/agents.sh](reference/agents-script.md) |
12 | **understand** why | Explanation | [Why ACP](explanation/why-acp.md) · [The interface stack](explanation/ui-stack.md) · [Architecture](explanation/architecture.md) · [Memory and local models](explanation/memory-and-local-models.md) |12 | **understand** why | Explanation | [Why ACP](explanation/why-acp.md) · [The interface stack](explanation/ui-stack.md) · [Architecture](explanation/architecture.md) · [Memory and local models](explanation/memory-and-local-models.md) |
13 13
14 ## Design records14 ## Design records
modified docs/en/explanation/architecture.md +5 -1
@@ -6,7 +6,7 @@ Before writing the application, the project fixed how its code is divided, what
66
77 ![Packages and dependencies](../../diagrams/packages.png)
88
9-The diagram's source is [packages.drawio](../../diagrams/packages.drawio). On 29 October 2026 only the `schema` package exists; the others are the plan for the next milestones.
9+The diagram's source is [packages.drawio](../../diagrams/packages.drawio). Blue boxes exist since milestone M1; grey ones are planned.
1010
1111 ## Why this split
1212
@@ -16,6 +16,10 @@ Three facts drove it:
1616 - **The interface must not know ACP.** If the Svelte code handled ACP messages, changing protocol version or agent would ripple into the interface. Everything the interface sees goes through `internal/app`, as Sidekicks' own calls and events.
1717 - **The configuration can change while agents run.** It is loaded, validated and watched in one place (`internal/config`), which tells the others.
1818
19+## Where Wails stops
20+
21+Only `main.go`, at the root of the repository, knows Wails: it creates the window, binds the services of `internal/app` and plugs the Wails event manager in. Everything below it is plain Go that emits through a small `Emitter` interface, so the agent manager, the configuration and the services are tested without a window. The main package sits at the root rather than in `cmd/` because the build files Wails generates expect it there; fighting the tool would cost more than it gives.
22+
1923 ## What lives in the YAML and what lives in the database
2024
2125 The YAML file describes **what exists**: agents, limits, preferences, and channels to create. It is meant to be read and edited by a person. What **happens** — the channels created from the interface, who joined them, the messages — is stored by the application in Pebble, an embedded key-value store. A channel declared in YAML is created once at startup and then lives in the database like any other, so that editing the YAML never deletes a conversation by accident.
@@ -6,7 +6,7 @@ Before writing the application, the project fixed how its code is divided, what
6 6
7 ![Packages and dependencies](../../diagrams/packages.png)7 ![Packages and dependencies](../../diagrams/packages.png)
8 8
9-The diagram's source is [packages.drawio](../../diagrams/packages.drawio). On 29 October 2026 only the `schema` package exists; the others are the plan for the next milestones.9+The diagram's source is [packages.drawio](../../diagrams/packages.drawio). Blue boxes exist since milestone M1; grey ones are planned.
10 10
11 ## Why this split11 ## Why this split
12 12
@@ -16,6 +16,10 @@ Three facts drove it:
16 - **The interface must not know ACP.** If the Svelte code handled ACP messages, changing protocol version or agent would ripple into the interface. Everything the interface sees goes through `internal/app`, as Sidekicks' own calls and events.16 - **The interface must not know ACP.** If the Svelte code handled ACP messages, changing protocol version or agent would ripple into the interface. Everything the interface sees goes through `internal/app`, as Sidekicks' own calls and events.
17 - **The configuration can change while agents run.** It is loaded, validated and watched in one place (`internal/config`), which tells the others.17 - **The configuration can change while agents run.** It is loaded, validated and watched in one place (`internal/config`), which tells the others.
18 18
19+## Where Wails stops
20+
21+Only `main.go`, at the root of the repository, knows Wails: it creates the window, binds the services of `internal/app` and plugs the Wails event manager in. Everything below it is plain Go that emits through a small `Emitter` interface, so the agent manager, the configuration and the services are tested without a window. The main package sits at the root rather than in `cmd/` because the build files Wails generates expect it there; fighting the tool would cost more than it gives.
22+
19 ## What lives in the YAML and what lives in the database23 ## What lives in the YAML and what lives in the database
20 24
21 The YAML file describes **what exists**: agents, limits, preferences, and channels to create. It is meant to be read and edited by a person. What **happens** — the channels created from the interface, who joined them, the messages — is stored by the application in Pebble, an embedded key-value store. A channel declared in YAML is created once at startup and then lives in the database like any other, so that editing the YAML never deletes a conversation by accident.25 The YAML file describes **what exists**: agents, limits, preferences, and channels to create. It is meant to be read and edited by a person. What **happens** — the channels created from the interface, who joined them, the messages — is stored by the application in Pebble, an embedded key-value store. A channel declared in YAML is created once at startup and then lives in the database like any other, so that editing the YAML never deletes a conversation by accident.
modified docs/en/explanation/memory-and-local-models.md +2 -2
@@ -12,11 +12,11 @@ Because the guard lives in a script that passes stdio through, it also protects
1212
1313 ## In the product
1414
15-The same reasoning applies to the people who will use Sidekicks on their laptops, so the architecture carries it ([ADR 0003](../../adr/0003-architecture.md)):
15+The same reasoning applies to the people who will use Sidekicks on their laptops, so the application applies it (`internal/agents`, milestone M1; [ADR 0003](../../adr/0003-architecture.md)):
1616
1717 - agents start **when first called**, not when the application opens;
1818 - an agent unused for a while (ten minutes by default) is stopped to free its model;
19-- a limit on simultaneous agents (`limits.maxRunningAgents`, two by default) is part of the [configuration](../reference/configuration.md);
19+- a limit on simultaneous agents (`limits.maxRunningAgents`, two by default) is part of the [configuration](../reference/configuration.md). When it is reached, the agent nobody has used for the longest time makes room — provided it is idle: an agent that is answering is never interrupted, and the start is refused instead;
2020 - in a channel with several agents, only the agents **mentioned** answer, one after the other by default, rather than every agent at once.
2121
2222 ## Rejected alternatives
@@ -12,11 +12,11 @@ Because the guard lives in a script that passes stdio through, it also protects
12 12
13 ## In the product13 ## In the product
14 14
15-The same reasoning applies to the people who will use Sidekicks on their laptops, so the architecture carries it ([ADR 0003](../../adr/0003-architecture.md)):15+The same reasoning applies to the people who will use Sidekicks on their laptops, so the application applies it (`internal/agents`, milestone M1; [ADR 0003](../../adr/0003-architecture.md)):
16 16
17 - agents start **when first called**, not when the application opens;17 - agents start **when first called**, not when the application opens;
18 - an agent unused for a while (ten minutes by default) is stopped to free its model;18 - an agent unused for a while (ten minutes by default) is stopped to free its model;
19-- a limit on simultaneous agents (`limits.maxRunningAgents`, two by default) is part of the [configuration](../reference/configuration.md);19+- a limit on simultaneous agents (`limits.maxRunningAgents`, two by default) is part of the [configuration](../reference/configuration.md). When it is reached, the agent nobody has used for the longest time makes room — provided it is idle: an agent that is answering is never interrupted, and the start is refused instead;
20 - in a channel with several agents, only the agents **mentioned** answer, one after the other by default, rather than every agent at once.20 - in a channel with several agents, only the agents **mentioned** answer, one after the other by default, rather than every agent at once.
21 21
22 ## Rejected alternatives22 ## Rejected alternatives
added docs/en/how-to/develop-the-application.md +49 -0
new file mode 100644
@@ -0,0 +1,49 @@
1+# How to develop the application
2+
3+This guide shows how to set up a machine, run Sidekicks with hot reload and build it. It assumes you know Go and a terminal.
4+
5+## Install the tools
6+
7+| Tool | Version | macOS | Linux (Debian/Ubuntu) |
8+| --- | --- | --- | --- |
9+| Go | ≥ 1.26 | `brew install go` | distribution package or go.dev |
10+| Node | ≥ 20 | `brew install node` | distribution package |
11+| pnpm | 12.11.2 | `npm install -g pnpm@12.11.2` | same |
12+| Wails CLI | v3.0.0 | `go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0` | same |
13+| Native toolkit | — | `xcode-select --install` | `sudo apt-get install pkg-config libgtk-4-dev libwebkitgtk-6.0-dev` |
14+
15+Then check with `wails3 doctor`.
16+
17+## Install the dependencies and run
18+
19+```bash
20+make install
21+SIDEKICKS_CONFIG="$PWD/examples/test-agents.yaml" make dev
22+```
23+
24+`make dev` opens the window and reloads the interface when a Svelte file changes, and rebuilds the Go side when a Go file changes. Without `SIDEKICKS_CONFIG`, Sidekicks uses (and creates) the configuration at its default location.
25+
26+## Build
27+
28+```bash
29+make build # bin/sidekicks
30+make package # macOS: bin/sidekicks.app, signed ad hoc
31+```
32+
33+## When you change…
34+
35+- **a Go service or an event payload** (`internal/app`, `internal/events`, `main.go`): run `make bindings` and commit `frontend/bindings` — the CI fails when they are stale.
36+- **the configuration format**: update `schema/config.schema.json`, `internal/config`, the testdata and the [configuration reference](../reference/configuration.md).
37+- **a package or its imports**: update `docs/diagrams/packages.drawio` (its generator is described in the [architecture explanation](../explanation/architecture.md)).
38+- **any source file you create**: start it with the licence header (`make lint-headers` checks it).
39+
40+## Variants
41+
42+- **The repository is shared between macOS and a Linux machine or sandbox**: nothing special to do — `frontend/pnpm-workspace.yaml` makes pnpm install the native binaries of both systems in the same `node_modules`. With npm instead of pnpm, each system would break the other.
43+- **Use the test agents without `SIDEKICKS_CONFIG`** (including the packaged `.app`): `make install-test-config` writes them at the default location, with absolute paths and the environment an application opened from the Finder lacks.
44+- **Run without hot reload**: `make frontend && go build -o bin/sidekicks . && bin/sidekicks --config examples/test-agents.yaml`.
45+
46+## See also
47+
48+- [How to run the tests](run-the-tests.md) · [Reference: commands](../reference/commands.md)
49+- Why this stack: [The interface stack](../explanation/ui-stack.md)
new file mode 100644
@@ -0,0 +1,49 @@
1+# How to develop the application
2+
3+This guide shows how to set up a machine, run Sidekicks with hot reload and build it. It assumes you know Go and a terminal.
4+
5+## Install the tools
6+
7+| Tool | Version | macOS | Linux (Debian/Ubuntu) |
8+| --- | --- | --- | --- |
9+| Go | ≥ 1.26 | `brew install go` | distribution package or go.dev |
10+| Node | ≥ 20 | `brew install node` | distribution package |
11+| pnpm | 12.11.2 | `npm install -g pnpm@12.11.2` | same |
12+| Wails CLI | v3.0.0 | `go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0` | same |
13+| Native toolkit | — | `xcode-select --install` | `sudo apt-get install pkg-config libgtk-4-dev libwebkitgtk-6.0-dev` |
14+
15+Then check with `wails3 doctor`.
16+
17+## Install the dependencies and run
18+
19+```bash
20+make install
21+SIDEKICKS_CONFIG="$PWD/examples/test-agents.yaml" make dev
22+```
23+
24+`make dev` opens the window and reloads the interface when a Svelte file changes, and rebuilds the Go side when a Go file changes. Without `SIDEKICKS_CONFIG`, Sidekicks uses (and creates) the configuration at its default location.
25+
26+## Build
27+
28+```bash
29+make build # bin/sidekicks
30+make package # macOS: bin/sidekicks.app, signed ad hoc
31+```
32+
33+## When you change…
34+
35+- **a Go service or an event payload** (`internal/app`, `internal/events`, `main.go`): run `make bindings` and commit `frontend/bindings` — the CI fails when they are stale.
36+- **the configuration format**: update `schema/config.schema.json`, `internal/config`, the testdata and the [configuration reference](../reference/configuration.md).
37+- **a package or its imports**: update `docs/diagrams/packages.drawio` (its generator is described in the [architecture explanation](../explanation/architecture.md)).
38+- **any source file you create**: start it with the licence header (`make lint-headers` checks it).
39+
40+## Variants
41+
42+- **The repository is shared between macOS and a Linux machine or sandbox**: nothing special to do — `frontend/pnpm-workspace.yaml` makes pnpm install the native binaries of both systems in the same `node_modules`. With npm instead of pnpm, each system would break the other.
43+- **Use the test agents without `SIDEKICKS_CONFIG`** (including the packaged `.app`): `make install-test-config` writes them at the default location, with absolute paths and the environment an application opened from the Finder lacks.
44+- **Run without hot reload**: `make frontend && go build -o bin/sidekicks . && bin/sidekicks --config examples/test-agents.yaml`.
45+
46+## See also
47+
48+- [How to run the tests](run-the-tests.md) · [Reference: commands](../reference/commands.md)
49+- Why this stack: [The interface stack](../explanation/ui-stack.md)
modified docs/en/how-to/run-the-tests.md +26 -13
@@ -1,36 +1,49 @@
11 # How to run the tests
22
3-This guide shows how to run the project's test suite before you commit. It assumes a clone of the repository and a terminal at its root.
3+This guide shows how to check the project before you commit or open a merge request. It assumes the development prerequisites are installed (see [How to develop the application](develop-the-application.md)) and a terminal at the root of the repository.
44
5-## Run everything
5+## Run what the CI runs
66
77 ```bash
8-make test
8+make check
99 ```
1010
11-It runs the Go unit tests (`go test ./...`) and the tests of the shell tooling (`scripts/agents_test.sh`). Neither needs a real agent, a model or the network: `mm` and llmman are replaced by fakes. A green run ends with:
11+It runs the linters (`make lint`), then every test that needs no real agent, no model and no network (`make test`). A green run ends with:
1212
1313 ```text
14-ok git.rickub.com/bots-garden/sidekicks/schema …
1514 14 passed, 0 failed
15+all licence header tests passed
1616 ```
1717
18-`make help` lists the targets.
18+The CI (`.github/workflows/ci.yml`) runs the same command, plus `make bindings-check`.
1919
2020 ## Run one part
2121
22-| To test | Command |
22+| To check | Command |
2323 | --- | --- |
24-| The Go packages | `make test-go`, or `go test ./schema -run Invalid -v` for one test with its output |
25-| `scripts/agents.sh` | `make test-scripts` |
24+| Go packages | `make test-go`, or `go test -race ./internal/agents -run Limit -v` for one test |
25+| Frontend (vitest) | `make test-frontend` |
26+| Shell tooling | `make test-scripts` |
27+| Go lint (golangci-lint, pinned in `tools/go.mod`) | `make lint-go` |
28+| Frontend types and Svelte warnings | `make lint-frontend` |
29+| Licence headers | `make lint-headers` |
30+| Committed TypeScript bindings are up to date | `make bindings-check` |
31+
32+## Run the tests against the real agents
33+
34+```bash
35+make test-real
36+```
37+
38+It runs the tests built with the `realagents` tag: today, the ACP handshake with Alice. They need `mm` (or `MM_BIN`) and llmman, and start agents only through `scripts/agents.sh`, so the two-agents limit and the unloading apply.
2639
2740 ## Variants
2841
29-- **The spikes** (`spikes/acp-trace`, `spikes/wails-ui`) are separate Go modules with no unit tests; `make test` does not build them. Check them with `go vet .` inside each folder, and `npx svelte-check` in `spikes/wails-ui/frontend`.
30-- **Tests against real agents** do not exist yet; when they do (issue #10), they will use the `realagents` build tag and go through `scripts/agents.sh`.
31-- **`make: warning: Clock skew detected`** appears when the repository sits on a folder shared with another machine whose clock differs; it does not affect the result.
42+- **`go test` complains about `frontend/dist`**: the Go binary embeds the built frontend. `make test-go` builds it when missing; with plain `go test`, run `make frontend` once.
43+- **The spikes** (`spikes/`) are separate modules and are not tested; check them with `go vet .` inside each folder.
44+- **`make: warning: Clock skew detected`** appears when the repository is on a folder shared with another machine; it does not change the result.
3245
3346 ## See also
3447
35-- What the configuration tests check → [Reference: the configuration file](../reference/configuration.md)
48+- Every target → [Reference: commands](../reference/commands.md)
3649 - The tooling under test → [Reference: scripts/agents.sh](../reference/agents-script.md)
@@ -1,36 +1,49 @@
1 # How to run the tests1 # How to run the tests
2 2
3-This guide shows how to run the project's test suite before you commit. It assumes a clone of the repository and a terminal at its root.3+This guide shows how to check the project before you commit or open a merge request. It assumes the development prerequisites are installed (see [How to develop the application](develop-the-application.md)) and a terminal at the root of the repository.
4 4
5-## Run everything5+## Run what the CI runs
6 6
7 ```bash7 ```bash
8-make test8+make check
9 ```9 ```
10 10
11-It runs the Go unit tests (`go test ./...`) and the tests of the shell tooling (`scripts/agents_test.sh`). Neither needs a real agent, a model or the network: `mm` and llmman are replaced by fakes. A green run ends with:11+It runs the linters (`make lint`), then every test that needs no real agent, no model and no network (`make test`). A green run ends with:
12 12
13 ```text13 ```text
14-ok git.rickub.com/bots-garden/sidekicks/schema …
15 14 passed, 0 failed14 14 passed, 0 failed
15+all licence header tests passed
16 ```16 ```
17 17
18-`make help` lists the targets.18+The CI (`.github/workflows/ci.yml`) runs the same command, plus `make bindings-check`.
19 19
20 ## Run one part20 ## Run one part
21 21
22-| To test | Command |22+| To check | Command |
23 | --- | --- |23 | --- | --- |
24-| The Go packages | `make test-go`, or `go test ./schema -run Invalid -v` for one test with its output |24+| Go packages | `make test-go`, or `go test -race ./internal/agents -run Limit -v` for one test |
25-| `scripts/agents.sh` | `make test-scripts` |25+| Frontend (vitest) | `make test-frontend` |
26+| Shell tooling | `make test-scripts` |
27+| Go lint (golangci-lint, pinned in `tools/go.mod`) | `make lint-go` |
28+| Frontend types and Svelte warnings | `make lint-frontend` |
29+| Licence headers | `make lint-headers` |
30+| Committed TypeScript bindings are up to date | `make bindings-check` |
31+
32+## Run the tests against the real agents
33+
34+```bash
35+make test-real
36+```
37+
38+It runs the tests built with the `realagents` tag: today, the ACP handshake with Alice. They need `mm` (or `MM_BIN`) and llmman, and start agents only through `scripts/agents.sh`, so the two-agents limit and the unloading apply.
26 39
27 ## Variants40 ## Variants
28 41
29-- **The spikes** (`spikes/acp-trace`, `spikes/wails-ui`) are separate Go modules with no unit tests; `make test` does not build them. Check them with `go vet .` inside each folder, and `npx svelte-check` in `spikes/wails-ui/frontend`.42+- **`go test` complains about `frontend/dist`**: the Go binary embeds the built frontend. `make test-go` builds it when missing; with plain `go test`, run `make frontend` once.
30-- **Tests against real agents** do not exist yet; when they do (issue #10), they will use the `realagents` build tag and go through `scripts/agents.sh`.43+- **The spikes** (`spikes/`) are separate modules and are not tested; check them with `go vet .` inside each folder.
31-- **`make: warning: Clock skew detected`** appears when the repository sits on a folder shared with another machine whose clock differs; it does not affect the result.44+- **`make: warning: Clock skew detected`** appears when the repository is on a folder shared with another machine; it does not change the result.
32 45
33 ## See also46 ## See also
34 47
35-- What the configuration tests check → [Reference: the configuration file](../reference/configuration.md)48+- Every target → [Reference: commands](../reference/commands.md)
36 - The tooling under test → [Reference: scripts/agents.sh](../reference/agents-script.md)49 - The tooling under test → [Reference: scripts/agents.sh](../reference/agents-script.md)
added docs/en/how-to/test-milestone-m1-on-macos.md +112 -0
new file mode 100644
@@ -0,0 +1,112 @@
1+# How to test milestone M1 on macOS
2+
3+This guide checks on a Mac (Apple Silicon) what milestone M1 (Foundations) produced: the build, the automated checks, the configuration loading, the agent manager and the panel shell. It assumes the Mac was prepared for [milestone Initialize](test-milestone-initialize-on-macos.md) (Go, Node, Xcode tools, Wails CLI, `mm`, llmman and the models) and the repository is on the M1 branch. Allow 30 minutes.
4+
5+## 0. Prepare
6+
7+```bash
8+npm install -g pnpm@12.11.2
9+pnpm --version # 12.11.2
10+make install
11+```
12+
13+`make install` installs the frontend dependencies with the native binaries of macOS **and** Linux, so the shared folder keeps working in the Linux sandbox.
14+
15+- [ ] `make install` ends without errors.
16+
17+## 1. The automated checks (#8)
18+
19+```bash
20+make check
21+```
22+
23+Expected at the end: `14 passed, 0 failed` and `all licence header tests passed`, with `0 issues.` from golangci-lint before.
24+
25+```bash
26+make test-real
27+```
28+
29+Expected: `ok … internal/acp` — the ACP handshake with the real Alice. `scripts/agents.sh ps` lists no agent afterwards.
30+
31+- [ ] `make check` passes.
32+- [ ] `make test-real` passes.
33+
34+## 2. The application with the test agents (#10, #11, #12, #13)
35+
36+```bash
37+SIDEKICKS_CONFIG="$PWD/examples/test-agents.yaml" make dev
38+```
39+
40+Check, in order:
41+
42+1. **Window**: the left dock starts below the red/orange/green buttons; the status bar says « 0 agent actif » and shows the configuration path.
43+2. **Agents**: Alice, Bob, Riker, Garfield are listed, « arrêté ».
44+3. **Start**: hover Alice, click ▶ — orange (« démarre… »), then green (« prêt »); « 1 agent actif ».
45+4. **Journal**: click **Journal** — the panel opens at a comfortable size with Alice's `mm v1.0.3 | …` lines; the selector shows Alice.
46+5. **Limit**: start Bob, then Riker. Riker starts and **Alice** (the least recently used) goes back to « arrêté »: two agents at most. In a terminal, `scripts/agents.sh ps` lists two agents.
47+6. **Stop**: hover Bob, click ■ — « arrêté ».
48+7. **Docks**: click **Agents**, **Journal**, **Settings** in the status bar — each dock folds and unfolds; drag the separators, quit (⌘Q) and relaunch: the sizes are kept.
49+8. **Quit with agents running**: with Riker still running, quit (⌘Q). `scripts/agents.sh ps` lists no agent and no model.
50+9. **Theme**: System Settings → Appearance → Light / Dark: the whole window follows.
51+
52+- [ ] Points 1 to 9 are as described.
53+
54+## 3. The configuration (#9)
55+
56+Follow steps 5 and 6 of the tutorial [Your first launch of Sidekicks](../tutorials/first-launch.md): a typo in the file shows a red banner and located messages within a second, and fixing it removes them without restarting.
57+
58+Then check the default file: quit, move your configuration away if you have one, and run without `SIDEKICKS_CONFIG`:
59+
60+```bash
61+mv ~/Library/Application\ Support/sidekicks/config.yaml /tmp/ 2>/dev/null
62+make dev
63+```
64+
65+Settings shows the full path `/Users/<you>/Library/Application Support/sidekicks/config.yaml`, « emplacement par défaut — créé pour vous » and « ✓ Configuration valide ». Open the file to check that every key has a comment — without quotes around `~`, or the shell does not replace it with your home folder:
66+
67+```bash
68+open ~/Library/Application\ Support/sidekicks/ # the folder, in the Finder
69+open -e ~/Library/Application\ Support/sidekicks/config.yaml # the file, in TextEdit
70+```
71+
72+To make the default configuration work with the test agents, write it with absolute paths to the repository (the empty file is kept as `.bak`):
73+
74+```bash
75+make install-test-config
76+```
77+
78+It also gives each agent `MM_BIN` and your `PATH`: an application opened from the Finder only gets a minimal `PATH` and would find neither `mm` nor `jq`. If you would rather get your own file back, put it back instead:
79+
80+```bash
81+mv /tmp/config.yaml ~/Library/Application\ Support/sidekicks/config.yaml
82+```
83+
84+- [ ] Problems are located and the file reloads on save.
85+- [ ] The default file is created, commented and valid.
86+
87+## 4. The packaged application
88+
89+```bash
90+make package
91+open bin/sidekicks.app
92+```
93+
94+Without `SIDEKICKS_CONFIG`, it opens the default configuration — the test agents' one after `make install-test-config`. Repeat points 1, 3 and 7 of part 2.
95+
96+- [ ] `bin/sidekicks.app` opens and behaves as in development mode.
97+
98+If everything is as described, M1 is confirmed: close issue #12 and set milestone **M1 · Fondations** to `closed` in `.tickets/milestones.yaml` — or ask the assistant to do it. Otherwise, comment the issue concerned with what differs.
99+
100+## Troubleshooting
101+
102+| Symptom | Likely cause | Fix |
103+| --- | --- | --- |
104+| `pnpm: command not found` | pnpm not installed | `npm install -g pnpm@12.11.2` |
105+| `Cannot find native binding` during the frontend build | `node_modules` installed with npm, or before `pnpm-workspace.yaml` existed | delete `frontend/node_modules`, run `make install` |
106+| `wails3: command not found` | `GOPATH/bin` not on `PATH` | `export PATH="$(go env GOPATH)/bin:$PATH"` |
107+| An agent goes to « erreur » at once | `mm` or llmman missing | read the message under the agent and its Journal |
108+| `make check` fails in `bindings` code | bindings older than the Go code | `make bindings` |
109+
110+## See also
111+
112+- [How to develop the application](develop-the-application.md) · [Reference: agent states and events](../reference/agents-and-events.md)
new file mode 100644
@@ -0,0 +1,112 @@
1+# How to test milestone M1 on macOS
2+
3+This guide checks on a Mac (Apple Silicon) what milestone M1 (Foundations) produced: the build, the automated checks, the configuration loading, the agent manager and the panel shell. It assumes the Mac was prepared for [milestone Initialize](test-milestone-initialize-on-macos.md) (Go, Node, Xcode tools, Wails CLI, `mm`, llmman and the models) and the repository is on the M1 branch. Allow 30 minutes.
4+
5+## 0. Prepare
6+
7+```bash
8+npm install -g pnpm@12.11.2
9+pnpm --version # 12.11.2
10+make install
11+```
12+
13+`make install` installs the frontend dependencies with the native binaries of macOS **and** Linux, so the shared folder keeps working in the Linux sandbox.
14+
15+- [ ] `make install` ends without errors.
16+
17+## 1. The automated checks (#8)
18+
19+```bash
20+make check
21+```
22+
23+Expected at the end: `14 passed, 0 failed` and `all licence header tests passed`, with `0 issues.` from golangci-lint before.
24+
25+```bash
26+make test-real
27+```
28+
29+Expected: `ok … internal/acp` — the ACP handshake with the real Alice. `scripts/agents.sh ps` lists no agent afterwards.
30+
31+- [ ] `make check` passes.
32+- [ ] `make test-real` passes.
33+
34+## 2. The application with the test agents (#10, #11, #12, #13)
35+
36+```bash
37+SIDEKICKS_CONFIG="$PWD/examples/test-agents.yaml" make dev
38+```
39+
40+Check, in order:
41+
42+1. **Window**: the left dock starts below the red/orange/green buttons; the status bar says « 0 agent actif » and shows the configuration path.
43+2. **Agents**: Alice, Bob, Riker, Garfield are listed, « arrêté ».
44+3. **Start**: hover Alice, click ▶ — orange (« démarre… »), then green (« prêt »); « 1 agent actif ».
45+4. **Journal**: click **Journal** — the panel opens at a comfortable size with Alice's `mm v1.0.3 | …` lines; the selector shows Alice.
46+5. **Limit**: start Bob, then Riker. Riker starts and **Alice** (the least recently used) goes back to « arrêté »: two agents at most. In a terminal, `scripts/agents.sh ps` lists two agents.
47+6. **Stop**: hover Bob, click ■ — « arrêté ».
48+7. **Docks**: click **Agents**, **Journal**, **Settings** in the status bar — each dock folds and unfolds; drag the separators, quit (⌘Q) and relaunch: the sizes are kept.
49+8. **Quit with agents running**: with Riker still running, quit (⌘Q). `scripts/agents.sh ps` lists no agent and no model.
50+9. **Theme**: System Settings → Appearance → Light / Dark: the whole window follows.
51+
52+- [ ] Points 1 to 9 are as described.
53+
54+## 3. The configuration (#9)
55+
56+Follow steps 5 and 6 of the tutorial [Your first launch of Sidekicks](../tutorials/first-launch.md): a typo in the file shows a red banner and located messages within a second, and fixing it removes them without restarting.
57+
58+Then check the default file: quit, move your configuration away if you have one, and run without `SIDEKICKS_CONFIG`:
59+
60+```bash
61+mv ~/Library/Application\ Support/sidekicks/config.yaml /tmp/ 2>/dev/null
62+make dev
63+```
64+
65+Settings shows the full path `/Users/<you>/Library/Application Support/sidekicks/config.yaml`, « emplacement par défaut — créé pour vous » and « ✓ Configuration valide ». Open the file to check that every key has a comment — without quotes around `~`, or the shell does not replace it with your home folder:
66+
67+```bash
68+open ~/Library/Application\ Support/sidekicks/ # the folder, in the Finder
69+open -e ~/Library/Application\ Support/sidekicks/config.yaml # the file, in TextEdit
70+```
71+
72+To make the default configuration work with the test agents, write it with absolute paths to the repository (the empty file is kept as `.bak`):
73+
74+```bash
75+make install-test-config
76+```
77+
78+It also gives each agent `MM_BIN` and your `PATH`: an application opened from the Finder only gets a minimal `PATH` and would find neither `mm` nor `jq`. If you would rather get your own file back, put it back instead:
79+
80+```bash
81+mv /tmp/config.yaml ~/Library/Application\ Support/sidekicks/config.yaml
82+```
83+
84+- [ ] Problems are located and the file reloads on save.
85+- [ ] The default file is created, commented and valid.
86+
87+## 4. The packaged application
88+
89+```bash
90+make package
91+open bin/sidekicks.app
92+```
93+
94+Without `SIDEKICKS_CONFIG`, it opens the default configuration — the test agents' one after `make install-test-config`. Repeat points 1, 3 and 7 of part 2.
95+
96+- [ ] `bin/sidekicks.app` opens and behaves as in development mode.
97+
98+If everything is as described, M1 is confirmed: close issue #12 and set milestone **M1 · Fondations** to `closed` in `.tickets/milestones.yaml` — or ask the assistant to do it. Otherwise, comment the issue concerned with what differs.
99+
100+## Troubleshooting
101+
102+| Symptom | Likely cause | Fix |
103+| --- | --- | --- |
104+| `pnpm: command not found` | pnpm not installed | `npm install -g pnpm@12.11.2` |
105+| `Cannot find native binding` during the frontend build | `node_modules` installed with npm, or before `pnpm-workspace.yaml` existed | delete `frontend/node_modules`, run `make install` |
106+| `wails3: command not found` | `GOPATH/bin` not on `PATH` | `export PATH="$(go env GOPATH)/bin:$PATH"` |
107+| An agent goes to « erreur » at once | `mm` or llmman missing | read the message under the agent and its Journal |
108+| `make check` fails in `bindings` code | bindings older than the Go code | `make bindings` |
109+
110+## See also
111+
112+- [How to develop the application](develop-the-application.md) · [Reference: agent states and events](../reference/agents-and-events.md)
added docs/en/reference/agents-and-events.md +51 -0
new file mode 100644
@@ -0,0 +1,51 @@
1+# Reference: agent states and events
2+
3+## Agent states
4+
5+| State | Shown as | Meaning |
6+| --- | --- | --- |
7+| `stopped` | arrêté | No process. The initial state of every agent. |
8+| `starting` | démarre… | The process is launched and the ACP handshake (`initialize`) is in progress. |
9+| `ready` | prêt | Running and idle. |
10+| `busy` | occupé | Running and used by at least one conversation. |
11+| `error` | erreur | The start failed, or the process exited on its own (crash). The status carries the reason, followed by the agent's last stderr lines for a failed start. |
12+
13+`outdated` (shown as « à redémarrer ») is set on a running agent whose definition changed in the configuration; a restart applies it.
14+
15+## Transitions
16+
17+| From | Event | To |
18+| --- | --- | --- |
19+| `stopped`, `error` | start requested (▶ in the Agents panel, or first use) | `starting` |
20+| `starting` | handshake succeeds | `ready` |
21+| `starting` | handshake fails, protocol other than ACP v1, timeout (30 s) | `error` |
22+| `ready` | used | `busy` |
23+| `busy` | last user done | `ready` |
24+| `ready`, `busy` | stop requested (■), application quits, agent removed from the configuration | `stopped` |
25+| `ready` | unused for `limits.stopIdleAgentsAfter` (checked every 30 s) | `stopped` |
26+| `ready` | another agent must start and this one is the least recently used idle agent | `stopped` |
27+| `ready`, `busy` | the process exits on its own | `error` (never restarted automatically) |
28+| `starting` | stop requested | `stopped` once the start ends |
29+
30+When `limits.maxRunningAgents` agents are running and all are `busy`, a start is refused with the message `too many agents running: N agent(s) busy (…), the limit is N`.
31+
32+Stopping an agent closes its stdin, waits 5 seconds, then kills its process group (the agent and what it started, such as `mm` under `scripts/agents.sh`).
33+
34+## Events from Go to the interface
35+
36+| Event | Payload | Sent when |
37+| --- | --- | --- |
38+| `config:changed` | `{path, valid, problems[]}` | The configuration file was saved and reloaded (valid or not). |
39+| `agent:state` | `{agent, state, error?}` | An agent changes state or its definition changes. |
40+| `agent:log` | `{agent, line}` | An agent writes a line on its standard error. |
41+
42+The Go names and types are in `internal/events`; the TypeScript constants and types are generated in `frontend/bindings/events` and `frontend/bindings/…/internal/events`.
43+
44+## Services called by the interface
45+
46+| Service | Method | Effect |
47+| --- | --- | --- |
48+| `AgentService` | `List()` | Every agent with `name, title, icon, description, state, error, outdated`, in configuration order. |
49+| | `Start(name)`, `Stop(name)`, `Restart(name)` | As in the transitions above; an error is returned to the interface. |
50+| | `Logs(name)` | The last 200 stderr lines of the agent. |
51+| `ConfigService` | `Current()` | `path, source, created, valid, problems[], warnings[], theme, language, channels[]`. |
new file mode 100644
@@ -0,0 +1,51 @@
1+# Reference: agent states and events
2+
3+## Agent states
4+
5+| State | Shown as | Meaning |
6+| --- | --- | --- |
7+| `stopped` | arrêté | No process. The initial state of every agent. |
8+| `starting` | démarre… | The process is launched and the ACP handshake (`initialize`) is in progress. |
9+| `ready` | prêt | Running and idle. |
10+| `busy` | occupé | Running and used by at least one conversation. |
11+| `error` | erreur | The start failed, or the process exited on its own (crash). The status carries the reason, followed by the agent's last stderr lines for a failed start. |
12+
13+`outdated` (shown as « à redémarrer ») is set on a running agent whose definition changed in the configuration; a restart applies it.
14+
15+## Transitions
16+
17+| From | Event | To |
18+| --- | --- | --- |
19+| `stopped`, `error` | start requested (▶ in the Agents panel, or first use) | `starting` |
20+| `starting` | handshake succeeds | `ready` |
21+| `starting` | handshake fails, protocol other than ACP v1, timeout (30 s) | `error` |
22+| `ready` | used | `busy` |
23+| `busy` | last user done | `ready` |
24+| `ready`, `busy` | stop requested (■), application quits, agent removed from the configuration | `stopped` |
25+| `ready` | unused for `limits.stopIdleAgentsAfter` (checked every 30 s) | `stopped` |
26+| `ready` | another agent must start and this one is the least recently used idle agent | `stopped` |
27+| `ready`, `busy` | the process exits on its own | `error` (never restarted automatically) |
28+| `starting` | stop requested | `stopped` once the start ends |
29+
30+When `limits.maxRunningAgents` agents are running and all are `busy`, a start is refused with the message `too many agents running: N agent(s) busy (…), the limit is N`.
31+
32+Stopping an agent closes its stdin, waits 5 seconds, then kills its process group (the agent and what it started, such as `mm` under `scripts/agents.sh`).
33+
34+## Events from Go to the interface
35+
36+| Event | Payload | Sent when |
37+| --- | --- | --- |
38+| `config:changed` | `{path, valid, problems[]}` | The configuration file was saved and reloaded (valid or not). |
39+| `agent:state` | `{agent, state, error?}` | An agent changes state or its definition changes. |
40+| `agent:log` | `{agent, line}` | An agent writes a line on its standard error. |
41+
42+The Go names and types are in `internal/events`; the TypeScript constants and types are generated in `frontend/bindings/events` and `frontend/bindings/…/internal/events`.
43+
44+## Services called by the interface
45+
46+| Service | Method | Effect |
47+| --- | --- | --- |
48+| `AgentService` | `List()` | Every agent with `name, title, icon, description, state, error, outdated`, in configuration order. |
49+| | `Start(name)`, `Stop(name)`, `Restart(name)` | As in the transitions above; an error is returned to the interface. |
50+| | `Logs(name)` | The last 200 stderr lines of the agent. |
51+| `ConfigService` | `Current()` | `path, source, created, valid, problems[], warnings[], theme, language, channels[]`. |
added docs/en/reference/commands.md +51 -0
new file mode 100644
@@ -0,0 +1,51 @@
1+# Reference: commands
2+
3+## `sidekicks`
4+
5+```text
6+sidekicks [--config <file>]
7+```
8+
9+| Option / variable | Effect |
10+| --- | --- |
11+| `--config <file>` | Configuration file to use. Takes precedence over everything else. |
12+| `SIDEKICKS_CONFIG` | Configuration file when `--config` is not given. |
13+| (none) | `<user config dir>/sidekicks/config.yaml`, created with comments when missing. |
14+
15+The order is detailed in the [configuration reference](configuration.md#location).
16+
17+## `make` targets
18+
19+| Target | Effect |
20+| --- | --- |
21+| `help` | Lists the targets. |
22+| `install` | `pnpm install` in `frontend/` (macOS and Linux native binaries). |
23+| `bindings` | Regenerates `frontend/bindings` from the Go services and events (`wails3 generate bindings -clean=true -ts -i`). |
24+| `frontend` | `install`, then builds `frontend/dist`. |
25+| `dev` | `wails3 dev`: the application with hot reload. |
26+| `build` | `wails3 build`: `bin/sidekicks`. |
27+| `package` | `wails3 package`: on macOS `bin/sidekicks.app`. |
28+| `install-test-config` | `scripts/install-test-config.sh`: writes `examples/test-agents.yaml` at the default configuration location with absolute paths to the repository, and `MM_BIN` and the current `PATH` in each agent's `env`; an existing file is kept as `config.yaml.<date>.bak`. |
29+| `check` | `lint` then `test`. |
30+| `test` | `test-go`, `test-frontend`, `test-scripts`. |
31+| `test-go` | `go test ./...` (builds `frontend/dist` first if missing). |
32+| `test-frontend` | `vitest run` in `frontend/`. |
33+| `test-scripts` | `scripts/agents_test.sh` and `scripts/check-license-headers_test.sh`. |
34+| `test-real` | `go test -tags realagents -count=1 ./...` — needs `mm` and llmman. |
35+| `lint` | `lint-go`, `lint-frontend`, `lint-headers`. |
36+| `lint-go` | golangci-lint v2.14.0 (`tools/go.mod`), configuration `.golangci.yml`. |
37+| `lint-frontend` | `svelte-check`. |
38+| `lint-headers` | `scripts/check-license-headers.sh`. |
39+| `bindings-check` | `bindings`, then fails if `frontend/bindings` differs from the committed version. |
40+
41+## `scripts/check-license-headers.sh`
42+
43+```text
44+scripts/check-license-headers.sh [repository root]
45+```
46+
47+Lists the source files (`.go .ts .js .svelte .css .html .sh .py .yml .yaml`, `Makefile`) known to git whose first three lines do not contain `Sidekicks Source-Available License`, and exits 1 if there is one. Not checked: `frontend/bindings`, Wails' `build/` and `Taskfile.yml`, `*.d.ts`, `pnpm-lock.yaml`, `vite.config.ts`, `svelte.config.js`, `agents/`, `*/testdata/*`, `git.sh`, `new.*.sh`, `tag-step.sh`, `.claude/`, `.tickets/`.
48+
49+## CI
50+
51+`.github/workflows/ci.yml`, on every push and merge request: installs Go (from `go.mod`), Node 22, pnpm (from `frontend/package.json`), the GTK 4 / WebKitGTK 6 build packages and the Wails CLI, then runs `make bindings-check` and `make check`.
new file mode 100644
@@ -0,0 +1,51 @@
1+# Reference: commands
2+
3+## `sidekicks`
4+
5+```text
6+sidekicks [--config <file>]
7+```
8+
9+| Option / variable | Effect |
10+| --- | --- |
11+| `--config <file>` | Configuration file to use. Takes precedence over everything else. |
12+| `SIDEKICKS_CONFIG` | Configuration file when `--config` is not given. |
13+| (none) | `<user config dir>/sidekicks/config.yaml`, created with comments when missing. |
14+
15+The order is detailed in the [configuration reference](configuration.md#location).
16+
17+## `make` targets
18+
19+| Target | Effect |
20+| --- | --- |
21+| `help` | Lists the targets. |
22+| `install` | `pnpm install` in `frontend/` (macOS and Linux native binaries). |
23+| `bindings` | Regenerates `frontend/bindings` from the Go services and events (`wails3 generate bindings -clean=true -ts -i`). |
24+| `frontend` | `install`, then builds `frontend/dist`. |
25+| `dev` | `wails3 dev`: the application with hot reload. |
26+| `build` | `wails3 build`: `bin/sidekicks`. |
27+| `package` | `wails3 package`: on macOS `bin/sidekicks.app`. |
28+| `install-test-config` | `scripts/install-test-config.sh`: writes `examples/test-agents.yaml` at the default configuration location with absolute paths to the repository, and `MM_BIN` and the current `PATH` in each agent's `env`; an existing file is kept as `config.yaml.<date>.bak`. |
29+| `check` | `lint` then `test`. |
30+| `test` | `test-go`, `test-frontend`, `test-scripts`. |
31+| `test-go` | `go test ./...` (builds `frontend/dist` first if missing). |
32+| `test-frontend` | `vitest run` in `frontend/`. |
33+| `test-scripts` | `scripts/agents_test.sh` and `scripts/check-license-headers_test.sh`. |
34+| `test-real` | `go test -tags realagents -count=1 ./...` — needs `mm` and llmman. |
35+| `lint` | `lint-go`, `lint-frontend`, `lint-headers`. |
36+| `lint-go` | golangci-lint v2.14.0 (`tools/go.mod`), configuration `.golangci.yml`. |
37+| `lint-frontend` | `svelte-check`. |
38+| `lint-headers` | `scripts/check-license-headers.sh`. |
39+| `bindings-check` | `bindings`, then fails if `frontend/bindings` differs from the committed version. |
40+
41+## `scripts/check-license-headers.sh`
42+
43+```text
44+scripts/check-license-headers.sh [repository root]
45+```
46+
47+Lists the source files (`.go .ts .js .svelte .css .html .sh .py .yml .yaml`, `Makefile`) known to git whose first three lines do not contain `Sidekicks Source-Available License`, and exits 1 if there is one. Not checked: `frontend/bindings`, Wails' `build/` and `Taskfile.yml`, `*.d.ts`, `pnpm-lock.yaml`, `vite.config.ts`, `svelte.config.js`, `agents/`, `*/testdata/*`, `git.sh`, `new.*.sh`, `tag-step.sh`, `.claude/`, `.tickets/`.
48+
49+## CI
50+
51+`.github/workflows/ci.yml`, on every push and merge request: installs Go (from `go.mod`), Node 22, pnpm (from `frontend/package.json`), the GTK 4 / WebKitGTK 6 build packages and the Wails CLI, then runs `make bindings-check` and `make check`.
modified docs/en/reference/configuration.md +5 -5
@@ -7,9 +7,9 @@
77
88 One YAML file describes the agents Sidekicks can talk to, the channels created at startup, and preferences. Conversations, messages and ACP sessions are not in it (see [Architecture](../explanation/architecture.md)).
99
10-**Status (2026-10-29)**: the format and its JSON Schema exist and are tested; the application that reads the file does not exist yet (issue #9). Sections marked *loader* describe behaviour issue #9 must implement.
10+The application reads it with the Go package `internal/config` (milestone M1): validation, defaults, paths, the default file, reloading on every save.
1111
12-## Location (*loader*)
12+## Location
1313
1414 The file is looked up in this order; the first one that applies wins:
1515
@@ -55,7 +55,7 @@ Unknown keys are **errors**, at every level: a misspelt `comand:` must be report
5555 | Key | Type | Default | Meaning |
5656 | --- | --- | --- | --- |
5757 | `maxRunningAgents` | integer ≥ 1 | `2` | Agents running at the same time. Each local agent keeps a model in memory; the default fits a 32 GB laptop with small models. |
58-| `stopIdleAgentsAfter` | Go duration (`30s`, `10m`, `1h30m`) or `0` | `10m` | Stops an agent unused for this long, to free its memory. `0` never stops it. |
58+| `stopIdleAgentsAfter` | Go duration (`30s`, `10m`, `1h30m`), or `0` (number or text) | `10m` | Stops an agent unused for this long, to free its memory. `0` never stops it. |
5959
6060 ### An agent
6161
@@ -93,7 +93,7 @@ The runtime tells Sidekicks how to reach the agent's **files and terminal** (Age
9393
9494 A channel declared here is created at startup **if it does not exist yet**; after that it lives in the application like any other channel. Removing it from the file does not delete it (deleting history must be an explicit user action).
9595
96-## Rules checked by the loader, not by the schema (*loader*)
96+## Rules checked by the loader, not by the schema
9797
9898 JSON Schema cannot express them; the configuration loader (issue #9) checks them and reports them like schema errors:
9999
@@ -102,7 +102,7 @@ JSON Schema cannot express them; the configuration loader (issue #9) checks them
102102 3. every name in a channel's `agents` is the name of an agent;
103103 4. `command` exists (in `PATH`, or as a file) — reported as a warning, since an agent may be installed later.
104104
105-## Error messages (*loader*)
105+## Error messages
106106
107107 Every error names the line and the key, in the language of the interface, and says how to fix it. Examples with the interface in French:
108108
@@ -7,9 +7,9 @@
7 7
8 One YAML file describes the agents Sidekicks can talk to, the channels created at startup, and preferences. Conversations, messages and ACP sessions are not in it (see [Architecture](../explanation/architecture.md)).8 One YAML file describes the agents Sidekicks can talk to, the channels created at startup, and preferences. Conversations, messages and ACP sessions are not in it (see [Architecture](../explanation/architecture.md)).
9 9
10-**Status (2026-10-29)**: the format and its JSON Schema exist and are tested; the application that reads the file does not exist yet (issue #9). Sections marked *loader* describe behaviour issue #9 must implement.10+The application reads it with the Go package `internal/config` (milestone M1): validation, defaults, paths, the default file, reloading on every save.
11 11
12-## Location (*loader*)12+## Location
13 13
14 The file is looked up in this order; the first one that applies wins:14 The file is looked up in this order; the first one that applies wins:
15 15
@@ -55,7 +55,7 @@ Unknown keys are **errors**, at every level: a misspelt `comand:` must be report
55 | Key | Type | Default | Meaning |55 | Key | Type | Default | Meaning |
56 | --- | --- | --- | --- |56 | --- | --- | --- | --- |
57 | `maxRunningAgents` | integer ≥ 1 | `2` | Agents running at the same time. Each local agent keeps a model in memory; the default fits a 32 GB laptop with small models. |57 | `maxRunningAgents` | integer ≥ 1 | `2` | Agents running at the same time. Each local agent keeps a model in memory; the default fits a 32 GB laptop with small models. |
58-| `stopIdleAgentsAfter` | Go duration (`30s`, `10m`, `1h30m`) or `0` | `10m` | Stops an agent unused for this long, to free its memory. `0` never stops it. |58+| `stopIdleAgentsAfter` | Go duration (`30s`, `10m`, `1h30m`), or `0` (number or text) | `10m` | Stops an agent unused for this long, to free its memory. `0` never stops it. |
59 59
60 ### An agent60 ### An agent
61 61
@@ -93,7 +93,7 @@ The runtime tells Sidekicks how to reach the agent's **files and terminal** (Age
93 93
94 A channel declared here is created at startup **if it does not exist yet**; after that it lives in the application like any other channel. Removing it from the file does not delete it (deleting history must be an explicit user action).94 A channel declared here is created at startup **if it does not exist yet**; after that it lives in the application like any other channel. Removing it from the file does not delete it (deleting history must be an explicit user action).
95 95
96-## Rules checked by the loader, not by the schema (*loader*)96+## Rules checked by the loader, not by the schema
97 97
98 JSON Schema cannot express them; the configuration loader (issue #9) checks them and reports them like schema errors:98 JSON Schema cannot express them; the configuration loader (issue #9) checks them and reports them like schema errors:
99 99
@@ -102,7 +102,7 @@ JSON Schema cannot express them; the configuration loader (issue #9) checks them
102 3. every name in a channel's `agents` is the name of an agent;102 3. every name in a channel's `agents` is the name of an agent;
103 4. `command` exists (in `PATH`, or as a file) — reported as a warning, since an agent may be installed later.103 4. `command` exists (in `PATH`, or as a file) — reported as a warning, since an agent may be installed later.
104 104
105-## Error messages (*loader*)105+## Error messages
106 106
107 Every error names the line and the key, in the language of the interface, and says how to fix it. Examples with the interface in French:107 Every error names the line and the key, in the language of the interface, and says how to fix it. Examples with the interface in French:
108 108
added docs/en/tutorials/first-launch.md +95 -0
new file mode 100644
@@ -0,0 +1,95 @@
1+# Tutorial: your first launch of Sidekicks
2+
3+By the end of this tutorial, you will have written a configuration with one agent, opened Sidekicks with it, started Alice from the interface, read her log, and watched Sidekicks catch a mistake in the file and recover when you fix it.
4+
5+## Prerequisites
6+
7+- The development tools of [How to develop the application](../how-to/develop-the-application.md), and `make install` done once.
8+- `mm` on your `PATH` and llmman running, with Alice's model pulled (see the [first ACP exchange](first-acp-exchange.md) tutorial).
9+- A terminal at the root of the repository.
10+
11+## Step 1 — Write a configuration
12+
13+Type:
14+
15+```bash
16+cat > /tmp/sidekicks-tutorial.yaml <<EOF
17+version: 1
18+agents:
19+ - name: alice
20+ title: Alice (assistante)
21+ icon: "👩"
22+ command: $PWD/scripts/agents.sh
23+ args: [run, alice]
24+ workspace: $PWD/agents/alice
25+channels:
26+ - name: général
27+ agents: [alice]
28+EOF
29+```
30+
31+We now have a configuration with one agent, Alice, started through the project's script.
32+
33+## Step 2 — Open Sidekicks with it
34+
35+Type:
36+
37+```bash
38+SIDEKICKS_CONFIG=/tmp/sidekicks-tutorial.yaml make dev
39+```
40+
41+After a minute the first time, the Sidekicks window opens. You should see, on the left, **CHANNELS** with `# général`, and **AGENTS** with `👩 Alice (assistante)` and the word « arrêté » under it. At the bottom, the status bar says « 0 agent actif ».
42+
43+## Step 3 — Start Alice
44+
45+Move the pointer over Alice: a ▶ button appears on the right. Click it.
46+
47+You should see Alice's dot turn orange (« démarre… ») then green (« prêt »), and the status bar say « 1 agent actif ».
48+
49+## Step 4 — Read her log
50+
51+Click **Journal** in the status bar. A panel opens at the bottom with Alice's lines, starting with:
52+
53+```text
54+mm v1.0.3 | config: …/agents/alice/agent.yaml | provider: llmman | …
55+[acp] initialize: client "sidekicks" protocolVersion=1 …
56+```
57+
58+This is what Alice writes for whoever looks after her; Sidekicks keeps it for you.
59+
60+## Step 5 — Make a mistake on purpose
61+
62+Click **Settings** in the status bar. It says « ✓ Configuration valide ». Now, in a second terminal, break the file:
63+
64+```bash
65+sed -i.bak 's/ command:/ comand:/' /tmp/sidekicks-tutorial.yaml
66+```
67+
68+Within a second, a red banner appears in the centre, and **Settings** shows:
69+
70+```text
71+ligne 3 — agents[0] : il manque la clé « command »
72+ligne 6 — agents[0].comand : clé inconnue « comand » ; vouliez-vous dire « command » ?
73+```
74+
75+Alice is still listed: Sidekicks keeps the last valid configuration.
76+
77+## Step 6 — Fix it
78+
79+```bash
80+mv /tmp/sidekicks-tutorial.yaml.bak /tmp/sidekicks-tutorial.yaml
81+```
82+
83+The banner disappears and Settings shows « ✓ Configuration valide » again — no restart needed.
84+
85+## Step 7 — Stop
86+
87+Hover Alice and click ■: her dot turns grey. Close the window: had Alice still been running, Sidekicks would have stopped her and freed her model.
88+
89+## What now?
90+
91+You have run Sidekicks, driven an agent's life and seen how the configuration is checked. To go further:
92+
93+- All the keys of the file → [Reference: the configuration file](../reference/configuration.md)
94+- What each state means → [Reference: agent states and events](../reference/agents-and-events.md)
95+- Why agents start and stop this way → [Memory and local models](../explanation/memory-and-local-models.md)
new file mode 100644
@@ -0,0 +1,95 @@
1+# Tutorial: your first launch of Sidekicks
2+
3+By the end of this tutorial, you will have written a configuration with one agent, opened Sidekicks with it, started Alice from the interface, read her log, and watched Sidekicks catch a mistake in the file and recover when you fix it.
4+
5+## Prerequisites
6+
7+- The development tools of [How to develop the application](../how-to/develop-the-application.md), and `make install` done once.
8+- `mm` on your `PATH` and llmman running, with Alice's model pulled (see the [first ACP exchange](first-acp-exchange.md) tutorial).
9+- A terminal at the root of the repository.
10+
11+## Step 1 — Write a configuration
12+
13+Type:
14+
15+```bash
16+cat > /tmp/sidekicks-tutorial.yaml <<EOF
17+version: 1
18+agents:
19+ - name: alice
20+ title: Alice (assistante)
21+ icon: "👩"
22+ command: $PWD/scripts/agents.sh
23+ args: [run, alice]
24+ workspace: $PWD/agents/alice
25+channels:
26+ - name: général
27+ agents: [alice]
28+EOF
29+```
30+
31+We now have a configuration with one agent, Alice, started through the project's script.
32+
33+## Step 2 — Open Sidekicks with it
34+
35+Type:
36+
37+```bash
38+SIDEKICKS_CONFIG=/tmp/sidekicks-tutorial.yaml make dev
39+```
40+
41+After a minute the first time, the Sidekicks window opens. You should see, on the left, **CHANNELS** with `# général`, and **AGENTS** with `👩 Alice (assistante)` and the word « arrêté » under it. At the bottom, the status bar says « 0 agent actif ».
42+
43+## Step 3 — Start Alice
44+
45+Move the pointer over Alice: a ▶ button appears on the right. Click it.
46+
47+You should see Alice's dot turn orange (« démarre… ») then green (« prêt »), and the status bar say « 1 agent actif ».
48+
49+## Step 4 — Read her log
50+
51+Click **Journal** in the status bar. A panel opens at the bottom with Alice's lines, starting with:
52+
53+```text
54+mm v1.0.3 | config: …/agents/alice/agent.yaml | provider: llmman | …
55+[acp] initialize: client "sidekicks" protocolVersion=1 …
56+```
57+
58+This is what Alice writes for whoever looks after her; Sidekicks keeps it for you.
59+
60+## Step 5 — Make a mistake on purpose
61+
62+Click **Settings** in the status bar. It says « ✓ Configuration valide ». Now, in a second terminal, break the file:
63+
64+```bash
65+sed -i.bak 's/ command:/ comand:/' /tmp/sidekicks-tutorial.yaml
66+```
67+
68+Within a second, a red banner appears in the centre, and **Settings** shows:
69+
70+```text
71+ligne 3 — agents[0] : il manque la clé « command »
72+ligne 6 — agents[0].comand : clé inconnue « comand » ; vouliez-vous dire « command » ?
73+```
74+
75+Alice is still listed: Sidekicks keeps the last valid configuration.
76+
77+## Step 6 — Fix it
78+
79+```bash
80+mv /tmp/sidekicks-tutorial.yaml.bak /tmp/sidekicks-tutorial.yaml
81+```
82+
83+The banner disappears and Settings shows « ✓ Configuration valide » again — no restart needed.
84+
85+## Step 7 — Stop
86+
87+Hover Alice and click ■: her dot turns grey. Close the window: had Alice still been running, Sidekicks would have stopped her and freed her model.
88+
89+## What now?
90+
91+You have run Sidekicks, driven an agent's life and seen how the configuration is checked. To go further:
92+
93+- All the keys of the file → [Reference: the configuration file](../reference/configuration.md)
94+- What each state means → [Reference: agent states and events](../reference/agents-and-events.md)
95+- Why agents start and stop this way → [Memory and local models](../explanation/memory-and-local-models.md)
modified docs/fr/README.md +4 -4
@@ -1,14 +1,14 @@
11 # Documentation de Sidekicks (français)
22
3-Sidekicks est une application de bureau pour converser avec des agents d'IA dans des channels, grâce à l'Agent Client Protocol, pensée pour des personnes qui ne sont pas développeuses. Le projet démarre : au 29 octobre 2026, les décisions, le format de configuration et l'outillage de test existent ; l'application, pas encore.
3+Sidekicks est une application de bureau pour converser avec des agents d'IA dans des channels, grâce à l'Agent Client Protocol, pensée pour des personnes qui ne sont pas développeuses. La milestone M1 (Fondations) est terminée : l'application s'ouvre, lit sa configuration, démarre et arrête les agents, et affiche leur état et leur journal. Les conversations arrivent avec la milestone M2.
44
55 Cette documentation suit la méthode [Diátaxis](https://diataxis.fr) : choisissez la section qui correspond à votre besoin du moment.
66
77 | Vous voulez… | Section | Pages |
88 | --- | --- | --- |
9-| **apprendre** en pratiquant | Tutoriels | [Votre premier échange ACP avec un agent de test](tutorials/premier-echange-acp.md) |
10-| **réaliser** une tâche précise | Guides pratiques | [Lancer les tests](how-to/lancer-les-tests.md) · [Lancer un agent de test](how-to/lancer-un-agent-de-test.md) · [Tester la milestone Initialize sous macOS](how-to/tester-la-milestone-initialize-sous-macos.md) |
11-| **consulter** un détail exact | Référence | [Le fichier de configuration](reference/configuration.md) · [scripts/agents.sh](reference/script-agents.md) |
9+| **apprendre** en pratiquant | Tutoriels | [Votre premier lancement de Sidekicks](tutorials/premier-lancement.md) · [Votre premier échange ACP avec un agent de test](tutorials/premier-echange-acp.md) |
10+| **réaliser** une tâche précise | Guides pratiques | [Développer l'application](how-to/developper-l-application.md) · [Lancer les tests](how-to/lancer-les-tests.md) · [Lancer un agent de test](how-to/lancer-un-agent-de-test.md) · [Tester la milestone M1 sous macOS](how-to/tester-la-milestone-m1-sous-macos.md) · [Tester la milestone Initialize sous macOS](how-to/tester-la-milestone-initialize-sous-macos.md) |
11+| **consulter** un détail exact | Référence | [Le fichier de configuration](reference/configuration.md) · [États des agents et événements](reference/agents-et-evenements.md) · [Commandes](reference/commandes.md) · [scripts/agents.sh](reference/script-agents.md) |
1212 | **comprendre** le pourquoi | Explications | [Pourquoi ACP](explanation/pourquoi-acp.md) · [La stack de l'interface](explanation/stack-interface.md) · [Architecture](explanation/architecture.md) · [Mémoire et modèles locaux](explanation/memoire-et-modeles-locaux.md) |
1313
1414 ## Documents de conception
@@ -1,14 +1,14 @@
1 # Documentation de Sidekicks (français)1 # Documentation de Sidekicks (français)
2 2
3-Sidekicks est une application de bureau pour converser avec des agents d'IA dans des channels, grâce à l'Agent Client Protocol, pensée pour des personnes qui ne sont pas développeuses. Le projet démarre : au 29 octobre 2026, les décisions, le format de configuration et l'outillage de test existent ; l'application, pas encore.3+Sidekicks est une application de bureau pour converser avec des agents d'IA dans des channels, grâce à l'Agent Client Protocol, pensée pour des personnes qui ne sont pas développeuses. La milestone M1 (Fondations) est terminée : l'application s'ouvre, lit sa configuration, démarre et arrête les agents, et affiche leur état et leur journal. Les conversations arrivent avec la milestone M2.
4 4
5 Cette documentation suit la méthode [Diátaxis](https://diataxis.fr) : choisissez la section qui correspond à votre besoin du moment.5 Cette documentation suit la méthode [Diátaxis](https://diataxis.fr) : choisissez la section qui correspond à votre besoin du moment.
6 6
7 | Vous voulez… | Section | Pages |7 | Vous voulez… | Section | Pages |
8 | --- | --- | --- |8 | --- | --- | --- |
9-| **apprendre** en pratiquant | Tutoriels | [Votre premier échange ACP avec un agent de test](tutorials/premier-echange-acp.md) |9+| **apprendre** en pratiquant | Tutoriels | [Votre premier lancement de Sidekicks](tutorials/premier-lancement.md) · [Votre premier échange ACP avec un agent de test](tutorials/premier-echange-acp.md) |
10-| **réaliser** une tâche précise | Guides pratiques | [Lancer les tests](how-to/lancer-les-tests.md) · [Lancer un agent de test](how-to/lancer-un-agent-de-test.md) · [Tester la milestone Initialize sous macOS](how-to/tester-la-milestone-initialize-sous-macos.md) |10+| **réaliser** une tâche précise | Guides pratiques | [Développer l'application](how-to/developper-l-application.md) · [Lancer les tests](how-to/lancer-les-tests.md) · [Lancer un agent de test](how-to/lancer-un-agent-de-test.md) · [Tester la milestone M1 sous macOS](how-to/tester-la-milestone-m1-sous-macos.md) · [Tester la milestone Initialize sous macOS](how-to/tester-la-milestone-initialize-sous-macos.md) |
11-| **consulter** un détail exact | Référence | [Le fichier de configuration](reference/configuration.md) · [scripts/agents.sh](reference/script-agents.md) |11+| **consulter** un détail exact | Référence | [Le fichier de configuration](reference/configuration.md) · [États des agents et événements](reference/agents-et-evenements.md) · [Commandes](reference/commandes.md) · [scripts/agents.sh](reference/script-agents.md) |
12 | **comprendre** le pourquoi | Explications | [Pourquoi ACP](explanation/pourquoi-acp.md) · [La stack de l'interface](explanation/stack-interface.md) · [Architecture](explanation/architecture.md) · [Mémoire et modèles locaux](explanation/memoire-et-modeles-locaux.md) |12 | **comprendre** le pourquoi | Explications | [Pourquoi ACP](explanation/pourquoi-acp.md) · [La stack de l'interface](explanation/stack-interface.md) · [Architecture](explanation/architecture.md) · [Mémoire et modèles locaux](explanation/memoire-et-modeles-locaux.md) |
13 13
14 ## Documents de conception14 ## Documents de conception
modified docs/fr/explanation/architecture.md +5 -1
@@ -6,7 +6,7 @@ Avant d'écrire l'application, le projet a fixé le découpage du code, ce qui e
66
77 ![Packages et dépendances](../../diagrams/packages.png)
88
9-La source du schéma est [packages.drawio](../../diagrams/packages.drawio). Au 29 octobre 2026, seul le package `schema` existe ; les autres sont le plan des prochaines milestones.
9+La source du schéma est [packages.drawio](../../diagrams/packages.drawio). Les boîtes bleues existent depuis la milestone M1 ; les grises sont prévues.
1010
1111 ## Pourquoi ce découpage
1212
@@ -16,6 +16,10 @@ Trois faits l'ont guidé :
1616 - **L'interface ne doit pas connaître ACP.** Si le code Svelte manipulait des messages ACP, changer de version de protocole ou d'agent se répercuterait jusque dans l'interface. Tout ce que voit l'interface passe par `internal/app`, sous forme d'appels et d'événements propres à Sidekicks.
1717 - **La configuration peut changer pendant que des agents tournent.** Elle est chargée, validée et surveillée à un seul endroit (`internal/config`), qui prévient les autres.
1818
19+## Où s'arrête Wails
20+
21+Seul `main.go`, à la racine du dépôt, connaît Wails : il crée la fenêtre, branche les services de `internal/app` et raccorde le gestionnaire d'événements de Wails. Tout le reste est du Go ordinaire qui émet ses événements via une petite interface `Emitter` ; le gestionnaire d'agents, la configuration et les services se testent donc sans fenêtre. Le package principal est à la racine plutôt que dans `cmd/`, car les fichiers de build générés par Wails l'attendent là : lutter contre l'outil coûterait plus que cela n'apporte.
22+
1923 ## Ce qui vit dans le YAML et ce qui vit dans la base
2024
2125 Le fichier YAML décrit **ce qui existe** : agents, limites, préférences, et channels à créer. Il est fait pour être lu et modifié par une personne. Ce qui **se passe** — les channels créés depuis l'interface, qui les a rejoints, les messages — est stocké par l'application dans Pebble, une base clé-valeur embarquée. Un channel déclaré dans le YAML est créé une fois au démarrage, puis vit dans la base comme les autres : modifier le YAML ne supprime jamais une conversation par accident.
@@ -6,7 +6,7 @@ Avant d'écrire l'application, le projet a fixé le découpage du code, ce qui e
6 6
7 ![Packages et dépendances](../../diagrams/packages.png)7 ![Packages et dépendances](../../diagrams/packages.png)
8 8
9-La source du schéma est [packages.drawio](../../diagrams/packages.drawio). Au 29 octobre 2026, seul le package `schema` existe ; les autres sont le plan des prochaines milestones.9+La source du schéma est [packages.drawio](../../diagrams/packages.drawio). Les boîtes bleues existent depuis la milestone M1 ; les grises sont prévues.
10 10
11 ## Pourquoi ce découpage11 ## Pourquoi ce découpage
12 12
@@ -16,6 +16,10 @@ Trois faits l'ont guidé :
16 - **L'interface ne doit pas connaître ACP.** Si le code Svelte manipulait des messages ACP, changer de version de protocole ou d'agent se répercuterait jusque dans l'interface. Tout ce que voit l'interface passe par `internal/app`, sous forme d'appels et d'événements propres à Sidekicks.16 - **L'interface ne doit pas connaître ACP.** Si le code Svelte manipulait des messages ACP, changer de version de protocole ou d'agent se répercuterait jusque dans l'interface. Tout ce que voit l'interface passe par `internal/app`, sous forme d'appels et d'événements propres à Sidekicks.
17 - **La configuration peut changer pendant que des agents tournent.** Elle est chargée, validée et surveillée à un seul endroit (`internal/config`), qui prévient les autres.17 - **La configuration peut changer pendant que des agents tournent.** Elle est chargée, validée et surveillée à un seul endroit (`internal/config`), qui prévient les autres.
18 18
19+## Où s'arrête Wails
20+
21+Seul `main.go`, à la racine du dépôt, connaît Wails : il crée la fenêtre, branche les services de `internal/app` et raccorde le gestionnaire d'événements de Wails. Tout le reste est du Go ordinaire qui émet ses événements via une petite interface `Emitter` ; le gestionnaire d'agents, la configuration et les services se testent donc sans fenêtre. Le package principal est à la racine plutôt que dans `cmd/`, car les fichiers de build générés par Wails l'attendent là : lutter contre l'outil coûterait plus que cela n'apporte.
22+
19 ## Ce qui vit dans le YAML et ce qui vit dans la base23 ## Ce qui vit dans le YAML et ce qui vit dans la base
20 24
21 Le fichier YAML décrit **ce qui existe** : agents, limites, préférences, et channels à créer. Il est fait pour être lu et modifié par une personne. Ce qui **se passe** — les channels créés depuis l'interface, qui les a rejoints, les messages — est stocké par l'application dans Pebble, une base clé-valeur embarquée. Un channel déclaré dans le YAML est créé une fois au démarrage, puis vit dans la base comme les autres : modifier le YAML ne supprime jamais une conversation par accident.25 Le fichier YAML décrit **ce qui existe** : agents, limites, préférences, et channels à créer. Il est fait pour être lu et modifié par une personne. Ce qui **se passe** — les channels créés depuis l'interface, qui les a rejoints, les messages — est stocké par l'application dans Pebble, une base clé-valeur embarquée. Un channel déclaré dans le YAML est créé une fois au démarrage, puis vit dans la base comme les autres : modifier le YAML ne supprime jamais une conversation par accident.
modified docs/fr/explanation/memoire-et-modeles-locaux.md +2 -2
@@ -12,11 +12,11 @@ Comme la protection vit dans un script qui relaie stdio, elle protège aussi les
1212
1313 ## Dans le produit
1414
15-Le même raisonnement s'applique aux personnes qui utiliseront Sidekicks sur leur portable ; l'architecture le prend donc en charge ([ADR 0003](../../adr/0003-architecture.md)) :
15+Le même raisonnement s'applique aux personnes qui utiliseront Sidekicks sur leur portable ; l'application l'applique donc (`internal/agents`, milestone M1 ; [ADR 0003](../../adr/0003-architecture.md)) :
1616
1717 - les agents démarrent **au premier appel**, pas à l'ouverture de l'application ;
1818 - un agent inutilisé depuis un moment (dix minutes par défaut) est arrêté pour libérer son modèle ;
19-- une limite d'agents simultanés (`limits.maxRunningAgents`, deux par défaut) fait partie de la [configuration](../reference/configuration.md) ;
19+- une limite d'agents simultanés (`limits.maxRunningAgents`, deux par défaut) fait partie de la [configuration](../reference/configuration.md). Quand elle est atteinte, l'agent inutilisé depuis le plus longtemps laisse sa place — à condition qu'il soit inactif : un agent en train de répondre n'est jamais interrompu, et le démarrage est alors refusé ;
2020 - dans un channel à plusieurs agents, seuls les agents **mentionnés** répondent, l'un après l'autre par défaut, plutôt que tous à la fois.
2121
2222 ## Alternatives écartées
@@ -12,11 +12,11 @@ Comme la protection vit dans un script qui relaie stdio, elle protège aussi les
12 12
13 ## Dans le produit13 ## Dans le produit
14 14
15-Le même raisonnement s'applique aux personnes qui utiliseront Sidekicks sur leur portable ; l'architecture le prend donc en charge ([ADR 0003](../../adr/0003-architecture.md)) :15+Le même raisonnement s'applique aux personnes qui utiliseront Sidekicks sur leur portable ; l'application l'applique donc (`internal/agents`, milestone M1 ; [ADR 0003](../../adr/0003-architecture.md)) :
16 16
17 - les agents démarrent **au premier appel**, pas à l'ouverture de l'application ;17 - les agents démarrent **au premier appel**, pas à l'ouverture de l'application ;
18 - un agent inutilisé depuis un moment (dix minutes par défaut) est arrêté pour libérer son modèle ;18 - un agent inutilisé depuis un moment (dix minutes par défaut) est arrêté pour libérer son modèle ;
19-- une limite d'agents simultanés (`limits.maxRunningAgents`, deux par défaut) fait partie de la [configuration](../reference/configuration.md) ;19+- une limite d'agents simultanés (`limits.maxRunningAgents`, deux par défaut) fait partie de la [configuration](../reference/configuration.md). Quand elle est atteinte, l'agent inutilisé depuis le plus longtemps laisse sa place — à condition qu'il soit inactif : un agent en train de répondre n'est jamais interrompu, et le démarrage est alors refusé ;
20 - dans un channel à plusieurs agents, seuls les agents **mentionnés** répondent, l'un après l'autre par défaut, plutôt que tous à la fois.20 - dans un channel à plusieurs agents, seuls les agents **mentionnés** répondent, l'un après l'autre par défaut, plutôt que tous à la fois.
21 21
22 ## Alternatives écartées22 ## Alternatives écartées
added docs/fr/how-to/developper-l-application.md +49 -0
new file mode 100644
@@ -0,0 +1,49 @@
1+# Comment développer l'application
2+
3+Ce guide montre comment préparer une machine, lancer Sidekicks avec rechargement à chaud et le construire. Il suppose que vous connaissez Go et le terminal.
4+
5+## Installer les outils
6+
7+| Outil | Version | macOS | Linux (Debian/Ubuntu) |
8+| --- | --- | --- | --- |
9+| Go | ≥ 1.26 | `brew install go` | paquet de la distribution ou go.dev |
10+| Node | ≥ 20 | `brew install node` | paquet de la distribution |
11+| pnpm | 12.11.2 | `npm install -g pnpm@12.11.2` | idem |
12+| CLI Wails | v3.0.0 | `go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0` | idem |
13+| Boîte à outils native | — | `xcode-select --install` | `sudo apt-get install pkg-config libgtk-4-dev libwebkitgtk-6.0-dev` |
14+
15+Vérifiez ensuite avec `wails3 doctor`.
16+
17+## Installer les dépendances et lancer
18+
19+```bash
20+make install
21+SIDEKICKS_CONFIG="$PWD/examples/test-agents.yaml" make dev
22+```
23+
24+`make dev` ouvre la fenêtre, recharge l'interface quand un fichier Svelte change et recompile le Go quand un fichier Go change. Sans `SIDEKICKS_CONFIG`, Sidekicks utilise (et crée) la configuration à son emplacement par défaut.
25+
26+## Construire
27+
28+```bash
29+make build # bin/sidekicks
30+make package # macOS : bin/sidekicks.app, signé ad hoc
31+```
32+
33+## Quand vous modifiez…
34+
35+- **un service Go ou une charge d'événement** (`internal/app`, `internal/events`, `main.go`) : lancez `make bindings` et commitez `frontend/bindings` — la CI échoue s'ils ne sont pas à jour.
36+- **le format de configuration** : mettez à jour `schema/config.schema.json`, `internal/config`, les données de test et la [référence de la configuration](../reference/configuration.md).
37+- **un package ou ses imports** : mettez à jour `docs/diagrams/packages.drawio` (son générateur est décrit dans l'[explication de l'architecture](../explanation/architecture.md)).
38+- **tout fichier source que vous créez** : commencez-le par l'en-tête de licence (`make lint-headers` le vérifie).
39+
40+## Variantes
41+
42+- **Le dépôt est partagé entre macOS et une machine ou sandbox Linux** : rien de particulier à faire — `frontend/pnpm-workspace.yaml` demande à pnpm d'installer les binaires natifs des deux systèmes dans le même `node_modules`. Avec npm au lieu de pnpm, chaque système casserait l'autre.
43+- **Utiliser les agents de test sans `SIDEKICKS_CONFIG`** (y compris l'`.app` empaquetée) : `make install-test-config` les écrit à l'emplacement par défaut, avec des chemins absolus et l'environnement qui manque à une application ouverte depuis le Finder.
44+- **Lancer sans rechargement à chaud** : `make frontend && go build -o bin/sidekicks . && bin/sidekicks --config examples/test-agents.yaml`.
45+
46+## Voir aussi
47+
48+- [Comment lancer les tests](lancer-les-tests.md) · [Référence : commandes](../reference/commandes.md)
49+- Pourquoi cette stack : [La stack de l'interface](../explanation/stack-interface.md)
new file mode 100644
@@ -0,0 +1,49 @@
1+# Comment développer l'application
2+
3+Ce guide montre comment préparer une machine, lancer Sidekicks avec rechargement à chaud et le construire. Il suppose que vous connaissez Go et le terminal.
4+
5+## Installer les outils
6+
7+| Outil | Version | macOS | Linux (Debian/Ubuntu) |
8+| --- | --- | --- | --- |
9+| Go | ≥ 1.26 | `brew install go` | paquet de la distribution ou go.dev |
10+| Node | ≥ 20 | `brew install node` | paquet de la distribution |
11+| pnpm | 12.11.2 | `npm install -g pnpm@12.11.2` | idem |
12+| CLI Wails | v3.0.0 | `go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0` | idem |
13+| Boîte à outils native | — | `xcode-select --install` | `sudo apt-get install pkg-config libgtk-4-dev libwebkitgtk-6.0-dev` |
14+
15+Vérifiez ensuite avec `wails3 doctor`.
16+
17+## Installer les dépendances et lancer
18+
19+```bash
20+make install
21+SIDEKICKS_CONFIG="$PWD/examples/test-agents.yaml" make dev
22+```
23+
24+`make dev` ouvre la fenêtre, recharge l'interface quand un fichier Svelte change et recompile le Go quand un fichier Go change. Sans `SIDEKICKS_CONFIG`, Sidekicks utilise (et crée) la configuration à son emplacement par défaut.
25+
26+## Construire
27+
28+```bash
29+make build # bin/sidekicks
30+make package # macOS : bin/sidekicks.app, signé ad hoc
31+```
32+
33+## Quand vous modifiez…
34+
35+- **un service Go ou une charge d'événement** (`internal/app`, `internal/events`, `main.go`) : lancez `make bindings` et commitez `frontend/bindings` — la CI échoue s'ils ne sont pas à jour.
36+- **le format de configuration** : mettez à jour `schema/config.schema.json`, `internal/config`, les données de test et la [référence de la configuration](../reference/configuration.md).
37+- **un package ou ses imports** : mettez à jour `docs/diagrams/packages.drawio` (son générateur est décrit dans l'[explication de l'architecture](../explanation/architecture.md)).
38+- **tout fichier source que vous créez** : commencez-le par l'en-tête de licence (`make lint-headers` le vérifie).
39+
40+## Variantes
41+
42+- **Le dépôt est partagé entre macOS et une machine ou sandbox Linux** : rien de particulier à faire — `frontend/pnpm-workspace.yaml` demande à pnpm d'installer les binaires natifs des deux systèmes dans le même `node_modules`. Avec npm au lieu de pnpm, chaque système casserait l'autre.
43+- **Utiliser les agents de test sans `SIDEKICKS_CONFIG`** (y compris l'`.app` empaquetée) : `make install-test-config` les écrit à l'emplacement par défaut, avec des chemins absolus et l'environnement qui manque à une application ouverte depuis le Finder.
44+- **Lancer sans rechargement à chaud** : `make frontend && go build -o bin/sidekicks . && bin/sidekicks --config examples/test-agents.yaml`.
45+
46+## Voir aussi
47+
48+- [Comment lancer les tests](lancer-les-tests.md) · [Référence : commandes](../reference/commandes.md)
49+- Pourquoi cette stack : [La stack de l'interface](../explanation/stack-interface.md)
modified docs/fr/how-to/lancer-les-tests.md +26 -13
@@ -1,36 +1,49 @@
11 # Comment lancer les tests
22
3-Ce guide montre comment lancer la suite de tests du projet avant un commit. Il suppose un clone du dépôt et un terminal ouvert à sa racine.
3+Ce guide montre comment vérifier le projet avant un commit ou une merge request. Il suppose les prérequis de développement installés (voir [Comment développer l'application](developper-l-application.md)) et un terminal ouvert à la racine du dépôt.
44
5-## Tout lancer
5+## Lancer ce que lance la CI
66
77 ```bash
8-make test
8+make check
99 ```
1010
11-La commande lance les tests unitaires Go (`go test ./...`) et ceux de l'outillage shell (`scripts/agents_test.sh`). Aucun n'a besoin d'un vrai agent, d'un modèle ni du réseau : `mm` et llmman sont remplacés par des imitations. Un passage réussi se termine par :
11+La commande lance les linters (`make lint`), puis tous les tests qui n'ont besoin ni d'un vrai agent, ni d'un modèle, ni du réseau (`make test`). Un passage réussi se termine par :
1212
1313 ```text
14-ok git.rickub.com/bots-garden/sidekicks/schema …
1514 14 passed, 0 failed
15+all licence header tests passed
1616 ```
1717
18-`make help` liste les cibles.
18+La CI (`.github/workflows/ci.yml`) lance la même commande, plus `make bindings-check`.
1919
2020 ## Lancer une partie
2121
22-| Pour tester | Commande |
22+| Pour vérifier | Commande |
2323 | --- | --- |
24-| Les packages Go | `make test-go`, ou `go test ./schema -run Invalid -v` pour un test avec sa sortie |
25-| `scripts/agents.sh` | `make test-scripts` |
24+| Les packages Go | `make test-go`, ou `go test -race ./internal/agents -run Limit -v` pour un test |
25+| Le frontend (vitest) | `make test-frontend` |
26+| L'outillage shell | `make test-scripts` |
27+| Le lint Go (golangci-lint, épinglé dans `tools/go.mod`) | `make lint-go` |
28+| Les types du frontend et les avertissements Svelte | `make lint-frontend` |
29+| Les en-têtes de licence | `make lint-headers` |
30+| Les bindings TypeScript commités sont à jour | `make bindings-check` |
31+
32+## Lancer les tests avec les vrais agents
33+
34+```bash
35+make test-real
36+```
37+
38+La commande lance les tests compilés avec le tag `realagents` : aujourd'hui, la poignée de main ACP avec Alice. Ils ont besoin de `mm` (ou `MM_BIN`) et de llmman, et ne démarrent les agents que via `scripts/agents.sh` : la limite de deux agents et le déchargement s'appliquent.
2639
2740 ## Variantes
2841
29-- **Les spikes** (`spikes/acp-trace`, `spikes/wails-ui`) sont des modules Go séparés, sans tests unitaires ; `make test` ne les compile pas. Vérifiez-les avec `go vet .` dans chaque dossier, et `npx svelte-check` dans `spikes/wails-ui/frontend`.
30-- **Les tests avec de vrais agents** n'existent pas encore ; quand ils existeront (issue #10), ils utiliseront le build tag `realagents` et passeront par `scripts/agents.sh`.
31-- **`make: warning: Clock skew detected`** apparaît quand le dépôt se trouve dans un dossier partagé avec une autre machine dont l'horloge diffère ; le résultat n'en dépend pas.
42+- **`go test` se plaint de `frontend/dist`** : le binaire Go embarque le frontend construit. `make test-go` le construit s'il manque ; avec `go test` seul, lancez `make frontend` une fois.
43+- **Les spikes** (`spikes/`) sont des modules séparés, non testés ; vérifiez-les avec `go vet .` dans chaque dossier.
44+- **`make: warning: Clock skew detected`** apparaît quand le dépôt est dans un dossier partagé avec une autre machine ; le résultat n'en dépend pas.
3245
3346 ## Voir aussi
3447
35-- Ce que vérifient les tests de configuration → [Référence : le fichier de configuration](../reference/configuration.md)
48+- Toutes les cibles → [Référence : commandes](../reference/commandes.md)
3649 - L'outillage testé → [Référence : scripts/agents.sh](../reference/script-agents.md)
@@ -1,36 +1,49 @@
1 # Comment lancer les tests1 # Comment lancer les tests
2 2
3-Ce guide montre comment lancer la suite de tests du projet avant un commit. Il suppose un clone du dépôt et un terminal ouvert à sa racine.3+Ce guide montre comment vérifier le projet avant un commit ou une merge request. Il suppose les prérequis de développement installés (voir [Comment développer l'application](developper-l-application.md)) et un terminal ouvert à la racine du dépôt.
4 4
5-## Tout lancer5+## Lancer ce que lance la CI
6 6
7 ```bash7 ```bash
8-make test8+make check
9 ```9 ```
10 10
11-La commande lance les tests unitaires Go (`go test ./...`) et ceux de l'outillage shell (`scripts/agents_test.sh`). Aucun n'a besoin d'un vrai agent, d'un modèle ni du réseau : `mm` et llmman sont remplacés par des imitations. Un passage réussi se termine par :11+La commande lance les linters (`make lint`), puis tous les tests qui n'ont besoin ni d'un vrai agent, ni d'un modèle, ni du réseau (`make test`). Un passage réussi se termine par :
12 12
13 ```text13 ```text
14-ok git.rickub.com/bots-garden/sidekicks/schema …
15 14 passed, 0 failed14 14 passed, 0 failed
15+all licence header tests passed
16 ```16 ```
17 17
18-`make help` liste les cibles.18+La CI (`.github/workflows/ci.yml`) lance la même commande, plus `make bindings-check`.
19 19
20 ## Lancer une partie20 ## Lancer une partie
21 21
22-| Pour tester | Commande |22+| Pour vérifier | Commande |
23 | --- | --- |23 | --- | --- |
24-| Les packages Go | `make test-go`, ou `go test ./schema -run Invalid -v` pour un test avec sa sortie |24+| Les packages Go | `make test-go`, ou `go test -race ./internal/agents -run Limit -v` pour un test |
25-| `scripts/agents.sh` | `make test-scripts` |25+| Le frontend (vitest) | `make test-frontend` |
26+| L'outillage shell | `make test-scripts` |
27+| Le lint Go (golangci-lint, épinglé dans `tools/go.mod`) | `make lint-go` |
28+| Les types du frontend et les avertissements Svelte | `make lint-frontend` |
29+| Les en-têtes de licence | `make lint-headers` |
30+| Les bindings TypeScript commités sont à jour | `make bindings-check` |
31+
32+## Lancer les tests avec les vrais agents
33+
34+```bash
35+make test-real
36+```
37+
38+La commande lance les tests compilés avec le tag `realagents` : aujourd'hui, la poignée de main ACP avec Alice. Ils ont besoin de `mm` (ou `MM_BIN`) et de llmman, et ne démarrent les agents que via `scripts/agents.sh` : la limite de deux agents et le déchargement s'appliquent.
26 39
27 ## Variantes40 ## Variantes
28 41
29-- **Les spikes** (`spikes/acp-trace`, `spikes/wails-ui`) sont des modules Go séparés, sans tests unitaires ; `make test` ne les compile pas. Vérifiez-les avec `go vet .` dans chaque dossier, et `npx svelte-check` dans `spikes/wails-ui/frontend`.42+- **`go test` se plaint de `frontend/dist`** : le binaire Go embarque le frontend construit. `make test-go` le construit s'il manque ; avec `go test` seul, lancez `make frontend` une fois.
30-- **Les tests avec de vrais agents** n'existent pas encore ; quand ils existeront (issue #10), ils utiliseront le build tag `realagents` et passeront par `scripts/agents.sh`.43+- **Les spikes** (`spikes/`) sont des modules séparés, non testés ; vérifiez-les avec `go vet .` dans chaque dossier.
31-- **`make: warning: Clock skew detected`** apparaît quand le dépôt se trouve dans un dossier partagé avec une autre machine dont l'horloge diffère ; le résultat n'en dépend pas.44+- **`make: warning: Clock skew detected`** apparaît quand le dépôt est dans un dossier partagé avec une autre machine ; le résultat n'en dépend pas.
32 45
33 ## Voir aussi46 ## Voir aussi
34 47
35-- Ce que vérifient les tests de configuration → [Référence : le fichier de configuration](../reference/configuration.md)48+- Toutes les cibles → [Référence : commandes](../reference/commandes.md)
36 - L'outillage testé → [Référence : scripts/agents.sh](../reference/script-agents.md)49 - L'outillage testé → [Référence : scripts/agents.sh](../reference/script-agents.md)
added docs/fr/how-to/tester-la-milestone-m1-sous-macos.md +112 -0
new file mode 100644
@@ -0,0 +1,112 @@
1+# Comment tester la milestone M1 sous macOS
2+
3+Ce guide vérifie sur un Mac (Apple Silicon) ce que la milestone M1 (Fondations) a produit : le build, les vérifications automatiques, le chargement de la configuration, le gestionnaire d'agents et le shell à panels. Il suppose le Mac préparé pour la [milestone Initialize](tester-la-milestone-initialize-sous-macos.md) (Go, Node, outils Xcode, CLI Wails, `mm`, llmman et les modèles) et le dépôt sur la branche de M1. Comptez 30 minutes.
4+
5+## 0. Préparer
6+
7+```bash
8+npm install -g pnpm@12.11.2
9+pnpm --version # 12.11.2
10+make install
11+```
12+
13+`make install` installe les dépendances du frontend avec les binaires natifs de macOS **et** de Linux : le dossier partagé continue de fonctionner dans la sandbox Linux.
14+
15+- [ ] `make install` se termine sans erreur.
16+
17+## 1. Les vérifications automatiques (#8)
18+
19+```bash
20+make check
21+```
22+
23+Attendu à la fin : `14 passed, 0 failed` et `all licence header tests passed`, avec `0 issues.` de golangci-lint avant.
24+
25+```bash
26+make test-real
27+```
28+
29+Attendu : `ok … internal/acp` — la poignée de main ACP avec la vraie Alice. `scripts/agents.sh ps` ne liste ensuite aucun agent.
30+
31+- [ ] `make check` passe.
32+- [ ] `make test-real` passe.
33+
34+## 2. L'application avec les agents de test (#10, #11, #12, #13)
35+
36+```bash
37+SIDEKICKS_CONFIG="$PWD/examples/test-agents.yaml" make dev
38+```
39+
40+Vérifiez, dans l'ordre :
41+
42+1. **Fenêtre** : le dock gauche commence sous les boutons rouge/orange/vert ; la barre d'état indique « 0 agent actif » et le chemin de la configuration.
43+2. **Agents** : Alice, Bob, Riker, Garfield sont listés, « arrêté ».
44+3. **Démarrage** : passez sur Alice, cliquez sur ▶ — orange (« démarre… »), puis vert (« prêt ») ; « 1 agent actif ».
45+4. **Journal** : cliquez sur **Journal** — le panneau s'ouvre à une taille confortable avec les lignes `mm v1.0.3 | …` d'Alice ; le sélecteur affiche Alice.
46+5. **Limite** : démarrez Bob, puis Riker. Riker démarre et **Alice** (utilisée le moins récemment) repasse à « arrêté » : deux agents au plus. Dans un terminal, `scripts/agents.sh ps` liste deux agents.
47+6. **Arrêt** : passez sur Bob, cliquez sur ■ — « arrêté ».
48+7. **Docks** : cliquez sur **Agents**, **Journal**, **Settings** dans la barre d'état — chaque dock se replie et se déplie ; déplacez les séparateurs, quittez (⌘Q) et relancez : les tailles sont conservées.
49+8. **Quitter avec des agents en marche** : Riker toujours en marche, quittez (⌘Q). `scripts/agents.sh ps` ne liste plus ni agent ni modèle.
50+9. **Thème** : Réglages Système → Apparence → Clair / Sombre : toute la fenêtre suit.
51+
52+- [ ] Les points 1 à 9 sont conformes.
53+
54+## 3. La configuration (#9)
55+
56+Suivez les étapes 5 et 6 du tutoriel [Votre premier lancement de Sidekicks](../tutorials/premier-lancement.md) : une faute de frappe dans le fichier affiche en moins d'une seconde un bandeau rouge et des messages localisés, et la correction les fait disparaître sans redémarrer.
57+
58+Vérifiez ensuite le fichier par défaut : quittez, mettez votre configuration de côté si vous en avez une, et lancez sans `SIDEKICKS_CONFIG` :
59+
60+```bash
61+mv ~/Library/Application\ Support/sidekicks/config.yaml /tmp/ 2>/dev/null
62+make dev
63+```
64+
65+Settings affiche le chemin complet `/Users/<vous>/Library/Application Support/sidekicks/config.yaml`, « emplacement par défaut — créé pour vous » et « ✓ Configuration valide ». Ouvrez le fichier pour vérifier que chaque clé est commentée — sans guillemets autour de `~`, sinon le shell ne le remplace pas par votre dossier personnel :
66+
67+```bash
68+open ~/Library/Application\ Support/sidekicks/ # le dossier, dans le Finder
69+open -e ~/Library/Application\ Support/sidekicks/config.yaml # le fichier, dans TextEdit
70+```
71+
72+Pour que la configuration par défaut fonctionne avec les agents de test, écrivez-la avec des chemins absolus vers le dépôt (le fichier vide est gardé en `.bak`) :
73+
74+```bash
75+make install-test-config
76+```
77+
78+Elle contient aussi, pour chaque agent, `MM_BIN` et votre `PATH` : une application ouverte depuis le Finder ne reçoit qu'un `PATH` minimal et ne trouverait ni `mm` ni `jq`. Si vous préférez retrouver votre propre fichier, remettez-le en place à la place :
79+
80+```bash
81+mv /tmp/config.yaml ~/Library/Application\ Support/sidekicks/config.yaml
82+```
83+
84+- [ ] Les problèmes sont localisés et le fichier est rechargé à l'enregistrement.
85+- [ ] Le fichier par défaut est créé, commenté et valide.
86+
87+## 4. L'application empaquetée
88+
89+```bash
90+make package
91+open bin/sidekicks.app
92+```
93+
94+Sans `SIDEKICKS_CONFIG`, elle ouvre la configuration par défaut — celle des agents de test après `make install-test-config`. Refaites les points 1, 3 et 7 de la partie 2.
95+
96+- [ ] `bin/sidekicks.app` s'ouvre et se comporte comme en développement.
97+
98+Si tout est conforme, M1 est confirmée : fermez l'issue #12 et passez la milestone **M1 · Fondations** à `closed` dans `.tickets/milestones.yaml` — ou demandez-le à l'assistant. Sinon, commentez l'issue concernée avec ce qui diffère.
99+
100+## En cas de problème
101+
102+| Symptôme | Cause probable | Remède |
103+| --- | --- | --- |
104+| `pnpm: command not found` | pnpm non installé | `npm install -g pnpm@12.11.2` |
105+| `Cannot find native binding` pendant le build du frontend | `node_modules` installé avec npm, ou avant l'arrivée de `pnpm-workspace.yaml` | supprimez `frontend/node_modules`, lancez `make install` |
106+| `wails3: command not found` | `GOPATH/bin` absent du `PATH` | `export PATH="$(go env GOPATH)/bin:$PATH"` |
107+| Un agent passe aussitôt à « erreur » | `mm` ou llmman absent | lisez le message sous l'agent et son Journal |
108+| `make check` échoue dans le code de `bindings` | bindings plus anciens que le code Go | `make bindings` |
109+
110+## Voir aussi
111+
112+- [Comment développer l'application](developper-l-application.md) · [Référence : états des agents et événements](../reference/agents-et-evenements.md)
new file mode 100644
@@ -0,0 +1,112 @@
1+# Comment tester la milestone M1 sous macOS
2+
3+Ce guide vérifie sur un Mac (Apple Silicon) ce que la milestone M1 (Fondations) a produit : le build, les vérifications automatiques, le chargement de la configuration, le gestionnaire d'agents et le shell à panels. Il suppose le Mac préparé pour la [milestone Initialize](tester-la-milestone-initialize-sous-macos.md) (Go, Node, outils Xcode, CLI Wails, `mm`, llmman et les modèles) et le dépôt sur la branche de M1. Comptez 30 minutes.
4+
5+## 0. Préparer
6+
7+```bash
8+npm install -g pnpm@12.11.2
9+pnpm --version # 12.11.2
10+make install
11+```
12+
13+`make install` installe les dépendances du frontend avec les binaires natifs de macOS **et** de Linux : le dossier partagé continue de fonctionner dans la sandbox Linux.
14+
15+- [ ] `make install` se termine sans erreur.
16+
17+## 1. Les vérifications automatiques (#8)
18+
19+```bash
20+make check
21+```
22+
23+Attendu à la fin : `14 passed, 0 failed` et `all licence header tests passed`, avec `0 issues.` de golangci-lint avant.
24+
25+```bash
26+make test-real
27+```
28+
29+Attendu : `ok … internal/acp` — la poignée de main ACP avec la vraie Alice. `scripts/agents.sh ps` ne liste ensuite aucun agent.
30+
31+- [ ] `make check` passe.
32+- [ ] `make test-real` passe.
33+
34+## 2. L'application avec les agents de test (#10, #11, #12, #13)
35+
36+```bash
37+SIDEKICKS_CONFIG="$PWD/examples/test-agents.yaml" make dev
38+```
39+
40+Vérifiez, dans l'ordre :
41+
42+1. **Fenêtre** : le dock gauche commence sous les boutons rouge/orange/vert ; la barre d'état indique « 0 agent actif » et le chemin de la configuration.
43+2. **Agents** : Alice, Bob, Riker, Garfield sont listés, « arrêté ».
44+3. **Démarrage** : passez sur Alice, cliquez sur ▶ — orange (« démarre… »), puis vert (« prêt ») ; « 1 agent actif ».
45+4. **Journal** : cliquez sur **Journal** — le panneau s'ouvre à une taille confortable avec les lignes `mm v1.0.3 | …` d'Alice ; le sélecteur affiche Alice.
46+5. **Limite** : démarrez Bob, puis Riker. Riker démarre et **Alice** (utilisée le moins récemment) repasse à « arrêté » : deux agents au plus. Dans un terminal, `scripts/agents.sh ps` liste deux agents.
47+6. **Arrêt** : passez sur Bob, cliquez sur ■ — « arrêté ».
48+7. **Docks** : cliquez sur **Agents**, **Journal**, **Settings** dans la barre d'état — chaque dock se replie et se déplie ; déplacez les séparateurs, quittez (⌘Q) et relancez : les tailles sont conservées.
49+8. **Quitter avec des agents en marche** : Riker toujours en marche, quittez (⌘Q). `scripts/agents.sh ps` ne liste plus ni agent ni modèle.
50+9. **Thème** : Réglages Système → Apparence → Clair / Sombre : toute la fenêtre suit.
51+
52+- [ ] Les points 1 à 9 sont conformes.
53+
54+## 3. La configuration (#9)
55+
56+Suivez les étapes 5 et 6 du tutoriel [Votre premier lancement de Sidekicks](../tutorials/premier-lancement.md) : une faute de frappe dans le fichier affiche en moins d'une seconde un bandeau rouge et des messages localisés, et la correction les fait disparaître sans redémarrer.
57+
58+Vérifiez ensuite le fichier par défaut : quittez, mettez votre configuration de côté si vous en avez une, et lancez sans `SIDEKICKS_CONFIG` :
59+
60+```bash
61+mv ~/Library/Application\ Support/sidekicks/config.yaml /tmp/ 2>/dev/null
62+make dev
63+```
64+
65+Settings affiche le chemin complet `/Users/<vous>/Library/Application Support/sidekicks/config.yaml`, « emplacement par défaut — créé pour vous » et « ✓ Configuration valide ». Ouvrez le fichier pour vérifier que chaque clé est commentée — sans guillemets autour de `~`, sinon le shell ne le remplace pas par votre dossier personnel :
66+
67+```bash
68+open ~/Library/Application\ Support/sidekicks/ # le dossier, dans le Finder
69+open -e ~/Library/Application\ Support/sidekicks/config.yaml # le fichier, dans TextEdit
70+```
71+
72+Pour que la configuration par défaut fonctionne avec les agents de test, écrivez-la avec des chemins absolus vers le dépôt (le fichier vide est gardé en `.bak`) :
73+
74+```bash
75+make install-test-config
76+```
77+
78+Elle contient aussi, pour chaque agent, `MM_BIN` et votre `PATH` : une application ouverte depuis le Finder ne reçoit qu'un `PATH` minimal et ne trouverait ni `mm` ni `jq`. Si vous préférez retrouver votre propre fichier, remettez-le en place à la place :
79+
80+```bash
81+mv /tmp/config.yaml ~/Library/Application\ Support/sidekicks/config.yaml
82+```
83+
84+- [ ] Les problèmes sont localisés et le fichier est rechargé à l'enregistrement.
85+- [ ] Le fichier par défaut est créé, commenté et valide.
86+
87+## 4. L'application empaquetée
88+
89+```bash
90+make package
91+open bin/sidekicks.app
92+```
93+
94+Sans `SIDEKICKS_CONFIG`, elle ouvre la configuration par défaut — celle des agents de test après `make install-test-config`. Refaites les points 1, 3 et 7 de la partie 2.
95+
96+- [ ] `bin/sidekicks.app` s'ouvre et se comporte comme en développement.
97+
98+Si tout est conforme, M1 est confirmée : fermez l'issue #12 et passez la milestone **M1 · Fondations** à `closed` dans `.tickets/milestones.yaml` — ou demandez-le à l'assistant. Sinon, commentez l'issue concernée avec ce qui diffère.
99+
100+## En cas de problème
101+
102+| Symptôme | Cause probable | Remède |
103+| --- | --- | --- |
104+| `pnpm: command not found` | pnpm non installé | `npm install -g pnpm@12.11.2` |
105+| `Cannot find native binding` pendant le build du frontend | `node_modules` installé avec npm, ou avant l'arrivée de `pnpm-workspace.yaml` | supprimez `frontend/node_modules`, lancez `make install` |
106+| `wails3: command not found` | `GOPATH/bin` absent du `PATH` | `export PATH="$(go env GOPATH)/bin:$PATH"` |
107+| Un agent passe aussitôt à « erreur » | `mm` ou llmman absent | lisez le message sous l'agent et son Journal |
108+| `make check` échoue dans le code de `bindings` | bindings plus anciens que le code Go | `make bindings` |
109+
110+## Voir aussi
111+
112+- [Comment développer l'application](developper-l-application.md) · [Référence : états des agents et événements](../reference/agents-et-evenements.md)
added docs/fr/reference/agents-et-evenements.md +51 -0
new file mode 100644
@@ -0,0 +1,51 @@
1+# Référence : états des agents et événements
2+
3+## États d'un agent
4+
5+| État | Affiché | Signification |
6+| --- | --- | --- |
7+| `stopped` | arrêté | Aucun processus. L'état initial de chaque agent. |
8+| `starting` | démarre… | Le processus est lancé et la poignée de main ACP (`initialize`) est en cours. |
9+| `ready` | prêt | En marche et inoccupé. |
10+| `busy` | occupé | En marche et utilisé par au moins une conversation. |
11+| `error` | erreur | Le démarrage a échoué, ou le processus s'est arrêté seul (crash). Le statut porte la raison, suivie des dernières lignes stderr de l'agent en cas d'échec au démarrage. |
12+
13+`outdated` (affiché « à redémarrer ») marque un agent en marche dont la définition a changé dans la configuration ; un redémarrage l'applique.
14+
15+## Transitions
16+
17+| Depuis | Événement | Vers |
18+| --- | --- | --- |
19+| `stopped`, `error` | démarrage demandé (▶ dans le panneau Agents, ou première utilisation) | `starting` |
20+| `starting` | la poignée de main réussit | `ready` |
21+| `starting` | la poignée de main échoue, protocole autre qu'ACP v1, délai dépassé (30 s) | `error` |
22+| `ready` | utilisé | `busy` |
23+| `busy` | le dernier utilisateur a fini | `ready` |
24+| `ready`, `busy` | arrêt demandé (■), fermeture de l'application, agent retiré de la configuration | `stopped` |
25+| `ready` | inutilisé depuis `limits.stopIdleAgentsAfter` (vérifié toutes les 30 s) | `stopped` |
26+| `ready` | un autre agent doit démarrer et celui-ci est l'agent inoccupé utilisé le moins récemment | `stopped` |
27+| `ready`, `busy` | le processus s'arrête seul | `error` (jamais redémarré automatiquement) |
28+| `starting` | arrêt demandé | `stopped` dès la fin du démarrage |
29+
30+Quand `limits.maxRunningAgents` agents tournent et qu'ils sont tous `busy`, un démarrage est refusé avec le message `too many agents running: N agent(s) busy (…), the limit is N`.
31+
32+Arrêter un agent ferme son entrée standard, attend 5 secondes, puis tue son groupe de processus (l'agent et ce qu'il a lancé, comme `mm` sous `scripts/agents.sh`).
33+
34+## Événements du Go vers l'interface
35+
36+| Événement | Charge | Envoyé quand |
37+| --- | --- | --- |
38+| `config:changed` | `{path, valid, problems[]}` | Le fichier de configuration a été enregistré et rechargé (valide ou non). |
39+| `agent:state` | `{agent, state, error?}` | Un agent change d'état ou sa définition change. |
40+| `agent:log` | `{agent, line}` | Un agent écrit une ligne sur sa sortie d'erreur. |
41+
42+Les noms et types Go sont dans `internal/events` ; les constantes et types TypeScript sont générés dans `frontend/bindings/events` et `frontend/bindings/…/internal/events`.
43+
44+## Services appelés par l'interface
45+
46+| Service | Méthode | Effet |
47+| --- | --- | --- |
48+| `AgentService` | `List()` | Tous les agents avec `name, title, icon, description, state, error, outdated`, dans l'ordre de la configuration. |
49+| | `Start(name)`, `Stop(name)`, `Restart(name)` | Comme dans les transitions ci-dessus ; une erreur est renvoyée à l'interface. |
50+| | `Logs(name)` | Les 200 dernières lignes stderr de l'agent. |
51+| `ConfigService` | `Current()` | `path, source, created, valid, problems[], warnings[], theme, language, channels[]`. |
new file mode 100644
@@ -0,0 +1,51 @@
1+# Référence : états des agents et événements
2+
3+## États d'un agent
4+
5+| État | Affiché | Signification |
6+| --- | --- | --- |
7+| `stopped` | arrêté | Aucun processus. L'état initial de chaque agent. |
8+| `starting` | démarre… | Le processus est lancé et la poignée de main ACP (`initialize`) est en cours. |
9+| `ready` | prêt | En marche et inoccupé. |
10+| `busy` | occupé | En marche et utilisé par au moins une conversation. |
11+| `error` | erreur | Le démarrage a échoué, ou le processus s'est arrêté seul (crash). Le statut porte la raison, suivie des dernières lignes stderr de l'agent en cas d'échec au démarrage. |
12+
13+`outdated` (affiché « à redémarrer ») marque un agent en marche dont la définition a changé dans la configuration ; un redémarrage l'applique.
14+
15+## Transitions
16+
17+| Depuis | Événement | Vers |
18+| --- | --- | --- |
19+| `stopped`, `error` | démarrage demandé (▶ dans le panneau Agents, ou première utilisation) | `starting` |
20+| `starting` | la poignée de main réussit | `ready` |
21+| `starting` | la poignée de main échoue, protocole autre qu'ACP v1, délai dépassé (30 s) | `error` |
22+| `ready` | utilisé | `busy` |
23+| `busy` | le dernier utilisateur a fini | `ready` |
24+| `ready`, `busy` | arrêt demandé (■), fermeture de l'application, agent retiré de la configuration | `stopped` |
25+| `ready` | inutilisé depuis `limits.stopIdleAgentsAfter` (vérifié toutes les 30 s) | `stopped` |
26+| `ready` | un autre agent doit démarrer et celui-ci est l'agent inoccupé utilisé le moins récemment | `stopped` |
27+| `ready`, `busy` | le processus s'arrête seul | `error` (jamais redémarré automatiquement) |
28+| `starting` | arrêt demandé | `stopped` dès la fin du démarrage |
29+
30+Quand `limits.maxRunningAgents` agents tournent et qu'ils sont tous `busy`, un démarrage est refusé avec le message `too many agents running: N agent(s) busy (…), the limit is N`.
31+
32+Arrêter un agent ferme son entrée standard, attend 5 secondes, puis tue son groupe de processus (l'agent et ce qu'il a lancé, comme `mm` sous `scripts/agents.sh`).
33+
34+## Événements du Go vers l'interface
35+
36+| Événement | Charge | Envoyé quand |
37+| --- | --- | --- |
38+| `config:changed` | `{path, valid, problems[]}` | Le fichier de configuration a été enregistré et rechargé (valide ou non). |
39+| `agent:state` | `{agent, state, error?}` | Un agent change d'état ou sa définition change. |
40+| `agent:log` | `{agent, line}` | Un agent écrit une ligne sur sa sortie d'erreur. |
41+
42+Les noms et types Go sont dans `internal/events` ; les constantes et types TypeScript sont générés dans `frontend/bindings/events` et `frontend/bindings/…/internal/events`.
43+
44+## Services appelés par l'interface
45+
46+| Service | Méthode | Effet |
47+| --- | --- | --- |
48+| `AgentService` | `List()` | Tous les agents avec `name, title, icon, description, state, error, outdated`, dans l'ordre de la configuration. |
49+| | `Start(name)`, `Stop(name)`, `Restart(name)` | Comme dans les transitions ci-dessus ; une erreur est renvoyée à l'interface. |
50+| | `Logs(name)` | Les 200 dernières lignes stderr de l'agent. |
51+| `ConfigService` | `Current()` | `path, source, created, valid, problems[], warnings[], theme, language, channels[]`. |
added docs/fr/reference/commandes.md +51 -0
new file mode 100644
@@ -0,0 +1,51 @@
1+# Référence : commandes
2+
3+## `sidekicks`
4+
5+```text
6+sidekicks [--config <fichier>]
7+```
8+
9+| Option / variable | Effet |
10+| --- | --- |
11+| `--config <fichier>` | Fichier de configuration à utiliser. Prioritaire sur tout le reste. |
12+| `SIDEKICKS_CONFIG` | Fichier de configuration quand `--config` n'est pas donné. |
13+| (aucune) | `<dossier de config utilisateur>/sidekicks/config.yaml`, créé avec des commentaires s'il manque. |
14+
15+L'ordre est détaillé dans la [référence de la configuration](configuration.md#emplacement).
16+
17+## Cibles `make`
18+
19+| Cible | Effet |
20+| --- | --- |
21+| `help` | Liste les cibles. |
22+| `install` | `pnpm install` dans `frontend/` (binaires natifs macOS et Linux). |
23+| `bindings` | Régénère `frontend/bindings` à partir des services et événements Go (`wails3 generate bindings -clean=true -ts -i`). |
24+| `frontend` | `install`, puis construit `frontend/dist`. |
25+| `dev` | `wails3 dev` : l'application avec rechargement à chaud. |
26+| `build` | `wails3 build` : `bin/sidekicks`. |
27+| `package` | `wails3 package` : sous macOS `bin/sidekicks.app`. |
28+| `install-test-config` | `scripts/install-test-config.sh` : écrit `examples/test-agents.yaml` à l'emplacement par défaut de la configuration, avec des chemins absolus vers le dépôt, et `MM_BIN` et le `PATH` courant dans l'`env` de chaque agent ; un fichier existant est gardé sous `config.yaml.<date>.bak`. |
29+| `check` | `lint` puis `test`. |
30+| `test` | `test-go`, `test-frontend`, `test-scripts`. |
31+| `test-go` | `go test ./...` (construit `frontend/dist` d'abord s'il manque). |
32+| `test-frontend` | `vitest run` dans `frontend/`. |
33+| `test-scripts` | `scripts/agents_test.sh` et `scripts/check-license-headers_test.sh`. |
34+| `test-real` | `go test -tags realagents -count=1 ./...` — nécessite `mm` et llmman. |
35+| `lint` | `lint-go`, `lint-frontend`, `lint-headers`. |
36+| `lint-go` | golangci-lint v2.14.0 (`tools/go.mod`), configuration `.golangci.yml`. |
37+| `lint-frontend` | `svelte-check`. |
38+| `lint-headers` | `scripts/check-license-headers.sh`. |
39+| `bindings-check` | `bindings`, puis échoue si `frontend/bindings` diffère de la version commitée. |
40+
41+## `scripts/check-license-headers.sh`
42+
43+```text
44+scripts/check-license-headers.sh [racine du dépôt]
45+```
46+
47+Liste les fichiers sources (`.go .ts .js .svelte .css .html .sh .py .yml .yaml`, `Makefile`) connus de git dont les trois premières lignes ne contiennent pas `Sidekicks Source-Available License`, et se termine avec le code 1 s'il y en a. Non vérifiés : `frontend/bindings`, `build/` et `Taskfile.yml` de Wails, `*.d.ts`, `pnpm-lock.yaml`, `vite.config.ts`, `svelte.config.js`, `agents/`, `*/testdata/*`, `git.sh`, `new.*.sh`, `tag-step.sh`, `.claude/`, `.tickets/`.
48+
49+## CI
50+
51+`.github/workflows/ci.yml`, à chaque push et merge request : installe Go (version de `go.mod`), Node 22, pnpm (version de `frontend/package.json`), les paquets de build GTK 4 / WebKitGTK 6 et la CLI Wails, puis lance `make bindings-check` et `make check`.
new file mode 100644
@@ -0,0 +1,51 @@
1+# Référence : commandes
2+
3+## `sidekicks`
4+
5+```text
6+sidekicks [--config <fichier>]
7+```
8+
9+| Option / variable | Effet |
10+| --- | --- |
11+| `--config <fichier>` | Fichier de configuration à utiliser. Prioritaire sur tout le reste. |
12+| `SIDEKICKS_CONFIG` | Fichier de configuration quand `--config` n'est pas donné. |
13+| (aucune) | `<dossier de config utilisateur>/sidekicks/config.yaml`, créé avec des commentaires s'il manque. |
14+
15+L'ordre est détaillé dans la [référence de la configuration](configuration.md#emplacement).
16+
17+## Cibles `make`
18+
19+| Cible | Effet |
20+| --- | --- |
21+| `help` | Liste les cibles. |
22+| `install` | `pnpm install` dans `frontend/` (binaires natifs macOS et Linux). |
23+| `bindings` | Régénère `frontend/bindings` à partir des services et événements Go (`wails3 generate bindings -clean=true -ts -i`). |
24+| `frontend` | `install`, puis construit `frontend/dist`. |
25+| `dev` | `wails3 dev` : l'application avec rechargement à chaud. |
26+| `build` | `wails3 build` : `bin/sidekicks`. |
27+| `package` | `wails3 package` : sous macOS `bin/sidekicks.app`. |
28+| `install-test-config` | `scripts/install-test-config.sh` : écrit `examples/test-agents.yaml` à l'emplacement par défaut de la configuration, avec des chemins absolus vers le dépôt, et `MM_BIN` et le `PATH` courant dans l'`env` de chaque agent ; un fichier existant est gardé sous `config.yaml.<date>.bak`. |
29+| `check` | `lint` puis `test`. |
30+| `test` | `test-go`, `test-frontend`, `test-scripts`. |
31+| `test-go` | `go test ./...` (construit `frontend/dist` d'abord s'il manque). |
32+| `test-frontend` | `vitest run` dans `frontend/`. |
33+| `test-scripts` | `scripts/agents_test.sh` et `scripts/check-license-headers_test.sh`. |
34+| `test-real` | `go test -tags realagents -count=1 ./...` — nécessite `mm` et llmman. |
35+| `lint` | `lint-go`, `lint-frontend`, `lint-headers`. |
36+| `lint-go` | golangci-lint v2.14.0 (`tools/go.mod`), configuration `.golangci.yml`. |
37+| `lint-frontend` | `svelte-check`. |
38+| `lint-headers` | `scripts/check-license-headers.sh`. |
39+| `bindings-check` | `bindings`, puis échoue si `frontend/bindings` diffère de la version commitée. |
40+
41+## `scripts/check-license-headers.sh`
42+
43+```text
44+scripts/check-license-headers.sh [racine du dépôt]
45+```
46+
47+Liste les fichiers sources (`.go .ts .js .svelte .css .html .sh .py .yml .yaml`, `Makefile`) connus de git dont les trois premières lignes ne contiennent pas `Sidekicks Source-Available License`, et se termine avec le code 1 s'il y en a. Non vérifiés : `frontend/bindings`, `build/` et `Taskfile.yml` de Wails, `*.d.ts`, `pnpm-lock.yaml`, `vite.config.ts`, `svelte.config.js`, `agents/`, `*/testdata/*`, `git.sh`, `new.*.sh`, `tag-step.sh`, `.claude/`, `.tickets/`.
48+
49+## CI
50+
51+`.github/workflows/ci.yml`, à chaque push et merge request : installe Go (version de `go.mod`), Node 22, pnpm (version de `frontend/package.json`), les paquets de build GTK 4 / WebKitGTK 6 et la CLI Wails, puis lance `make bindings-check` et `make check`.
modified docs/fr/reference/configuration.md +5 -5
@@ -7,9 +7,9 @@
77
88 Un fichier YAML décrit les agents avec lesquels Sidekicks peut converser, les channels créés au démarrage et les préférences. Les conversations, les messages et les sessions ACP n'y figurent pas (voir [Architecture](../explanation/architecture.md)).
99
10-**État (29/10/2026)** : le format et son JSON Schema existent et sont testés ; l'application qui lit le fichier n'existe pas encore (issue #9). Les sections marquées *chargeur* décrivent un comportement que l'issue #9 doit implémenter.
10+L'application le lit avec le package Go `internal/config` (milestone M1) : validation, valeurs par défaut, chemins, fichier par défaut, rechargement à chaque enregistrement.
1111
12-## Emplacement (*chargeur*)
12+## Emplacement
1313
1414 Le fichier est cherché dans cet ordre ; la première source applicable l'emporte :
1515
@@ -55,7 +55,7 @@ Les clés inconnues sont des **erreurs**, à tous les niveaux : une faute de fra
5555 | Clé | Type | Défaut | Signification |
5656 | --- | --- | --- | --- |
5757 | `maxRunningAgents` | entier ≥ 1 | `2` | Agents actifs en même temps. Chaque agent local garde un modèle en mémoire ; la valeur par défaut convient à un portable de 32 Go avec de petits modèles. |
58-| `stopIdleAgentsAfter` | durée Go (`30s`, `10m`, `1h30m`) ou `0` | `10m` | Arrête un agent inutilisé depuis ce délai pour libérer sa mémoire. `0` ne l'arrête jamais. |
58+| `stopIdleAgentsAfter` | durée Go (`30s`, `10m`, `1h30m`), ou `0` (nombre ou texte) | `10m` | Arrête un agent inutilisé depuis ce délai pour libérer sa mémoire. `0` ne l'arrête jamais. |
5959
6060 ### Un agent
6161
@@ -93,7 +93,7 @@ Le runtime indique à Sidekicks comment atteindre les **fichiers et le terminal*
9393
9494 Un channel déclaré ici est créé au démarrage **s'il n'existe pas encore** ; il vit ensuite dans l'application comme n'importe quel channel. Le retirer du fichier ne le supprime pas (supprimer un historique doit être une action explicite de l'utilisateur).
9595
96-## Règles vérifiées par le chargeur, pas par le schéma (*chargeur*)
96+## Règles vérifiées par le chargeur, pas par le schéma
9797
9898 JSON Schema ne sait pas les exprimer ; le chargeur de configuration (issue #9) les vérifie et les signale comme les erreurs de schéma :
9999
@@ -102,7 +102,7 @@ JSON Schema ne sait pas les exprimer ; le chargeur de configuration (issue #9) l
102102 3. chaque nom de la liste `agents` d'un channel est le nom d'un agent ;
103103 4. `command` existe (dans le `PATH`, ou comme fichier) — signalé comme avertissement, car un agent peut être installé plus tard.
104104
105-## Messages d'erreur (*chargeur*)
105+## Messages d'erreur
106106
107107 Chaque erreur indique la ligne et la clé, dans la langue de l'interface, et dit comment corriger. Exemples :
108108
@@ -7,9 +7,9 @@
7 7
8 Un fichier YAML décrit les agents avec lesquels Sidekicks peut converser, les channels créés au démarrage et les préférences. Les conversations, les messages et les sessions ACP n'y figurent pas (voir [Architecture](../explanation/architecture.md)).8 Un fichier YAML décrit les agents avec lesquels Sidekicks peut converser, les channels créés au démarrage et les préférences. Les conversations, les messages et les sessions ACP n'y figurent pas (voir [Architecture](../explanation/architecture.md)).
9 9
10-**État (29/10/2026)** : le format et son JSON Schema existent et sont testés ; l'application qui lit le fichier n'existe pas encore (issue #9). Les sections marquées *chargeur* décrivent un comportement que l'issue #9 doit implémenter.10+L'application le lit avec le package Go `internal/config` (milestone M1) : validation, valeurs par défaut, chemins, fichier par défaut, rechargement à chaque enregistrement.
11 11
12-## Emplacement (*chargeur*)12+## Emplacement
13 13
14 Le fichier est cherché dans cet ordre ; la première source applicable l'emporte :14 Le fichier est cherché dans cet ordre ; la première source applicable l'emporte :
15 15
@@ -55,7 +55,7 @@ Les clés inconnues sont des **erreurs**, à tous les niveaux : une faute de fra
55 | Clé | Type | Défaut | Signification |55 | Clé | Type | Défaut | Signification |
56 | --- | --- | --- | --- |56 | --- | --- | --- | --- |
57 | `maxRunningAgents` | entier ≥ 1 | `2` | Agents actifs en même temps. Chaque agent local garde un modèle en mémoire ; la valeur par défaut convient à un portable de 32 Go avec de petits modèles. |57 | `maxRunningAgents` | entier ≥ 1 | `2` | Agents actifs en même temps. Chaque agent local garde un modèle en mémoire ; la valeur par défaut convient à un portable de 32 Go avec de petits modèles. |
58-| `stopIdleAgentsAfter` | durée Go (`30s`, `10m`, `1h30m`) ou `0` | `10m` | Arrête un agent inutilisé depuis ce délai pour libérer sa mémoire. `0` ne l'arrête jamais. |58+| `stopIdleAgentsAfter` | durée Go (`30s`, `10m`, `1h30m`), ou `0` (nombre ou texte) | `10m` | Arrête un agent inutilisé depuis ce délai pour libérer sa mémoire. `0` ne l'arrête jamais. |
59 59
60 ### Un agent60 ### Un agent
61 61
@@ -93,7 +93,7 @@ Le runtime indique à Sidekicks comment atteindre les **fichiers et le terminal*
93 93
94 Un channel déclaré ici est créé au démarrage **s'il n'existe pas encore** ; il vit ensuite dans l'application comme n'importe quel channel. Le retirer du fichier ne le supprime pas (supprimer un historique doit être une action explicite de l'utilisateur).94 Un channel déclaré ici est créé au démarrage **s'il n'existe pas encore** ; il vit ensuite dans l'application comme n'importe quel channel. Le retirer du fichier ne le supprime pas (supprimer un historique doit être une action explicite de l'utilisateur).
95 95
96-## Règles vérifiées par le chargeur, pas par le schéma (*chargeur*)96+## Règles vérifiées par le chargeur, pas par le schéma
97 97
98 JSON Schema ne sait pas les exprimer ; le chargeur de configuration (issue #9) les vérifie et les signale comme les erreurs de schéma :98 JSON Schema ne sait pas les exprimer ; le chargeur de configuration (issue #9) les vérifie et les signale comme les erreurs de schéma :
99 99
@@ -102,7 +102,7 @@ JSON Schema ne sait pas les exprimer ; le chargeur de configuration (issue #9) l
102 3. chaque nom de la liste `agents` d'un channel est le nom d'un agent ;102 3. chaque nom de la liste `agents` d'un channel est le nom d'un agent ;
103 4. `command` existe (dans le `PATH`, ou comme fichier) — signalé comme avertissement, car un agent peut être installé plus tard.103 4. `command` existe (dans le `PATH`, ou comme fichier) — signalé comme avertissement, car un agent peut être installé plus tard.
104 104
105-## Messages d'erreur (*chargeur*)105+## Messages d'erreur
106 106
107 Chaque erreur indique la ligne et la clé, dans la langue de l'interface, et dit comment corriger. Exemples :107 Chaque erreur indique la ligne et la clé, dans la langue de l'interface, et dit comment corriger. Exemples :
108 108
added docs/fr/tutorials/premier-lancement.md +95 -0
new file mode 100644
@@ -0,0 +1,95 @@
1+# Tutoriel : votre premier lancement de Sidekicks
2+
3+À la fin de ce tutoriel, vous aurez écrit une configuration avec un agent, ouvert Sidekicks avec elle, démarré Alice depuis l'interface, lu son journal, et vu Sidekicks repérer une erreur dans le fichier puis se rétablir quand vous la corrigez.
4+
5+## Prérequis
6+
7+- Les outils de [Comment développer l'application](../how-to/developper-l-application.md), et `make install` fait une fois.
8+- `mm` dans votre `PATH` et llmman lancé, avec le modèle d'Alice téléchargé (voir le tutoriel [premier échange ACP](premier-echange-acp.md)).
9+- Un terminal ouvert à la racine du dépôt.
10+
11+## Étape 1 — Écrire une configuration
12+
13+Tapez :
14+
15+```bash
16+cat > /tmp/sidekicks-tutorial.yaml <<EOF
17+version: 1
18+agents:
19+ - name: alice
20+ title: Alice (assistante)
21+ icon: "👩"
22+ command: $PWD/scripts/agents.sh
23+ args: [run, alice]
24+ workspace: $PWD/agents/alice
25+channels:
26+ - name: général
27+ agents: [alice]
28+EOF
29+```
30+
31+Nous avons maintenant une configuration avec un agent, Alice, démarrée par le script du projet.
32+
33+## Étape 2 — Ouvrir Sidekicks avec elle
34+
35+Tapez :
36+
37+```bash
38+SIDEKICKS_CONFIG=/tmp/sidekicks-tutorial.yaml make dev
39+```
40+
41+Après une minute la première fois, la fenêtre de Sidekicks s'ouvre. Vous devriez voir, à gauche, **CHANNELS** avec `# général`, et **AGENTS** avec `👩 Alice (assistante)` et le mot « arrêté » en dessous. En bas, la barre d'état indique « 0 agent actif ».
42+
43+## Étape 3 — Démarrer Alice
44+
45+Passez la souris sur Alice : un bouton ▶ apparaît à droite. Cliquez dessus.
46+
47+Vous devriez voir la pastille d'Alice passer à l'orange (« démarre… ») puis au vert (« prêt »), et la barre d'état indiquer « 1 agent actif ».
48+
49+## Étape 4 — Lire son journal
50+
51+Cliquez sur **Journal** dans la barre d'état. Un panneau s'ouvre en bas avec les lignes d'Alice, en commençant par :
52+
53+```text
54+mm v1.0.3 | config: …/agents/alice/agent.yaml | provider: llmman | …
55+[acp] initialize: client "sidekicks" protocolVersion=1 …
56+```
57+
58+C'est ce qu'Alice écrit pour qui s'occupe d'elle ; Sidekicks le garde pour vous.
59+
60+## Étape 5 — Faire une erreur exprès
61+
62+Cliquez sur **Settings** dans la barre d'état. Il indique « ✓ Configuration valide ». Puis, dans un second terminal, cassez le fichier :
63+
64+```bash
65+sed -i.bak 's/ command:/ comand:/' /tmp/sidekicks-tutorial.yaml
66+```
67+
68+En moins d'une seconde, un bandeau rouge apparaît au centre, et **Settings** affiche :
69+
70+```text
71+ligne 3 — agents[0] : il manque la clé « command »
72+ligne 6 — agents[0].comand : clé inconnue « comand » ; vouliez-vous dire « command » ?
73+```
74+
75+Alice est toujours listée : Sidekicks garde la dernière configuration valide.
76+
77+## Étape 6 — Corriger
78+
79+```bash
80+mv /tmp/sidekicks-tutorial.yaml.bak /tmp/sidekicks-tutorial.yaml
81+```
82+
83+Le bandeau disparaît et Settings affiche de nouveau « ✓ Configuration valide », sans redémarrer.
84+
85+## Étape 7 — Arrêter
86+
87+Passez sur Alice et cliquez sur ■ : sa pastille devient grise. Fermez la fenêtre : si Alice tournait encore, Sidekicks l'aurait arrêtée et aurait libéré son modèle.
88+
89+## Et maintenant ?
90+
91+Vous avez lancé Sidekicks, piloté la vie d'un agent et vu comment la configuration est vérifiée. Pour aller plus loin :
92+
93+- Toutes les clés du fichier → [Référence : le fichier de configuration](../reference/configuration.md)
94+- Ce que veut dire chaque état → [Référence : états des agents et événements](../reference/agents-et-evenements.md)
95+- Pourquoi les agents démarrent et s'arrêtent ainsi → [Mémoire et modèles locaux](../explanation/memoire-et-modeles-locaux.md)
new file mode 100644
@@ -0,0 +1,95 @@
1+# Tutoriel : votre premier lancement de Sidekicks
2+
3+À la fin de ce tutoriel, vous aurez écrit une configuration avec un agent, ouvert Sidekicks avec elle, démarré Alice depuis l'interface, lu son journal, et vu Sidekicks repérer une erreur dans le fichier puis se rétablir quand vous la corrigez.
4+
5+## Prérequis
6+
7+- Les outils de [Comment développer l'application](../how-to/developper-l-application.md), et `make install` fait une fois.
8+- `mm` dans votre `PATH` et llmman lancé, avec le modèle d'Alice téléchargé (voir le tutoriel [premier échange ACP](premier-echange-acp.md)).
9+- Un terminal ouvert à la racine du dépôt.
10+
11+## Étape 1 — Écrire une configuration
12+
13+Tapez :
14+
15+```bash
16+cat > /tmp/sidekicks-tutorial.yaml <<EOF
17+version: 1
18+agents:
19+ - name: alice
20+ title: Alice (assistante)
21+ icon: "👩"
22+ command: $PWD/scripts/agents.sh
23+ args: [run, alice]
24+ workspace: $PWD/agents/alice
25+channels:
26+ - name: général
27+ agents: [alice]
28+EOF
29+```
30+
31+Nous avons maintenant une configuration avec un agent, Alice, démarrée par le script du projet.
32+
33+## Étape 2 — Ouvrir Sidekicks avec elle
34+
35+Tapez :
36+
37+```bash
38+SIDEKICKS_CONFIG=/tmp/sidekicks-tutorial.yaml make dev
39+```
40+
41+Après une minute la première fois, la fenêtre de Sidekicks s'ouvre. Vous devriez voir, à gauche, **CHANNELS** avec `# général`, et **AGENTS** avec `👩 Alice (assistante)` et le mot « arrêté » en dessous. En bas, la barre d'état indique « 0 agent actif ».
42+
43+## Étape 3 — Démarrer Alice
44+
45+Passez la souris sur Alice : un bouton ▶ apparaît à droite. Cliquez dessus.
46+
47+Vous devriez voir la pastille d'Alice passer à l'orange (« démarre… ») puis au vert (« prêt »), et la barre d'état indiquer « 1 agent actif ».
48+
49+## Étape 4 — Lire son journal
50+
51+Cliquez sur **Journal** dans la barre d'état. Un panneau s'ouvre en bas avec les lignes d'Alice, en commençant par :
52+
53+```text
54+mm v1.0.3 | config: …/agents/alice/agent.yaml | provider: llmman | …
55+[acp] initialize: client "sidekicks" protocolVersion=1 …
56+```
57+
58+C'est ce qu'Alice écrit pour qui s'occupe d'elle ; Sidekicks le garde pour vous.
59+
60+## Étape 5 — Faire une erreur exprès
61+
62+Cliquez sur **Settings** dans la barre d'état. Il indique « ✓ Configuration valide ». Puis, dans un second terminal, cassez le fichier :
63+
64+```bash
65+sed -i.bak 's/ command:/ comand:/' /tmp/sidekicks-tutorial.yaml
66+```
67+
68+En moins d'une seconde, un bandeau rouge apparaît au centre, et **Settings** affiche :
69+
70+```text
71+ligne 3 — agents[0] : il manque la clé « command »
72+ligne 6 — agents[0].comand : clé inconnue « comand » ; vouliez-vous dire « command » ?
73+```
74+
75+Alice est toujours listée : Sidekicks garde la dernière configuration valide.
76+
77+## Étape 6 — Corriger
78+
79+```bash
80+mv /tmp/sidekicks-tutorial.yaml.bak /tmp/sidekicks-tutorial.yaml
81+```
82+
83+Le bandeau disparaît et Settings affiche de nouveau « ✓ Configuration valide », sans redémarrer.
84+
85+## Étape 7 — Arrêter
86+
87+Passez sur Alice et cliquez sur ■ : sa pastille devient grise. Fermez la fenêtre : si Alice tournait encore, Sidekicks l'aurait arrêtée et aurait libéré son modèle.
88+
89+## Et maintenant ?
90+
91+Vous avez lancé Sidekicks, piloté la vie d'un agent et vu comment la configuration est vérifiée. Pour aller plus loin :
92+
93+- Toutes les clés du fichier → [Référence : le fichier de configuration](../reference/configuration.md)
94+- Ce que veut dire chaque état → [Référence : états des agents et événements](../reference/agents-et-evenements.md)
95+- Pourquoi les agents démarrent et s'arrêtent ainsi → [Mémoire et modèles locaux](../explanation/memoire-et-modeles-locaux.md)
added frontend/bindings/events/index.ts +25 -0
new file mode 100644
@@ -0,0 +1,25 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
5+// @ts-ignore: Unused imports
6+import type {} from "../github.com/wailsapp/wails/v3/internal/eventdata.js";
7+
8+// Make sure event data creation code is registered with the runtime.
9+import "../github.com/wailsapp/wails/v3/internal/eventcreate.js";
10+
11+/**
12+ * Name of the custom event "agent:log".
13+ */
14+export const AgentLog = "agent:log";
15+
16+/**
17+ * Name of the custom event "agent:state".
18+ */
19+export const AgentState = "agent:state";
20+
21+/**
22+ * Name of the custom event "config:changed".
23+ */
24+export const ConfigChanged = "config:changed";
25+
new file mode 100644
@@ -0,0 +1,25 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
5+// @ts-ignore: Unused imports
6+import type {} from "../github.com/wailsapp/wails/v3/internal/eventdata.js";
7+
8+// Make sure event data creation code is registered with the runtime.
9+import "../github.com/wailsapp/wails/v3/internal/eventcreate.js";
10+
11+/**
12+ * Name of the custom event "agent:log".
13+ */
14+export const AgentLog = "agent:log";
15+
16+/**
17+ * Name of the custom event "agent:state".
18+ */
19+export const AgentState = "agent:state";
20+
21+/**
22+ * Name of the custom event "config:changed".
23+ */
24+export const ConfigChanged = "config:changed";
25+
added frontend/bindings/git.rickub.com/bots-garden/sidekicks/internal/agents/index.ts +10 -0
new file mode 100644
@@ -0,0 +1,10 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+export {
5+ State
6+} from "./models.js";
7+
8+export type {
9+ Status
10+} from "./models.js";
new file mode 100644
@@ -0,0 +1,10 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+export {
5+ State
6+} from "./models.js";
7+
8+export type {
9+ Status
10+} from "./models.js";
added frontend/bindings/git.rickub.com/bots-garden/sidekicks/internal/agents/models.ts +42 -0
new file mode 100644
@@ -0,0 +1,42 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+/**
5+ * State is where an agent is in its life.
6+ */
7+export enum State {
8+ /**
9+ * The Go zero value for the underlying type of the enum.
10+ */
11+ $zero = "",
12+
13+ /**
14+ * The states, as shown in the Agents panel.
15+ */
16+ StateStopped = "stopped",
17+ StateStarting = "starting",
18+ StateReady = "ready",
19+ StateBusy = "busy",
20+ StateError = "error",
21+};
22+
23+/**
24+ * Status is the public view of an agent.
25+ */
26+export interface Status {
27+ "name": string;
28+ "title": string;
29+ "icon": string;
30+ "description": string;
31+ "state": State;
32+
33+ /**
34+ * Error explains StateError.
35+ */
36+ "error"?: string;
37+
38+ /**
39+ * Outdated: the configuration of this running agent changed; restart it to apply.
40+ */
41+ "outdated": boolean;
42+}
new file mode 100644
@@ -0,0 +1,42 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+/**
5+ * State is where an agent is in its life.
6+ */
7+export enum State {
8+ /**
9+ * The Go zero value for the underlying type of the enum.
10+ */
11+ $zero = "",
12+
13+ /**
14+ * The states, as shown in the Agents panel.
15+ */
16+ StateStopped = "stopped",
17+ StateStarting = "starting",
18+ StateReady = "ready",
19+ StateBusy = "busy",
20+ StateError = "error",
21+};
22+
23+/**
24+ * Status is the public view of an agent.
25+ */
26+export interface Status {
27+ "name": string;
28+ "title": string;
29+ "icon": string;
30+ "description": string;
31+ "state": State;
32+
33+ /**
34+ * Error explains StateError.
35+ */
36+ "error"?: string;
37+
38+ /**
39+ * Outdated: the configuration of this running agent changed; restart it to apply.
40+ */
41+ "outdated": boolean;
42+}
added frontend/bindings/git.rickub.com/bots-garden/sidekicks/internal/app/agentservice.ts +51 -0
new file mode 100644
@@ -0,0 +1,51 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+/**
5+ * AgentService lets the interface list, start, stop and restart agents and
6+ * read their logs. Every change is also announced with agent:state events.
7+ * @module
8+ */
9+
10+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
11+// @ts-ignore: Unused imports
12+import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
13+
14+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
15+// @ts-ignore: Unused imports
16+import * as agents$0 from "../agents/models.js";
17+
18+/**
19+ * List returns every agent of the configuration with its state.
20+ */
21+export function List(): $CancellablePromise<agents$0.Status[] | null> {
22+ return $Call.ByID(546084502);
23+}
24+
25+/**
26+ * Logs returns the last lines the agent wrote on its standard error.
27+ */
28+export function Logs(name: string): $CancellablePromise<string[] | null> {
29+ return $Call.ByID(4148761369, name);
30+}
31+
32+/**
33+ * Restart stops then starts the agent, e.g. after a crash or a configuration change.
34+ */
35+export function Restart(name: string): $CancellablePromise<void> {
36+ return $Call.ByID(2572971085, name);
37+}
38+
39+/**
40+ * Start starts the agent if it is not running.
41+ */
42+export function Start(name: string): $CancellablePromise<void> {
43+ return $Call.ByID(908650722, name);
44+}
45+
46+/**
47+ * Stop stops the agent, even if it is busy.
48+ */
49+export function Stop(name: string): $CancellablePromise<void> {
50+ return $Call.ByID(1581319906, name);
51+}
new file mode 100644
@@ -0,0 +1,51 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+/**
5+ * AgentService lets the interface list, start, stop and restart agents and
6+ * read their logs. Every change is also announced with agent:state events.
7+ * @module
8+ */
9+
10+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
11+// @ts-ignore: Unused imports
12+import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
13+
14+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
15+// @ts-ignore: Unused imports
16+import * as agents$0 from "../agents/models.js";
17+
18+/**
19+ * List returns every agent of the configuration with its state.
20+ */
21+export function List(): $CancellablePromise<agents$0.Status[] | null> {
22+ return $Call.ByID(546084502);
23+}
24+
25+/**
26+ * Logs returns the last lines the agent wrote on its standard error.
27+ */
28+export function Logs(name: string): $CancellablePromise<string[] | null> {
29+ return $Call.ByID(4148761369, name);
30+}
31+
32+/**
33+ * Restart stops then starts the agent, e.g. after a crash or a configuration change.
34+ */
35+export function Restart(name: string): $CancellablePromise<void> {
36+ return $Call.ByID(2572971085, name);
37+}
38+
39+/**
40+ * Start starts the agent if it is not running.
41+ */
42+export function Start(name: string): $CancellablePromise<void> {
43+ return $Call.ByID(908650722, name);
44+}
45+
46+/**
47+ * Stop stops the agent, even if it is busy.
48+ */
49+export function Stop(name: string): $CancellablePromise<void> {
50+ return $Call.ByID(1581319906, name);
51+}
added frontend/bindings/git.rickub.com/bots-garden/sidekicks/internal/app/configservice.ts +23 -0
new file mode 100644
@@ -0,0 +1,23 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+/**
5+ * ConfigService tells the interface which configuration is loaded and what is
6+ * wrong with it. A config:changed event says when to ask again.
7+ * @module
8+ */
9+
10+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
11+// @ts-ignore: Unused imports
12+import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
13+
14+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
15+// @ts-ignore: Unused imports
16+import * as $models from "./models.js";
17+
18+/**
19+ * Current returns the configuration in use.
20+ */
21+export function Current(): $CancellablePromise<$models.ConfigView> {
22+ return $Call.ByID(253092526);
23+}
new file mode 100644
@@ -0,0 +1,23 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+/**
5+ * ConfigService tells the interface which configuration is loaded and what is
6+ * wrong with it. A config:changed event says when to ask again.
7+ * @module
8+ */
9+
10+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
11+// @ts-ignore: Unused imports
12+import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
13+
14+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
15+// @ts-ignore: Unused imports
16+import * as $models from "./models.js";
17+
18+/**
19+ * Current returns the configuration in use.
20+ */
21+export function Current(): $CancellablePromise<$models.ConfigView> {
22+ return $Call.ByID(253092526);
23+}
added frontend/bindings/git.rickub.com/bots-garden/sidekicks/internal/app/index.ts +14 -0
new file mode 100644
@@ -0,0 +1,14 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+import * as AgentService from "./agentservice.js";
5+import * as ConfigService from "./configservice.js";
6+export {
7+ AgentService,
8+ ConfigService
9+};
10+
11+export type {
12+ ChannelView,
13+ ConfigView
14+} from "./models.js";
new file mode 100644
@@ -0,0 +1,14 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+import * as AgentService from "./agentservice.js";
5+import * as ConfigService from "./configservice.js";
6+export {
7+ AgentService,
8+ ConfigService
9+};
10+
11+export type {
12+ ChannelView,
13+ ConfigView
14+} from "./models.js";
added frontend/bindings/git.rickub.com/bots-garden/sidekicks/internal/app/models.ts +39 -0
new file mode 100644
@@ -0,0 +1,39 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+/**
5+ * ChannelView is a channel declared in the configuration.
6+ */
7+export interface ChannelView {
8+ "name": string;
9+ "description": string;
10+ "agents": string[] | null;
11+}
12+
13+/**
14+ * ConfigView is the configuration as the interface shows it.
15+ */
16+export interface ConfigView {
17+ "path": string;
18+
19+ /**
20+ * Source is flag, environment, last-opened or default.
21+ */
22+ "source": string;
23+
24+ /**
25+ * Created is true when Sidekicks has just created the default file.
26+ */
27+ "created": boolean;
28+
29+ /**
30+ * Valid is false when the file has problems; the last valid configuration
31+ * (or an empty one) is then in use.
32+ */
33+ "valid": boolean;
34+ "problems": string[] | null;
35+ "warnings": string[] | null;
36+ "theme": string;
37+ "language": string;
38+ "channels": ChannelView[] | null;
39+}
new file mode 100644
@@ -0,0 +1,39 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+/**
5+ * ChannelView is a channel declared in the configuration.
6+ */
7+export interface ChannelView {
8+ "name": string;
9+ "description": string;
10+ "agents": string[] | null;
11+}
12+
13+/**
14+ * ConfigView is the configuration as the interface shows it.
15+ */
16+export interface ConfigView {
17+ "path": string;
18+
19+ /**
20+ * Source is flag, environment, last-opened or default.
21+ */
22+ "source": string;
23+
24+ /**
25+ * Created is true when Sidekicks has just created the default file.
26+ */
27+ "created": boolean;
28+
29+ /**
30+ * Valid is false when the file has problems; the last valid configuration
31+ * (or an empty one) is then in use.
32+ */
33+ "valid": boolean;
34+ "problems": string[] | null;
35+ "warnings": string[] | null;
36+ "theme": string;
37+ "language": string;
38+ "channels": ChannelView[] | null;
39+}
added frontend/bindings/git.rickub.com/bots-garden/sidekicks/internal/events/index.ts +8 -0
new file mode 100644
@@ -0,0 +1,8 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+export type {
5+ AgentLogPayload,
6+ AgentStatePayload,
7+ ConfigChangedPayload
8+} from "./models.js";
new file mode 100644
@@ -0,0 +1,8 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+export type {
5+ AgentLogPayload,
6+ AgentStatePayload,
7+ ConfigChangedPayload
8+} from "./models.js";
added frontend/bindings/git.rickub.com/bots-garden/sidekicks/internal/events/models.ts +49 -0
new file mode 100644
@@ -0,0 +1,49 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+/**
5+ * AgentLogPayload is one line written by an agent on its standard error.
6+ */
7+export interface AgentLogPayload {
8+ "agent": string;
9+ "line": string;
10+}
11+
12+/**
13+ * AgentStatePayload is one state transition of an agent.
14+ */
15+export interface AgentStatePayload {
16+ "agent": string;
17+
18+ /**
19+ * State is one of stopped, starting, ready, busy, error.
20+ */
21+ "state": string;
22+
23+ /**
24+ * Error explains the error state; empty otherwise.
25+ */
26+ "error"?: string;
27+}
28+
29+/**
30+ * ConfigChangedPayload tells the interface which configuration is loaded and
31+ * what is wrong with it, if anything.
32+ */
33+export interface ConfigChangedPayload {
34+ /**
35+ * Path is the absolute path of the configuration file.
36+ */
37+ "path": string;
38+
39+ /**
40+ * Valid is false when the file could not be loaded; the previous
41+ * configuration then stays in use.
42+ */
43+ "valid": boolean;
44+
45+ /**
46+ * Problems are human-readable messages, one per problem, in the interface language.
47+ */
48+ "problems": string[] | null;
49+}
new file mode 100644
@@ -0,0 +1,49 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+/**
5+ * AgentLogPayload is one line written by an agent on its standard error.
6+ */
7+export interface AgentLogPayload {
8+ "agent": string;
9+ "line": string;
10+}
11+
12+/**
13+ * AgentStatePayload is one state transition of an agent.
14+ */
15+export interface AgentStatePayload {
16+ "agent": string;
17+
18+ /**
19+ * State is one of stopped, starting, ready, busy, error.
20+ */
21+ "state": string;
22+
23+ /**
24+ * Error explains the error state; empty otherwise.
25+ */
26+ "error"?: string;
27+}
28+
29+/**
30+ * ConfigChangedPayload tells the interface which configuration is loaded and
31+ * what is wrong with it, if anything.
32+ */
33+export interface ConfigChangedPayload {
34+ /**
35+ * Path is the absolute path of the configuration file.
36+ */
37+ "path": string;
38+
39+ /**
40+ * Valid is false when the file could not be loaded; the previous
41+ * configuration then stays in use.
42+ */
43+ "valid": boolean;
44+
45+ /**
46+ * Problems are human-readable messages, one per problem, in the interface language.
47+ */
48+ "problems": string[] | null;
49+}
added frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts +9 -0
new file mode 100644
@@ -0,0 +1,9 @@
1+//@ts-check
2+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
3+// This file is automatically generated. DO NOT EDIT
4+
5+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
6+// @ts-ignore: Unused imports
7+import { Create as $Create } from "@wailsio/runtime";
8+
9+Object.freeze($Create.Events);
new file mode 100644
@@ -0,0 +1,9 @@
1+//@ts-check
2+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
3+// This file is automatically generated. DO NOT EDIT
4+
5+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
6+// @ts-ignore: Unused imports
7+import { Create as $Create } from "@wailsio/runtime";
8+
9+Object.freeze($Create.Events);
added frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts +20 -0
new file mode 100644
@@ -0,0 +1,20 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
5+// @ts-ignore: Unused imports
6+import type { Events } from "@wailsio/runtime";
7+
8+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
9+// @ts-ignore: Unused imports
10+import type * as events$0 from "../../../../../git.rickub.com/bots-garden/sidekicks/internal/events/models.js";
11+
12+declare module "@wailsio/runtime" {
13+ namespace Events {
14+ interface CustomEvents {
15+ "agent:log": events$0.AgentLogPayload;
16+ "agent:state": events$0.AgentStatePayload;
17+ "config:changed": events$0.ConfigChangedPayload;
18+ }
19+ }
20+}
new file mode 100644
@@ -0,0 +1,20 @@
1+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2+// This file is automatically generated. DO NOT EDIT
3+
4+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
5+// @ts-ignore: Unused imports
6+import type { Events } from "@wailsio/runtime";
7+
8+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
9+// @ts-ignore: Unused imports
10+import type * as events$0 from "../../../../../git.rickub.com/bots-garden/sidekicks/internal/events/models.js";
11+
12+declare module "@wailsio/runtime" {
13+ namespace Events {
14+ interface CustomEvents {
15+ "agent:log": events$0.AgentLogPayload;
16+ "agent:state": events$0.AgentStatePayload;
17+ "config:changed": events$0.ConfigChangedPayload;
18+ }
19+ }
20+}
added frontend/index.html +13 -0
new file mode 100644
@@ -0,0 +1,13 @@
1+<!DOCTYPE html>
2+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
3+<html lang="fr">
4+ <head>
5+ <meta charset="UTF-8" />
6+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+ <title>Sidekicks</title>
8+ </head>
9+ <body>
10+ <div id="app"></div>
11+ <script type="module" src="/src/main.ts"></script>
12+ </body>
13+</html>
new file mode 100644
@@ -0,0 +1,13 @@
1+<!DOCTYPE html>
2+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
3+<html lang="fr">
4+ <head>
5+ <meta charset="UTF-8" />
6+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+ <title>Sidekicks</title>
8+ </head>
9+ <body>
10+ <div id="app"></div>
11+ <script type="module" src="/src/main.ts"></script>
12+ </body>
13+</html>
added frontend/package.json +29 -0
new file mode 100644
@@ -0,0 +1,29 @@
1+{
2+ "name": "sidekicks-frontend",
3+ "private": true,
4+ "version": "0.0.1",
5+ "type": "module",
6+ "scripts": {
7+ "dev": "vite",
8+ "build:dev": "vite build --minify false --mode development",
9+ "build": "vite build --mode production",
10+ "preview": "vite preview",
11+ "check": "svelte-check --tsconfig ./tsconfig.json",
12+ "test": "vitest run"
13+ },
14+ "dependencies": {
15+ "@wailsio/runtime": "3.0.0",
16+ "paneforge": "1.0.2"
17+ },
18+ "devDependencies": {
19+ "@sveltejs/vite-plugin-svelte": "^7.0.0",
20+ "@tsconfig/svelte": "^5.0.8",
21+ "svelte": "^5.46.4",
22+ "svelte-check": "^4.4.8",
23+ "tslib": "^2.6.2",
24+ "typescript": "^5.2.2",
25+ "vite": "^8.2.4",
26+ "vitest": "5.0.4"
27+ },
28+ "packageManager": "pnpm@12.11.2"
29+}
new file mode 100644
@@ -0,0 +1,29 @@
1+{
2+ "name": "sidekicks-frontend",
3+ "private": true,
4+ "version": "0.0.1",
5+ "type": "module",
6+ "scripts": {
7+ "dev": "vite",
8+ "build:dev": "vite build --minify false --mode development",
9+ "build": "vite build --mode production",
10+ "preview": "vite preview",
11+ "check": "svelte-check --tsconfig ./tsconfig.json",
12+ "test": "vitest run"
13+ },
14+ "dependencies": {
15+ "@wailsio/runtime": "3.0.0",
16+ "paneforge": "1.0.2"
17+ },
18+ "devDependencies": {
19+ "@sveltejs/vite-plugin-svelte": "^7.0.0",
20+ "@tsconfig/svelte": "^5.0.8",
21+ "svelte": "^5.46.4",
22+ "svelte-check": "^4.4.8",
23+ "tslib": "^2.6.2",
24+ "typescript": "^5.2.2",
25+ "vite": "^8.2.4",
26+ "vitest": "5.0.4"
27+ },
28+ "packageManager": "pnpm@12.11.2"
29+}
added frontend/pnpm-lock.yaml +1108 -0
new file mode 100644
@@ -0,0 +1,1108 @@
1+---
2+lockfileVersion: '9.0'
3+
4+importers:
5+
6+ .:
7+ configDependencies: {}
8+ packageManagerDependencies:
9+ pnpm:
10+ specifier: 12.11.2
11+ version: 12.11.2
12+
13+packages:
14+
15+ '@pnpm/exe.android-arm64@12.11.2':
16+ resolution: {integrity: sha512-owLw13OfPkEoY+hLBNeeG+dCFhywr7q2d/EHPz4mNL2RhKnv8gNwMARkjkiDQu234aItVwFNDeGnmQXm+ikkAg==}
17+ cpu: [arm64]
18+ os: [android]
19+
20+ '@pnpm/exe.android-x64@12.11.2':
21+ resolution: {integrity: sha512-GYPkVebl4iokOA/4MPSpGoUVfFlsth7uqOCe1XiaiM23LAdvUOMJBObqHJijr89ZqjwHm9pO6vO45GRwJ2U2qA==}
22+ cpu: [x64]
23+ os: [android]
24+
25+ '@pnpm/exe.darwin-arm64@12.11.2':
26+ resolution: {integrity: sha512-n9M4On66y8FDxUAKgMO1FXt0qihyG4e+56p3/F0uo8Bx9KplW8BwKVnXigzCJ8aNM6sVVPkOE6M9XXbeVIxnZA==}
27+ cpu: [arm64]
28+ os: [darwin]
29+
30+ '@pnpm/exe.darwin-x64@12.11.2':
31+ resolution: {integrity: sha512-oSW7n3sBT0c0cyk4kj10T2Y6Mpm+OFEiCAWozgX039tdoI6yqJF1uaFD3sw7ugLdu6VV3+uY0pa1TUZIPdxDWA==}
32+ cpu: [x64]
33+ os: [darwin]
34+
35+ '@pnpm/exe.freebsd-x64@12.11.2':
36+ resolution: {integrity: sha512-qbzc3KoDpa/Lkf8vs3jnDe6dUUbso+6eLIyssYFMiliLpYNxn8dirk11YoNnOQcdHVB50v5RFls1Yid6ttT5pg==}
37+ cpu: [x64]
38+ os: [freebsd]
39+
40+ '@pnpm/exe.linux-arm64-musl@12.11.2':
41+ resolution: {integrity: sha512-uMEYDwKRktGwANubQ+kYCwmhNwg3Ik6qK89ahsbQ5cP30R/5FVJb/T99vr9XRUBC/A7mZI6eeT92oyVas9ACyg==}
42+ cpu: [arm64]
43+ os: [linux]
44+ libc: [musl]
45+
46+ '@pnpm/exe.linux-arm64@12.11.2':
47+ resolution: {integrity: sha512-6e8ZAQUwFXAWHHHv5FesWqf/kEK04wuitBv5f/gxzkC0DiBfaixr+f+6JQede8SF4Ji5LcMwVFJbzAtffpWxEg==}
48+ cpu: [arm64]
49+ os: [linux]
50+ libc: [glibc]
51+
52+ '@pnpm/exe.linux-ppc64@12.11.2':
53+ resolution: {integrity: sha512-Ui8A4nepkVupx2VpuFfysEluMvWK6gTB5rgVts7Ss/+8dj0RD28x3F/seNeH/zSypgC6yKdV6qsKVQh/uffTuQ==}
54+ cpu: [ppc64]
55+ os: [linux]
56+ libc: [glibc]
57+
58+ '@pnpm/exe.linux-riscv64@12.11.2':
59+ resolution: {integrity: sha512-i1yLdQ5bu+wjQ+srtFoJAaY2gN/+oXHFdjul4llCeoDxHE0rTIG3R41NhHfCjyFPW4+CpbBT58933CL9sMOCnA==}
60+ cpu: [riscv64]
61+ os: [linux]
62+ libc: [glibc]
63+
64+ '@pnpm/exe.linux-s390x@12.11.2':
65+ resolution: {integrity: sha512-YS1/lhPjjTsN4QL50VaFZKRa3Uc/bDnhE8eKbJMFvuX06UuFS5YaiDg04APoZdfVSGqTfX27QAg7rfLMSWaqxA==}
66+ cpu: [s390x]
67+ os: [linux]
68+ libc: [glibc]
69+
70+ '@pnpm/exe.linux-x64-musl@12.11.2':
71+ resolution: {integrity: sha512-7eIxQR05fvJxCujUSKp5KTtehJ7QqrgqP/HD1+BcB/SaV2Be4XI1PEHiOgA1zxsiMIeMvTF6E4G4dYHrFjeXRg==}
72+ cpu: [x64]
73+ os: [linux]
74+ libc: [musl]
75+
76+ '@pnpm/exe.linux-x64@12.11.2':
77+ resolution: {integrity: sha512-FK7QKe24AYgppJwO3mpooYptduuvIGqKJPi5ac7MdnWkjUy46JBfYhCW0grnkWKC8YOf0x+vGviuQqJEj3SpQQ==}
78+ cpu: [x64]
79+ os: [linux]
80+ libc: [glibc]
81+
82+ '@pnpm/exe.win32-arm64@12.11.2':
83+ resolution: {integrity: sha512-PCTjVDdJb6VvraZxrEeFv6EqEA0hhIsAySx28e95J2Tqpi88hLpgOXgNqV0vpxq9A+b8GLQfyk/D+1UlJPv/YQ==}
84+ cpu: [arm64]
85+ os: [win32]
86+
87+ '@pnpm/exe.win32-x64@12.11.2':
88+ resolution: {integrity: sha512-8sgX86OvRjJ3yZs/TvGR2L31wH9ScPZ+f+geKI3ikuB0RRiK3i4LlTRO8OXN6pdRqj8epMde4312MCjj7JsJdA==}
89+ cpu: [x64]
90+ os: [win32]
91+
92+ pnpm@12.11.2:
93+ resolution: {integrity: sha512-JaUWUqGWeFr18ovabf6NxYLr76SsHRgCIc4TkCMcX7dmN4IupLbkYzznSs3LCnf3Ja8RutOBd/3t2u6BipVcuQ==}
94+ engines: {node: '>=18.*'}
95+ hasBin: true
96+
97+snapshots:
98+
99+ '@pnpm/exe.android-arm64@12.11.2':
100+ optional: true
101+
102+ '@pnpm/exe.android-x64@12.11.2':
103+ optional: true
104+
105+ '@pnpm/exe.darwin-arm64@12.11.2':
106+ optional: true
107+
108+ '@pnpm/exe.darwin-x64@12.11.2':
109+ optional: true
110+
111+ '@pnpm/exe.freebsd-x64@12.11.2':
112+ optional: true
113+
114+ '@pnpm/exe.linux-arm64-musl@12.11.2':
115+ optional: true
116+
117+ '@pnpm/exe.linux-arm64@12.11.2':
118+ optional: true
119+
120+ '@pnpm/exe.linux-ppc64@12.11.2':
121+ optional: true
122+
123+ '@pnpm/exe.linux-riscv64@12.11.2':
124+ optional: true
125+
126+ '@pnpm/exe.linux-s390x@12.11.2':
127+ optional: true
128+
129+ '@pnpm/exe.linux-x64-musl@12.11.2':
130+ optional: true
131+
132+ '@pnpm/exe.linux-x64@12.11.2':
133+ optional: true
134+
135+ '@pnpm/exe.win32-arm64@12.11.2':
136+ optional: true
137+
138+ '@pnpm/exe.win32-x64@12.11.2':
139+ optional: true
140+
141+ pnpm@12.11.2:
142+ optionalDependencies:
143+ '@pnpm/exe.android-arm64': 12.11.2
144+ '@pnpm/exe.android-x64': 12.11.2
145+ '@pnpm/exe.darwin-arm64': 12.11.2
146+ '@pnpm/exe.darwin-x64': 12.11.2
147+ '@pnpm/exe.freebsd-x64': 12.11.2
148+ '@pnpm/exe.linux-arm64': 12.11.2
149+ '@pnpm/exe.linux-arm64-musl': 12.11.2
150+ '@pnpm/exe.linux-ppc64': 12.11.2
151+ '@pnpm/exe.linux-riscv64': 12.11.2
152+ '@pnpm/exe.linux-s390x': 12.11.2
153+ '@pnpm/exe.linux-x64': 12.11.2
154+ '@pnpm/exe.linux-x64-musl': 12.11.2
155+ '@pnpm/exe.win32-arm64': 12.11.2
156+ '@pnpm/exe.win32-x64': 12.11.2
157+
158+---
159+lockfileVersion: '9.0'
160+
161+settings:
162+ autoInstallPeers: true
163+ excludeLinksFromLockfile: false
164+
165+importers:
166+
167+ .:
168+ dependencies:
169+ '@wailsio/runtime':
170+ specifier: 3.0.0
171+ version: 3.0.0
172+ paneforge:
173+ specifier: 1.0.2
174+ version: 1.0.2(svelte@5.57.3)
175+ devDependencies:
176+ '@sveltejs/vite-plugin-svelte':
177+ specifier: ^7.0.0
178+ version: 7.3.1(svelte@5.57.3)(vite@8.3.5)
179+ '@tsconfig/svelte':
180+ specifier: ^5.0.8
181+ version: 5.0.8
182+ svelte:
183+ specifier: ^5.46.4
184+ version: 5.57.3
185+ svelte-check:
186+ specifier: ^4.4.8
187+ version: 4.7.7(picomatch@4.0.7)(svelte@5.57.3)(typescript@5.9.3)
188+ tslib:
189+ specifier: ^2.6.2
190+ version: 2.8.1
191+ typescript:
192+ specifier: ^5.2.2
193+ version: 5.9.3
194+ vite:
195+ specifier: ^8.2.4
196+ version: 8.3.5
197+ vitest:
198+ specifier: 5.0.4
199+ version: 5.0.4(vite@8.3.5)
200+
201+packages:
202+
203+ '@jridgewell/gen-mapping@0.3.13':
204+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
205+
206+ '@jridgewell/remapping@2.3.5':
207+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
208+
209+ '@jridgewell/resolve-uri@3.1.2':
210+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
211+ engines: {node: '>=6.0.0'}
212+
213+ '@jridgewell/sourcemap-codec@1.6.0':
214+ resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
215+
216+ '@jridgewell/trace-mapping@0.3.31':
217+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
218+
219+ '@oxc-project/types@0.155.0':
220+ resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==}
221+
222+ '@rolldown/binding-android-arm-eabi@1.2.15':
223+ resolution: {integrity: sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==}
224+ engines: {node: ^20.19.0 || >=22.12.0}
225+ cpu: [arm]
226+ os: [android]
227+
228+ '@rolldown/binding-android-arm64@1.2.15':
229+ resolution: {integrity: sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==}
230+ engines: {node: ^20.19.0 || >=22.12.0}
231+ cpu: [arm64]
232+ os: [android]
233+
234+ '@rolldown/binding-darwin-arm64@1.2.15':
235+ resolution: {integrity: sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==}
236+ engines: {node: ^20.19.0 || >=22.12.0}
237+ cpu: [arm64]
238+ os: [darwin]
239+
240+ '@rolldown/binding-darwin-x64@1.2.15':
241+ resolution: {integrity: sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==}
242+ engines: {node: ^20.19.0 || >=22.12.0}
243+ cpu: [x64]
244+ os: [darwin]
245+
246+ '@rolldown/binding-freebsd-x64@1.2.15':
247+ resolution: {integrity: sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==}
248+ engines: {node: ^20.19.0 || >=22.12.0}
249+ cpu: [x64]
250+ os: [freebsd]
251+
252+ '@rolldown/binding-linux-arm-gnueabihf@1.2.15':
253+ resolution: {integrity: sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==}
254+ engines: {node: ^20.19.0 || >=22.12.0}
255+ cpu: [arm]
256+ os: [linux]
257+
258+ '@rolldown/binding-linux-arm64-gnu@1.2.15':
259+ resolution: {integrity: sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==}
260+ engines: {node: ^20.19.0 || >=22.12.0}
261+ cpu: [arm64]
262+ os: [linux]
263+ libc: [glibc]
264+
265+ '@rolldown/binding-linux-arm64-musl@1.2.15':
266+ resolution: {integrity: sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==}
267+ engines: {node: ^20.19.0 || >=22.12.0}
268+ cpu: [arm64]
269+ os: [linux]
270+ libc: [musl]
271+
272+ '@rolldown/binding-linux-ppc64-gnu@1.2.15':
273+ resolution: {integrity: sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==}
274+ engines: {node: ^20.19.0 || >=22.12.0}
275+ cpu: [ppc64]
276+ os: [linux]
277+ libc: [glibc]
278+
279+ '@rolldown/binding-linux-s390x-gnu@1.2.15':
280+ resolution: {integrity: sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==}
281+ engines: {node: ^20.19.0 || >=22.12.0}
282+ cpu: [s390x]
283+ os: [linux]
284+ libc: [glibc]
285+
286+ '@rolldown/binding-linux-x64-gnu@1.2.15':
287+ resolution: {integrity: sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==}
288+ engines: {node: ^20.19.0 || >=22.12.0}
289+ cpu: [x64]
290+ os: [linux]
291+ libc: [glibc]
292+
293+ '@rolldown/binding-linux-x64-musl@1.2.15':
294+ resolution: {integrity: sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==}
295+ engines: {node: ^20.19.0 || >=22.12.0}
296+ cpu: [x64]
297+ os: [linux]
298+ libc: [musl]
299+
300+ '@rolldown/binding-openharmony-arm64@1.2.15':
301+ resolution: {integrity: sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==}
302+ engines: {node: ^20.19.0 || >=22.12.0}
303+ cpu: [arm64]
304+ os: [openharmony]
305+
306+ '@rolldown/binding-win32-arm64-msvc@1.2.15':
307+ resolution: {integrity: sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==}
308+ engines: {node: ^20.19.0 || >=22.12.0}
309+ cpu: [arm64]
310+ os: [win32]
311+
312+ '@rolldown/binding-win32-x64-msvc@1.2.15':
313+ resolution: {integrity: sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==}
314+ engines: {node: ^20.19.0 || >=22.12.0}
315+ cpu: [x64]
316+ os: [win32]
317+
318+ '@rolldown/pluginutils@1.0.1':
319+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
320+
321+ '@sveltejs/acorn-typescript@1.0.13':
322+ resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==}
323+ peerDependencies:
324+ acorn: ^8.9.0
325+
326+ '@sveltejs/load-config@0.2.3':
327+ resolution: {integrity: sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==}
328+ engines: {node: '>= 18.0.0'}
329+
330+ '@sveltejs/vite-plugin-svelte@7.3.1':
331+ resolution: {integrity: sha512-ZPsLN8B1e/En+Ak5s4V7srFDT532oS0qieLsQwu63NGKsS+iAjoO2Js1BochlHlglcU+Pt7WAO3C5Ee+4f6gVA==}
332+ engines: {node: ^20.19 || ^22.12 || >=24}
333+ peerDependencies:
334+ svelte: ^5.46.4
335+ vite: ^8.0.0-beta.7 || ^8.0.0
336+
337+ '@tsconfig/svelte@5.0.8':
338+ resolution: {integrity: sha512-UkNnw1/oFEfecR8ypyHIQuWYdkPvHiwcQ78sh+ymIiYoF+uc5H1UBetbjyqT+vgGJ3qQN6nhucJviX6HesWtKQ==}
339+
340+ '@types/chai@5.2.3':
341+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
342+
343+ '@types/deep-eql@4.0.2':
344+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
345+
346+ '@types/estree@1.0.9':
347+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
348+
349+ '@vitest/mocker@5.0.4':
350+ resolution: {integrity: sha512-tqAlrYWce4iUxhr3ncNohejtCTOcHeE8XWrSNZY3OhPoDn01yeGv+gnaBy64ATp7o6pb03eIbBAjzmibpc9vIQ==}
351+ peerDependencies:
352+ msw: ^2.4.9 || ^3.0.0
353+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
354+ peerDependenciesMeta:
355+ msw:
356+ optional: true
357+ vite:
358+ optional: true
359+
360+ '@vitest/spy@5.0.4':
361+ resolution: {integrity: sha512-MZKk/gSkm2q+ICobMxCpH1mA+cCAWm8WV7UhxlqGjq/sJ3AGc1BqFOs5CRVXxH78wQtme9q/NAAgBquj4AcyMw==}
362+
363+ '@wailsio/runtime@3.0.0':
364+ resolution: {integrity: sha512-NkFJXYWD/4Y3TcHAqfIeST4oybtwv9dVWTmriZTMFbINYiQ2FB3Wr4MjsQ7o0BNynEj2GslmUisNejnixEmLVQ==}
365+
366+ acorn@8.19.0:
367+ resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==}
368+ engines: {node: '>=0.4.0'}
369+ hasBin: true
370+
371+ aria-query@5.3.1:
372+ resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==}
373+ engines: {node: '>= 0.4'}
374+
375+ assertion-error@2.0.1:
376+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
377+ engines: {node: '>=12'}
378+
379+ axobject-query@4.1.0:
380+ resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
381+ engines: {node: '>= 0.4'}
382+
383+ chai@6.3.0:
384+ resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==}
385+ engines: {node: '>=18'}
386+
387+ chokidar@4.0.3:
388+ resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
389+ engines: {node: '>= 14.16.0'}
390+
391+ clsx@2.1.1:
392+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
393+ engines: {node: '>=6'}
394+
395+ deepmerge@4.3.1:
396+ resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
397+ engines: {node: '>=0.10.0'}
398+
399+ detect-libc@2.1.2:
400+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
401+ engines: {node: '>=8'}
402+
403+ devalue@5.9.4:
404+ resolution: {integrity: sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==}
405+
406+ es-module-lexer@2.3.2:
407+ resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
408+
409+ esm-env@1.2.2:
410+ resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
411+
412+ esrap@2.4.0:
413+ resolution: {integrity: sha512-CQPcneEHw/czj32uwQC52nYnax8H4SXt25FCM+LIwGZ49cMIxpOj3bP8wmzcI+Z0HIy5KkClaPhRLfCySzwHPA==}
414+ peerDependencies:
415+ '@typescript-eslint/types': ^8.2.0
416+ peerDependenciesMeta:
417+ '@typescript-eslint/types':
418+ optional: true
419+
420+ estree-walker@3.0.3:
421+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
422+
423+ expect-type@1.4.0:
424+ resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
425+ engines: {node: '>=12.0.0'}
426+
427+ fdir@6.5.0:
428+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
429+ engines: {node: '>=12.0.0'}
430+ peerDependencies:
431+ picomatch: ^3 || ^4
432+ peerDependenciesMeta:
433+ picomatch:
434+ optional: true
435+
436+ fsevents@2.3.3:
437+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
438+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
439+ os: [darwin]
440+
441+ inline-style-parser@0.2.7:
442+ resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
443+
444+ is-reference@3.0.3:
445+ resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
446+
447+ lightningcss-android-arm64@1.33.0:
448+ resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
449+ engines: {node: '>= 12.0.0'}
450+ cpu: [arm64]
451+ os: [android]
452+
453+ lightningcss-darwin-arm64@1.33.0:
454+ resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==}
455+ engines: {node: '>= 12.0.0'}
456+ cpu: [arm64]
457+ os: [darwin]
458+
459+ lightningcss-darwin-x64@1.33.0:
460+ resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==}
461+ engines: {node: '>= 12.0.0'}
462+ cpu: [x64]
463+ os: [darwin]
464+
465+ lightningcss-freebsd-x64@1.33.0:
466+ resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==}
467+ engines: {node: '>= 12.0.0'}
468+ cpu: [x64]
469+ os: [freebsd]
470+
471+ lightningcss-linux-arm-gnueabihf@1.33.0:
472+ resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==}
473+ engines: {node: '>= 12.0.0'}
474+ cpu: [arm]
475+ os: [linux]
476+
477+ lightningcss-linux-arm64-gnu@1.33.0:
478+ resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==}
479+ engines: {node: '>= 12.0.0'}
480+ cpu: [arm64]
481+ os: [linux]
482+ libc: [glibc]
483+
484+ lightningcss-linux-arm64-musl@1.33.0:
485+ resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==}
486+ engines: {node: '>= 12.0.0'}
487+ cpu: [arm64]
488+ os: [linux]
489+ libc: [musl]
490+
491+ lightningcss-linux-x64-gnu@1.33.0:
492+ resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==}
493+ engines: {node: '>= 12.0.0'}
494+ cpu: [x64]
495+ os: [linux]
496+ libc: [glibc]
497+
498+ lightningcss-linux-x64-musl@1.33.0:
499+ resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==}
500+ engines: {node: '>= 12.0.0'}
501+ cpu: [x64]
502+ os: [linux]
503+ libc: [musl]
504+
505+ lightningcss-win32-arm64-msvc@1.33.0:
506+ resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==}
507+ engines: {node: '>= 12.0.0'}
508+ cpu: [arm64]
509+ os: [win32]
510+
511+ lightningcss-win32-x64-msvc@1.33.0:
512+ resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==}
513+ engines: {node: '>= 12.0.0'}
514+ cpu: [x64]
515+ os: [win32]
516+
517+ lightningcss@1.33.0:
518+ resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
519+ engines: {node: '>= 12.0.0'}
520+
521+ locate-character@3.0.0:
522+ resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
523+
524+ magic-string@0.30.21:
525+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
526+
527+ magic-string@1.4.5:
528+ resolution: {integrity: sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==}
529+
530+ mri@1.2.0:
531+ resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
532+ engines: {node: '>=4'}
533+
534+ nanoid@3.3.20:
535+ resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
536+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
537+ hasBin: true
538+
539+ obug@2.2.1:
540+ resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==}
541+ engines: {node: '>=12.20.0'}
542+
543+ obug@3.0.0:
544+ resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==}
545+ engines: {node: '>=12.20.0'}
546+
547+ paneforge@1.0.2:
548+ resolution: {integrity: sha512-KzmIXQH1wCfwZ4RsMohD/IUtEjVhteR+c+ulb/CHYJHX8SuDXoJmChtsc/Xs5Wl8NHS4L5Q7cxL8MG40gSU1bA==}
549+ peerDependencies:
550+ svelte: ^5.29.0
551+
552+ picocolors@1.1.1:
553+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
554+
555+ picomatch@4.0.7:
556+ resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
557+ engines: {node: '>=12'}
558+
559+ postcss@8.5.32:
560+ resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==}
561+ engines: {node: ^10 || ^12 || >=14}
562+
563+ readdirp@4.1.2:
564+ resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
565+ engines: {node: '>= 14.18.0'}
566+
567+ rolldown@1.2.15:
568+ resolution: {integrity: sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==}
569+ engines: {node: ^20.19.0 || >=22.12.0}
570+ hasBin: true
571+
572+ runed@0.23.4:
573+ resolution: {integrity: sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==}
574+ peerDependencies:
575+ svelte: ^5.7.0
576+
577+ runed@0.29.2:
578+ resolution: {integrity: sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==}
579+ peerDependencies:
580+ svelte: ^5.7.0
581+
582+ sade@1.8.1:
583+ resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
584+ engines: {node: '>=6'}
585+
586+ source-map-js@1.2.2:
587+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
588+ engines: {node: '>=0.10.0'}
589+
590+ std-env@4.3.0:
591+ resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==}
592+
593+ style-to-object@1.0.14:
594+ resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
595+
596+ svelte-check@4.7.7:
597+ resolution: {integrity: sha512-Ge2IMdeTpbJuzNFGRiiMg0RWgbScysKQu7xwcyFJ22+DU9F5nPX51+Q3gHmGqRxYQMx2knreVWt5oCtfe0vf2g==}
598+ engines: {node: '>= 18.0.0'}
599+ hasBin: true
600+ peerDependencies:
601+ svelte: ^4.0.0 || ^5.0.0-next.0
602+ typescript: ^5.0.0 || ^6.0.0
603+
604+ svelte-toolbelt@0.9.3:
605+ resolution: {integrity: sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw==}
606+ engines: {node: '>=18', pnpm: '>=8.7.0'}
607+ peerDependencies:
608+ svelte: ^5.30.2
609+
610+ svelte@5.57.3:
611+ resolution: {integrity: sha512-5mAftM3SoDV9X0zdF386F9p2pv8iKOwi/KfBS1XnBAkVCKFzVOLHUoa8iGWAt4buoicLmyWVRpuuD5wplXOQsg==}
612+ engines: {node: '>=18'}
613+
614+ tinybench@6.2.2:
615+ resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==}
616+ engines: {node: '>=20.0.0'}
617+
618+ tinyexec@1.3.1:
619+ resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==}
620+ engines: {node: '>=18'}
621+
622+ tinyglobby@0.2.17:
623+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
624+ engines: {node: '>=12.0.0'}
625+
626+ tslib@2.8.1:
627+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
628+
629+ typescript@5.9.3:
630+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
631+ engines: {node: '>=14.17'}
632+ hasBin: true
633+
634+ vite@8.3.5:
635+ resolution: {integrity: sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==}
636+ engines: {node: ^20.19.0 || >=22.12.0}
637+ hasBin: true
638+ peerDependencies:
639+ '@types/node': ^20.19.0 || >=22.12.0
640+ '@vitejs/devtools': ^0.7.1 || ^0.8.0
641+ esbuild: ^0.27.0 || ^0.28.0
642+ jiti: '>=1.21.0'
643+ less: ^4.0.0
644+ sass: ^1.70.0
645+ sass-embedded: ^1.70.0
646+ stylus: '>=0.54.8'
647+ sugarss: ^5.0.0
648+ terser: ^5.16.0
649+ tsx: ^4.8.1
650+ yaml: ^2.4.2
651+ peerDependenciesMeta:
652+ '@types/node':
653+ optional: true
654+ '@vitejs/devtools':
655+ optional: true
656+ esbuild:
657+ optional: true
658+ jiti:
659+ optional: true
660+ less:
661+ optional: true
662+ sass:
663+ optional: true
664+ sass-embedded:
665+ optional: true
666+ stylus:
667+ optional: true
668+ sugarss:
669+ optional: true
670+ terser:
671+ optional: true
672+ tsx:
673+ optional: true
674+ yaml:
675+ optional: true
676+
677+ vitefu@1.1.3:
678+ resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==}
679+ peerDependencies:
680+ vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
681+ peerDependenciesMeta:
682+ vite:
683+ optional: true
684+
685+ vitest@5.0.4:
686+ resolution: {integrity: sha512-z0WxZRV2Sg48wF0FSgMq3TFG8JS38/Qr96vZVZdX6+Em9raQTvnXQvdTFJe2m3rfyJnEHeHQ/snceIdi8D1pVg==}
687+ engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0}
688+ hasBin: true
689+ peerDependencies:
690+ '@edge-runtime/vm': '*'
691+ '@opentelemetry/api': ^1.9.0
692+ '@types/node': ^22.0.0 || >=24.0.0
693+ '@vitest/browser-playwright': 5.0.4
694+ '@vitest/browser-preview': 5.0.4
695+ '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0
696+ '@vitest/coverage-istanbul': 5.0.4
697+ '@vitest/coverage-v8': 5.0.4
698+ '@vitest/ui': 5.0.4
699+ happy-dom: '*'
700+ jsdom: '*'
701+ vite: ^6.4.0 || ^7.0.0 || ^8.0.0
702+ peerDependenciesMeta:
703+ '@edge-runtime/vm':
704+ optional: true
705+ '@opentelemetry/api':
706+ optional: true
707+ '@types/node':
708+ optional: true
709+ '@vitest/browser-playwright':
710+ optional: true
711+ '@vitest/browser-preview':
712+ optional: true
713+ '@vitest/browser-webdriverio':
714+ optional: true
715+ '@vitest/coverage-istanbul':
716+ optional: true
717+ '@vitest/coverage-v8':
718+ optional: true
719+ '@vitest/ui':
720+ optional: true
721+ happy-dom:
722+ optional: true
723+ jsdom:
724+ optional: true
725+
726+ why-is-node-running@3.2.1:
727+ resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==}
728+ engines: {node: '>=20.11'}
729+ hasBin: true
730+
731+ zimmerframe@1.1.5:
732+ resolution: {integrity: sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==}
733+
734+snapshots:
735+
736+ '@jridgewell/gen-mapping@0.3.13':
737+ dependencies:
738+ '@jridgewell/sourcemap-codec': 1.6.0
739+ '@jridgewell/trace-mapping': 0.3.31
740+
741+ '@jridgewell/remapping@2.3.5':
742+ dependencies:
743+ '@jridgewell/gen-mapping': 0.3.13
744+ '@jridgewell/trace-mapping': 0.3.31
745+
746+ '@jridgewell/resolve-uri@3.1.2': {}
747+
748+ '@jridgewell/sourcemap-codec@1.6.0': {}
749+
750+ '@jridgewell/trace-mapping@0.3.31':
751+ dependencies:
752+ '@jridgewell/resolve-uri': 3.1.2
753+ '@jridgewell/sourcemap-codec': 1.6.0
754+
755+ '@oxc-project/types@0.155.0': {}
756+
757+ '@rolldown/binding-android-arm-eabi@1.2.15':
758+ optional: true
759+
760+ '@rolldown/binding-android-arm64@1.2.15':
761+ optional: true
762+
763+ '@rolldown/binding-darwin-arm64@1.2.15':
764+ optional: true
765+
766+ '@rolldown/binding-darwin-x64@1.2.15':
767+ optional: true
768+
769+ '@rolldown/binding-freebsd-x64@1.2.15':
770+ optional: true
771+
772+ '@rolldown/binding-linux-arm-gnueabihf@1.2.15':
773+ optional: true
774+
775+ '@rolldown/binding-linux-arm64-gnu@1.2.15':
776+ optional: true
777+
778+ '@rolldown/binding-linux-arm64-musl@1.2.15':
779+ optional: true
780+
781+ '@rolldown/binding-linux-ppc64-gnu@1.2.15':
782+ optional: true
783+
784+ '@rolldown/binding-linux-s390x-gnu@1.2.15':
785+ optional: true
786+
787+ '@rolldown/binding-linux-x64-gnu@1.2.15':
788+ optional: true
789+
790+ '@rolldown/binding-linux-x64-musl@1.2.15':
791+ optional: true
792+
793+ '@rolldown/binding-openharmony-arm64@1.2.15':
794+ optional: true
795+
796+ '@rolldown/binding-win32-arm64-msvc@1.2.15':
797+ optional: true
798+
799+ '@rolldown/binding-win32-x64-msvc@1.2.15':
800+ optional: true
801+
802+ '@rolldown/pluginutils@1.0.1': {}
803+
804+ '@sveltejs/acorn-typescript@1.0.13(acorn@8.19.0)':
805+ dependencies:
806+ acorn: 8.19.0
807+
808+ '@sveltejs/load-config@0.2.3': {}
809+
810+ '@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.3)(vite@8.3.5)':
811+ dependencies:
812+ deepmerge: 4.3.1
813+ magic-string: 1.4.5
814+ obug: 2.2.1
815+ svelte: 5.57.3
816+ vite: 8.3.5
817+ vitefu: 1.1.3(vite@8.3.5)
818+
819+ '@tsconfig/svelte@5.0.8': {}
820+
821+ '@types/chai@5.2.3':
822+ dependencies:
823+ '@types/deep-eql': 4.0.2
824+ assertion-error: 2.0.1
825+
826+ '@types/deep-eql@4.0.2': {}
827+
828+ '@types/estree@1.0.9': {}
829+
830+ '@vitest/mocker@5.0.4(vite@8.3.5)':
831+ dependencies:
832+ '@jridgewell/trace-mapping': 0.3.31
833+ '@vitest/spy': 5.0.4
834+ estree-walker: 3.0.3
835+ magic-string: 1.4.5
836+ optionalDependencies:
837+ vite: 8.3.5
838+
839+ '@vitest/spy@5.0.4': {}
840+
841+ '@wailsio/runtime@3.0.0': {}
842+
843+ acorn@8.19.0: {}
844+
845+ aria-query@5.3.1: {}
846+
847+ assertion-error@2.0.1: {}
848+
849+ axobject-query@4.1.0: {}
850+
851+ chai@6.3.0: {}
852+
853+ chokidar@4.0.3:
854+ dependencies:
855+ readdirp: 4.1.2
856+
857+ clsx@2.1.1: {}
858+
859+ deepmerge@4.3.1: {}
860+
861+ detect-libc@2.1.2: {}
862+
863+ devalue@5.9.4: {}
864+
865+ es-module-lexer@2.3.2: {}
866+
867+ esm-env@1.2.2: {}
868+
869+ esrap@2.4.0:
870+ dependencies:
871+ '@jridgewell/sourcemap-codec': 1.6.0
872+
873+ estree-walker@3.0.3:
874+ dependencies:
875+ '@types/estree': 1.0.9
876+
877+ expect-type@1.4.0: {}
878+
879+ fdir@6.5.0(picomatch@4.0.7):
880+ optionalDependencies:
881+ picomatch: 4.0.7
882+
883+ fsevents@2.3.3:
884+ optional: true
885+
886+ inline-style-parser@0.2.7: {}
887+
888+ is-reference@3.0.3:
889+ dependencies:
890+ '@types/estree': 1.0.9
891+
892+ lightningcss-android-arm64@1.33.0:
893+ optional: true
894+
895+ lightningcss-darwin-arm64@1.33.0:
896+ optional: true
897+
898+ lightningcss-darwin-x64@1.33.0:
899+ optional: true
900+
901+ lightningcss-freebsd-x64@1.33.0:
902+ optional: true
903+
904+ lightningcss-linux-arm-gnueabihf@1.33.0:
905+ optional: true
906+
907+ lightningcss-linux-arm64-gnu@1.33.0:
908+ optional: true
909+
910+ lightningcss-linux-arm64-musl@1.33.0:
911+ optional: true
912+
913+ lightningcss-linux-x64-gnu@1.33.0:
914+ optional: true
915+
916+ lightningcss-linux-x64-musl@1.33.0:
917+ optional: true
918+
919+ lightningcss-win32-arm64-msvc@1.33.0:
920+ optional: true
921+
922+ lightningcss-win32-x64-msvc@1.33.0:
923+ optional: true
924+
925+ lightningcss@1.33.0:
926+ dependencies:
927+ detect-libc: 2.1.2
928+ optionalDependencies:
929+ lightningcss-android-arm64: 1.33.0
930+ lightningcss-darwin-arm64: 1.33.0
931+ lightningcss-darwin-x64: 1.33.0
932+ lightningcss-freebsd-x64: 1.33.0
933+ lightningcss-linux-arm-gnueabihf: 1.33.0
934+ lightningcss-linux-arm64-gnu: 1.33.0
935+ lightningcss-linux-arm64-musl: 1.33.0
936+ lightningcss-linux-x64-gnu: 1.33.0
937+ lightningcss-linux-x64-musl: 1.33.0
938+ lightningcss-win32-arm64-msvc: 1.33.0
939+ lightningcss-win32-x64-msvc: 1.33.0
940+
941+ locate-character@3.0.0: {}
942+
943+ magic-string@0.30.21:
944+ dependencies:
945+ '@jridgewell/sourcemap-codec': 1.6.0
946+
947+ magic-string@1.4.5:
948+ dependencies:
949+ '@jridgewell/sourcemap-codec': 1.6.0
950+
951+ mri@1.2.0: {}
952+
953+ nanoid@3.3.20: {}
954+
955+ obug@2.2.1: {}
956+
957+ obug@3.0.0: {}
958+
959+ paneforge@1.0.2(svelte@5.57.3):
960+ dependencies:
961+ runed: 0.23.4(svelte@5.57.3)
962+ svelte: 5.57.3
963+ svelte-toolbelt: 0.9.3(svelte@5.57.3)
964+
965+ picocolors@1.1.1: {}
966+
967+ picomatch@4.0.7: {}
968+
969+ postcss@8.5.32:
970+ dependencies:
971+ nanoid: 3.3.20
972+ picocolors: 1.1.1
973+ source-map-js: 1.2.2
974+
975+ readdirp@4.1.2: {}
976+
977+ rolldown@1.2.15:
978+ dependencies:
979+ '@oxc-project/types': 0.155.0
980+ '@rolldown/pluginutils': 1.0.1
981+ optionalDependencies:
982+ '@rolldown/binding-android-arm-eabi': 1.2.15
983+ '@rolldown/binding-android-arm64': 1.2.15
984+ '@rolldown/binding-darwin-arm64': 1.2.15
985+ '@rolldown/binding-darwin-x64': 1.2.15
986+ '@rolldown/binding-freebsd-x64': 1.2.15
987+ '@rolldown/binding-linux-arm-gnueabihf': 1.2.15
988+ '@rolldown/binding-linux-arm64-gnu': 1.2.15
989+ '@rolldown/binding-linux-arm64-musl': 1.2.15
990+ '@rolldown/binding-linux-ppc64-gnu': 1.2.15
991+ '@rolldown/binding-linux-s390x-gnu': 1.2.15
992+ '@rolldown/binding-linux-x64-gnu': 1.2.15
993+ '@rolldown/binding-linux-x64-musl': 1.2.15
994+ '@rolldown/binding-openharmony-arm64': 1.2.15
995+ '@rolldown/binding-win32-arm64-msvc': 1.2.15
996+ '@rolldown/binding-win32-x64-msvc': 1.2.15
997+
998+ runed@0.23.4(svelte@5.57.3):
999+ dependencies:
1000+ esm-env: 1.2.2
1001+ svelte: 5.57.3
1002+
1003+ runed@0.29.2(svelte@5.57.3):
1004+ dependencies:
1005+ esm-env: 1.2.2
1006+ svelte: 5.57.3
1007+
1008+ sade@1.8.1:
1009+ dependencies:
1010+ mri: 1.2.0
1011+
1012+ source-map-js@1.2.2: {}
1013+
1014+ std-env@4.3.0: {}
1015+
1016+ style-to-object@1.0.14:
1017+ dependencies:
1018+ inline-style-parser: 0.2.7
1019+
1020+ svelte-check@4.7.7(picomatch@4.0.7)(svelte@5.57.3)(typescript@5.9.3):
1021+ dependencies:
1022+ '@jridgewell/trace-mapping': 0.3.31
1023+ '@sveltejs/load-config': 0.2.3
1024+ chokidar: 4.0.3
1025+ fdir: 6.5.0(picomatch@4.0.7)
1026+ picocolors: 1.1.1
1027+ sade: 1.8.1
1028+ svelte: 5.57.3
1029+ typescript: 5.9.3
1030+ transitivePeerDependencies:
1031+ - picomatch
1032+
1033+ svelte-toolbelt@0.9.3(svelte@5.57.3):
1034+ dependencies:
1035+ clsx: 2.1.1
1036+ runed: 0.29.2(svelte@5.57.3)
1037+ style-to-object: 1.0.14
1038+ svelte: 5.57.3
1039+
1040+ svelte@5.57.3:
1041+ dependencies:
1042+ '@jridgewell/remapping': 2.3.5
1043+ '@jridgewell/sourcemap-codec': 1.6.0
1044+ '@sveltejs/acorn-typescript': 1.0.13(acorn@8.19.0)
1045+ '@types/estree': 1.0.9
1046+ acorn: 8.19.0
1047+ aria-query: 5.3.1
1048+ axobject-query: 4.1.0
1049+ clsx: 2.1.1
1050+ devalue: 5.9.4
1051+ esm-env: 1.2.2
1052+ esrap: 2.4.0
1053+ is-reference: 3.0.3
1054+ locate-character: 3.0.0
1055+ magic-string: 0.30.21
1056+ zimmerframe: 1.1.5
1057+ transitivePeerDependencies:
1058+ - '@typescript-eslint/types'
1059+
1060+ tinybench@6.2.2: {}
1061+
1062+ tinyexec@1.3.1: {}
1063+
1064+ tinyglobby@0.2.17:
1065+ dependencies:
1066+ fdir: 6.5.0(picomatch@4.0.7)
1067+ picomatch: 4.0.7
1068+
1069+ tslib@2.8.1: {}
1070+
1071+ typescript@5.9.3: {}
1072+
1073+ vite@8.3.5:
1074+ dependencies:
1075+ lightningcss: 1.33.0
1076+ picomatch: 4.0.7
1077+ postcss: 8.5.32
1078+ rolldown: 1.2.15
1079+ tinyglobby: 0.2.17
1080+ optionalDependencies:
1081+ fsevents: 2.3.3
1082+
1083+ vitefu@1.1.3(vite@8.3.5):
1084+ optionalDependencies:
1085+ vite: 8.3.5
1086+
1087+ vitest@5.0.4(vite@8.3.5):
1088+ dependencies:
1089+ '@types/chai': 5.2.3
1090+ '@vitest/mocker': 5.0.4(vite@8.3.5)
1091+ chai: 6.3.0
1092+ es-module-lexer: 2.3.2
1093+ expect-type: 1.4.0
1094+ magic-string: 1.4.5
1095+ obug: 3.0.0
1096+ picomatch: 4.0.7
1097+ std-env: 4.3.0
1098+ tinybench: 6.2.2
1099+ tinyexec: 1.3.1
1100+ tinyglobby: 0.2.17
1101+ vite: 8.3.5
1102+ why-is-node-running: 3.2.1
1103+ transitivePeerDependencies:
1104+ - msw
1105+
1106+ why-is-node-running@3.2.1: {}
1107+
1108+ zimmerframe@1.1.5: {}
new file mode 100644
@@ -0,0 +1,1108 @@
1+---
2+lockfileVersion: '9.0'
3+
4+importers:
5+
6+ .:
7+ configDependencies: {}
8+ packageManagerDependencies:
9+ pnpm:
10+ specifier: 12.11.2
11+ version: 12.11.2
12+
13+packages:
14+
15+ '@pnpm/exe.android-arm64@12.11.2':
16+ resolution: {integrity: sha512-owLw13OfPkEoY+hLBNeeG+dCFhywr7q2d/EHPz4mNL2RhKnv8gNwMARkjkiDQu234aItVwFNDeGnmQXm+ikkAg==}
17+ cpu: [arm64]
18+ os: [android]
19+
20+ '@pnpm/exe.android-x64@12.11.2':
21+ resolution: {integrity: sha512-GYPkVebl4iokOA/4MPSpGoUVfFlsth7uqOCe1XiaiM23LAdvUOMJBObqHJijr89ZqjwHm9pO6vO45GRwJ2U2qA==}
22+ cpu: [x64]
23+ os: [android]
24+
25+ '@pnpm/exe.darwin-arm64@12.11.2':
26+ resolution: {integrity: sha512-n9M4On66y8FDxUAKgMO1FXt0qihyG4e+56p3/F0uo8Bx9KplW8BwKVnXigzCJ8aNM6sVVPkOE6M9XXbeVIxnZA==}
27+ cpu: [arm64]
28+ os: [darwin]
29+
30+ '@pnpm/exe.darwin-x64@12.11.2':
31+ resolution: {integrity: sha512-oSW7n3sBT0c0cyk4kj10T2Y6Mpm+OFEiCAWozgX039tdoI6yqJF1uaFD3sw7ugLdu6VV3+uY0pa1TUZIPdxDWA==}
32+ cpu: [x64]
33+ os: [darwin]
34+
35+ '@pnpm/exe.freebsd-x64@12.11.2':
36+ resolution: {integrity: sha512-qbzc3KoDpa/Lkf8vs3jnDe6dUUbso+6eLIyssYFMiliLpYNxn8dirk11YoNnOQcdHVB50v5RFls1Yid6ttT5pg==}
37+ cpu: [x64]
38+ os: [freebsd]
39+
40+ '@pnpm/exe.linux-arm64-musl@12.11.2':
41+ resolution: {integrity: sha512-uMEYDwKRktGwANubQ+kYCwmhNwg3Ik6qK89ahsbQ5cP30R/5FVJb/T99vr9XRUBC/A7mZI6eeT92oyVas9ACyg==}
42+ cpu: [arm64]
43+ os: [linux]
44+ libc: [musl]
45+
46+ '@pnpm/exe.linux-arm64@12.11.2':
47+ resolution: {integrity: sha512-6e8ZAQUwFXAWHHHv5FesWqf/kEK04wuitBv5f/gxzkC0DiBfaixr+f+6JQede8SF4Ji5LcMwVFJbzAtffpWxEg==}
48+ cpu: [arm64]
49+ os: [linux]
50+ libc: [glibc]
51+
52+ '@pnpm/exe.linux-ppc64@12.11.2':
53+ resolution: {integrity: sha512-Ui8A4nepkVupx2VpuFfysEluMvWK6gTB5rgVts7Ss/+8dj0RD28x3F/seNeH/zSypgC6yKdV6qsKVQh/uffTuQ==}
54+ cpu: [ppc64]
55+ os: [linux]
56+ libc: [glibc]
57+
58+ '@pnpm/exe.linux-riscv64@12.11.2':
59+ resolution: {integrity: sha512-i1yLdQ5bu+wjQ+srtFoJAaY2gN/+oXHFdjul4llCeoDxHE0rTIG3R41NhHfCjyFPW4+CpbBT58933CL9sMOCnA==}
60+ cpu: [riscv64]
61+ os: [linux]
62+ libc: [glibc]
63+
64+ '@pnpm/exe.linux-s390x@12.11.2':
65+ resolution: {integrity: sha512-YS1/lhPjjTsN4QL50VaFZKRa3Uc/bDnhE8eKbJMFvuX06UuFS5YaiDg04APoZdfVSGqTfX27QAg7rfLMSWaqxA==}
66+ cpu: [s390x]
67+ os: [linux]
68+ libc: [glibc]
69+
70+ '@pnpm/exe.linux-x64-musl@12.11.2':
71+ resolution: {integrity: sha512-7eIxQR05fvJxCujUSKp5KTtehJ7QqrgqP/HD1+BcB/SaV2Be4XI1PEHiOgA1zxsiMIeMvTF6E4G4dYHrFjeXRg==}
72+ cpu: [x64]
73+ os: [linux]
74+ libc: [musl]
75+
76+ '@pnpm/exe.linux-x64@12.11.2':
77+ resolution: {integrity: sha512-FK7QKe24AYgppJwO3mpooYptduuvIGqKJPi5ac7MdnWkjUy46JBfYhCW0grnkWKC8YOf0x+vGviuQqJEj3SpQQ==}
78+ cpu: [x64]
79+ os: [linux]
80+ libc: [glibc]
81+
82+ '@pnpm/exe.win32-arm64@12.11.2':
83+ resolution: {integrity: sha512-PCTjVDdJb6VvraZxrEeFv6EqEA0hhIsAySx28e95J2Tqpi88hLpgOXgNqV0vpxq9A+b8GLQfyk/D+1UlJPv/YQ==}
84+ cpu: [arm64]
85+ os: [win32]
86+
87+ '@pnpm/exe.win32-x64@12.11.2':
88+ resolution: {integrity: sha512-8sgX86OvRjJ3yZs/TvGR2L31wH9ScPZ+f+geKI3ikuB0RRiK3i4LlTRO8OXN6pdRqj8epMde4312MCjj7JsJdA==}
89+ cpu: [x64]
90+ os: [win32]
91+
92+ pnpm@12.11.2:
93+ resolution: {integrity: sha512-JaUWUqGWeFr18ovabf6NxYLr76SsHRgCIc4TkCMcX7dmN4IupLbkYzznSs3LCnf3Ja8RutOBd/3t2u6BipVcuQ==}
94+ engines: {node: '>=18.*'}
95+ hasBin: true
96+
97+snapshots:
98+
99+ '@pnpm/exe.android-arm64@12.11.2':
100+ optional: true
101+
102+ '@pnpm/exe.android-x64@12.11.2':
103+ optional: true
104+
105+ '@pnpm/exe.darwin-arm64@12.11.2':
106+ optional: true
107+
108+ '@pnpm/exe.darwin-x64@12.11.2':
109+ optional: true
110+
111+ '@pnpm/exe.freebsd-x64@12.11.2':
112+ optional: true
113+
114+ '@pnpm/exe.linux-arm64-musl@12.11.2':
115+ optional: true
116+
117+ '@pnpm/exe.linux-arm64@12.11.2':
118+ optional: true
119+
120+ '@pnpm/exe.linux-ppc64@12.11.2':
121+ optional: true
122+
123+ '@pnpm/exe.linux-riscv64@12.11.2':
124+ optional: true
125+
126+ '@pnpm/exe.linux-s390x@12.11.2':
127+ optional: true
128+
129+ '@pnpm/exe.linux-x64-musl@12.11.2':
130+ optional: true
131+
132+ '@pnpm/exe.linux-x64@12.11.2':
133+ optional: true
134+
135+ '@pnpm/exe.win32-arm64@12.11.2':
136+ optional: true
137+
138+ '@pnpm/exe.win32-x64@12.11.2':
139+ optional: true
140+
141+ pnpm@12.11.2:
142+ optionalDependencies:
143+ '@pnpm/exe.android-arm64': 12.11.2
144+ '@pnpm/exe.android-x64': 12.11.2
145+ '@pnpm/exe.darwin-arm64': 12.11.2
146+ '@pnpm/exe.darwin-x64': 12.11.2
147+ '@pnpm/exe.freebsd-x64': 12.11.2
148+ '@pnpm/exe.linux-arm64': 12.11.2
149+ '@pnpm/exe.linux-arm64-musl': 12.11.2
150+ '@pnpm/exe.linux-ppc64': 12.11.2
151+ '@pnpm/exe.linux-riscv64': 12.11.2
152+ '@pnpm/exe.linux-s390x': 12.11.2
153+ '@pnpm/exe.linux-x64': 12.11.2
154+ '@pnpm/exe.linux-x64-musl': 12.11.2
155+ '@pnpm/exe.win32-arm64': 12.11.2
156+ '@pnpm/exe.win32-x64': 12.11.2
157+
158+---
159+lockfileVersion: '9.0'
160+
161+settings:
162+ autoInstallPeers: true
163+ excludeLinksFromLockfile: false
164+
165+importers:
166+
167+ .:
168+ dependencies:
169+ '@wailsio/runtime':
170+ specifier: 3.0.0
171+ version: 3.0.0
172+ paneforge:
173+ specifier: 1.0.2
174+ version: 1.0.2(svelte@5.57.3)
175+ devDependencies:
176+ '@sveltejs/vite-plugin-svelte':
177+ specifier: ^7.0.0
178+ version: 7.3.1(svelte@5.57.3)(vite@8.3.5)
179+ '@tsconfig/svelte':
180+ specifier: ^5.0.8
181+ version: 5.0.8
182+ svelte:
183+ specifier: ^5.46.4
184+ version: 5.57.3
185+ svelte-check:
186+ specifier: ^4.4.8
187+ version: 4.7.7(picomatch@4.0.7)(svelte@5.57.3)(typescript@5.9.3)
188+ tslib:
189+ specifier: ^2.6.2
190+ version: 2.8.1
191+ typescript:
192+ specifier: ^5.2.2
193+ version: 5.9.3
194+ vite:
195+ specifier: ^8.2.4
196+ version: 8.3.5
197+ vitest:
198+ specifier: 5.0.4
199+ version: 5.0.4(vite@8.3.5)
200+
201+packages:
202+
203+ '@jridgewell/gen-mapping@0.3.13':
204+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
205+
206+ '@jridgewell/remapping@2.3.5':
207+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
208+
209+ '@jridgewell/resolve-uri@3.1.2':
210+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
211+ engines: {node: '>=6.0.0'}
212+
213+ '@jridgewell/sourcemap-codec@1.6.0':
214+ resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
215+
216+ '@jridgewell/trace-mapping@0.3.31':
217+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
218+
219+ '@oxc-project/types@0.155.0':
220+ resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==}
221+
222+ '@rolldown/binding-android-arm-eabi@1.2.15':
223+ resolution: {integrity: sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==}
224+ engines: {node: ^20.19.0 || >=22.12.0}
225+ cpu: [arm]
226+ os: [android]
227+
228+ '@rolldown/binding-android-arm64@1.2.15':
229+ resolution: {integrity: sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==}
230+ engines: {node: ^20.19.0 || >=22.12.0}
231+ cpu: [arm64]
232+ os: [android]
233+
234+ '@rolldown/binding-darwin-arm64@1.2.15':
235+ resolution: {integrity: sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==}
236+ engines: {node: ^20.19.0 || >=22.12.0}
237+ cpu: [arm64]
238+ os: [darwin]
239+
240+ '@rolldown/binding-darwin-x64@1.2.15':
241+ resolution: {integrity: sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==}
242+ engines: {node: ^20.19.0 || >=22.12.0}
243+ cpu: [x64]
244+ os: [darwin]
245+
246+ '@rolldown/binding-freebsd-x64@1.2.15':
247+ resolution: {integrity: sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==}
248+ engines: {node: ^20.19.0 || >=22.12.0}
249+ cpu: [x64]
250+ os: [freebsd]
251+
252+ '@rolldown/binding-linux-arm-gnueabihf@1.2.15':
253+ resolution: {integrity: sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==}
254+ engines: {node: ^20.19.0 || >=22.12.0}
255+ cpu: [arm]
256+ os: [linux]
257+
258+ '@rolldown/binding-linux-arm64-gnu@1.2.15':
259+ resolution: {integrity: sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==}
260+ engines: {node: ^20.19.0 || >=22.12.0}
261+ cpu: [arm64]
262+ os: [linux]
263+ libc: [glibc]
264+
265+ '@rolldown/binding-linux-arm64-musl@1.2.15':
266+ resolution: {integrity: sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==}
267+ engines: {node: ^20.19.0 || >=22.12.0}
268+ cpu: [arm64]
269+ os: [linux]
270+ libc: [musl]
271+
272+ '@rolldown/binding-linux-ppc64-gnu@1.2.15':
273+ resolution: {integrity: sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==}
274+ engines: {node: ^20.19.0 || >=22.12.0}
275+ cpu: [ppc64]
276+ os: [linux]
277+ libc: [glibc]
278+
279+ '@rolldown/binding-linux-s390x-gnu@1.2.15':
280+ resolution: {integrity: sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==}
281+ engines: {node: ^20.19.0 || >=22.12.0}
282+ cpu: [s390x]
283+ os: [linux]
284+ libc: [glibc]
285+
286+ '@rolldown/binding-linux-x64-gnu@1.2.15':
287+ resolution: {integrity: sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==}
288+ engines: {node: ^20.19.0 || >=22.12.0}
289+ cpu: [x64]
290+ os: [linux]
291+ libc: [glibc]
292+
293+ '@rolldown/binding-linux-x64-musl@1.2.15':
294+ resolution: {integrity: sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==}
295+ engines: {node: ^20.19.0 || >=22.12.0}
296+ cpu: [x64]
297+ os: [linux]
298+ libc: [musl]
299+
300+ '@rolldown/binding-openharmony-arm64@1.2.15':
301+ resolution: {integrity: sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==}
302+ engines: {node: ^20.19.0 || >=22.12.0}
303+ cpu: [arm64]
304+ os: [openharmony]
305+
306+ '@rolldown/binding-win32-arm64-msvc@1.2.15':
307+ resolution: {integrity: sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==}
308+ engines: {node: ^20.19.0 || >=22.12.0}
309+ cpu: [arm64]
310+ os: [win32]
311+
312+ '@rolldown/binding-win32-x64-msvc@1.2.15':
313+ resolution: {integrity: sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==}
314+ engines: {node: ^20.19.0 || >=22.12.0}
315+ cpu: [x64]
316+ os: [win32]
317+
318+ '@rolldown/pluginutils@1.0.1':
319+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
320+
321+ '@sveltejs/acorn-typescript@1.0.13':
322+ resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==}
323+ peerDependencies:
324+ acorn: ^8.9.0
325+
326+ '@sveltejs/load-config@0.2.3':
327+ resolution: {integrity: sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==}
328+ engines: {node: '>= 18.0.0'}
329+
330+ '@sveltejs/vite-plugin-svelte@7.3.1':
331+ resolution: {integrity: sha512-ZPsLN8B1e/En+Ak5s4V7srFDT532oS0qieLsQwu63NGKsS+iAjoO2Js1BochlHlglcU+Pt7WAO3C5Ee+4f6gVA==}
332+ engines: {node: ^20.19 || ^22.12 || >=24}
333+ peerDependencies:
334+ svelte: ^5.46.4
335+ vite: ^8.0.0-beta.7 || ^8.0.0
336+
337+ '@tsconfig/svelte@5.0.8':
338+ resolution: {integrity: sha512-UkNnw1/oFEfecR8ypyHIQuWYdkPvHiwcQ78sh+ymIiYoF+uc5H1UBetbjyqT+vgGJ3qQN6nhucJviX6HesWtKQ==}
339+
340+ '@types/chai@5.2.3':
341+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
342+
343+ '@types/deep-eql@4.0.2':
344+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
345+
346+ '@types/estree@1.0.9':
347+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
348+
349+ '@vitest/mocker@5.0.4':
350+ resolution: {integrity: sha512-tqAlrYWce4iUxhr3ncNohejtCTOcHeE8XWrSNZY3OhPoDn01yeGv+gnaBy64ATp7o6pb03eIbBAjzmibpc9vIQ==}
351+ peerDependencies:
352+ msw: ^2.4.9 || ^3.0.0
353+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
354+ peerDependenciesMeta:
355+ msw:
356+ optional: true
357+ vite:
358+ optional: true
359+
360+ '@vitest/spy@5.0.4':
361+ resolution: {integrity: sha512-MZKk/gSkm2q+ICobMxCpH1mA+cCAWm8WV7UhxlqGjq/sJ3AGc1BqFOs5CRVXxH78wQtme9q/NAAgBquj4AcyMw==}
362+
363+ '@wailsio/runtime@3.0.0':
364+ resolution: {integrity: sha512-NkFJXYWD/4Y3TcHAqfIeST4oybtwv9dVWTmriZTMFbINYiQ2FB3Wr4MjsQ7o0BNynEj2GslmUisNejnixEmLVQ==}
365+
366+ acorn@8.19.0:
367+ resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==}
368+ engines: {node: '>=0.4.0'}
369+ hasBin: true
370+
371+ aria-query@5.3.1:
372+ resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==}
373+ engines: {node: '>= 0.4'}
374+
375+ assertion-error@2.0.1:
376+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
377+ engines: {node: '>=12'}
378+
379+ axobject-query@4.1.0:
380+ resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
381+ engines: {node: '>= 0.4'}
382+
383+ chai@6.3.0:
384+ resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==}
385+ engines: {node: '>=18'}
386+
387+ chokidar@4.0.3:
388+ resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
389+ engines: {node: '>= 14.16.0'}
390+
391+ clsx@2.1.1:
392+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
393+ engines: {node: '>=6'}
394+
395+ deepmerge@4.3.1:
396+ resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
397+ engines: {node: '>=0.10.0'}
398+
399+ detect-libc@2.1.2:
400+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
401+ engines: {node: '>=8'}
402+
403+ devalue@5.9.4:
404+ resolution: {integrity: sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==}
405+
406+ es-module-lexer@2.3.2:
407+ resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
408+
409+ esm-env@1.2.2:
410+ resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
411+
412+ esrap@2.4.0:
413+ resolution: {integrity: sha512-CQPcneEHw/czj32uwQC52nYnax8H4SXt25FCM+LIwGZ49cMIxpOj3bP8wmzcI+Z0HIy5KkClaPhRLfCySzwHPA==}
414+ peerDependencies:
415+ '@typescript-eslint/types': ^8.2.0
416+ peerDependenciesMeta:
417+ '@typescript-eslint/types':
418+ optional: true
419+
420+ estree-walker@3.0.3:
421+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
422+
423+ expect-type@1.4.0:
424+ resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
425+ engines: {node: '>=12.0.0'}
426+
427+ fdir@6.5.0:
428+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
429+ engines: {node: '>=12.0.0'}
430+ peerDependencies:
431+ picomatch: ^3 || ^4
432+ peerDependenciesMeta:
433+ picomatch:
434+ optional: true
435+
436+ fsevents@2.3.3:
437+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
438+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
439+ os: [darwin]
440+
441+ inline-style-parser@0.2.7:
442+ resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
443+
444+ is-reference@3.0.3:
445+ resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
446+
447+ lightningcss-android-arm64@1.33.0:
448+ resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
449+ engines: {node: '>= 12.0.0'}
450+ cpu: [arm64]
451+ os: [android]
452+
453+ lightningcss-darwin-arm64@1.33.0:
454+ resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==}
455+ engines: {node: '>= 12.0.0'}
456+ cpu: [arm64]
457+ os: [darwin]
458+
459+ lightningcss-darwin-x64@1.33.0:
460+ resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==}
461+ engines: {node: '>= 12.0.0'}
462+ cpu: [x64]
463+ os: [darwin]
464+
465+ lightningcss-freebsd-x64@1.33.0:
466+ resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==}
467+ engines: {node: '>= 12.0.0'}
468+ cpu: [x64]
469+ os: [freebsd]
470+
471+ lightningcss-linux-arm-gnueabihf@1.33.0:
472+ resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==}
473+ engines: {node: '>= 12.0.0'}
474+ cpu: [arm]
475+ os: [linux]
476+
477+ lightningcss-linux-arm64-gnu@1.33.0:
478+ resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==}
479+ engines: {node: '>= 12.0.0'}
480+ cpu: [arm64]
481+ os: [linux]
482+ libc: [glibc]
483+
484+ lightningcss-linux-arm64-musl@1.33.0:
485+ resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==}
486+ engines: {node: '>= 12.0.0'}
487+ cpu: [arm64]
488+ os: [linux]
489+ libc: [musl]
490+
491+ lightningcss-linux-x64-gnu@1.33.0:
492+ resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==}
493+ engines: {node: '>= 12.0.0'}
494+ cpu: [x64]
495+ os: [linux]
496+ libc: [glibc]
497+
498+ lightningcss-linux-x64-musl@1.33.0:
499+ resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==}
500+ engines: {node: '>= 12.0.0'}
501+ cpu: [x64]
502+ os: [linux]
503+ libc: [musl]
504+
505+ lightningcss-win32-arm64-msvc@1.33.0:
506+ resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==}
507+ engines: {node: '>= 12.0.0'}
508+ cpu: [arm64]
509+ os: [win32]
510+
511+ lightningcss-win32-x64-msvc@1.33.0:
512+ resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==}
513+ engines: {node: '>= 12.0.0'}
514+ cpu: [x64]
515+ os: [win32]
516+
517+ lightningcss@1.33.0:
518+ resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
519+ engines: {node: '>= 12.0.0'}
520+
521+ locate-character@3.0.0:
522+ resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
523+
524+ magic-string@0.30.21:
525+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
526+
527+ magic-string@1.4.5:
528+ resolution: {integrity: sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==}
529+
530+ mri@1.2.0:
531+ resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
532+ engines: {node: '>=4'}
533+
534+ nanoid@3.3.20:
535+ resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
536+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
537+ hasBin: true
538+
539+ obug@2.2.1:
540+ resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==}
541+ engines: {node: '>=12.20.0'}
542+
543+ obug@3.0.0:
544+ resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==}
545+ engines: {node: '>=12.20.0'}
546+
547+ paneforge@1.0.2:
548+ resolution: {integrity: sha512-KzmIXQH1wCfwZ4RsMohD/IUtEjVhteR+c+ulb/CHYJHX8SuDXoJmChtsc/Xs5Wl8NHS4L5Q7cxL8MG40gSU1bA==}
549+ peerDependencies:
550+ svelte: ^5.29.0
551+
552+ picocolors@1.1.1:
553+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
554+
555+ picomatch@4.0.7:
556+ resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
557+ engines: {node: '>=12'}
558+
559+ postcss@8.5.32:
560+ resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==}
561+ engines: {node: ^10 || ^12 || >=14}
562+
563+ readdirp@4.1.2:
564+ resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
565+ engines: {node: '>= 14.18.0'}
566+
567+ rolldown@1.2.15:
568+ resolution: {integrity: sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==}
569+ engines: {node: ^20.19.0 || >=22.12.0}
570+ hasBin: true
571+
572+ runed@0.23.4:
573+ resolution: {integrity: sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==}
574+ peerDependencies:
575+ svelte: ^5.7.0
576+
577+ runed@0.29.2:
578+ resolution: {integrity: sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==}
579+ peerDependencies:
580+ svelte: ^5.7.0
581+
582+ sade@1.8.1:
583+ resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
584+ engines: {node: '>=6'}
585+
586+ source-map-js@1.2.2:
587+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
588+ engines: {node: '>=0.10.0'}
589+
590+ std-env@4.3.0:
591+ resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==}
592+
593+ style-to-object@1.0.14:
594+ resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
595+
596+ svelte-check@4.7.7:
597+ resolution: {integrity: sha512-Ge2IMdeTpbJuzNFGRiiMg0RWgbScysKQu7xwcyFJ22+DU9F5nPX51+Q3gHmGqRxYQMx2knreVWt5oCtfe0vf2g==}
598+ engines: {node: '>= 18.0.0'}
599+ hasBin: true
600+ peerDependencies:
601+ svelte: ^4.0.0 || ^5.0.0-next.0
602+ typescript: ^5.0.0 || ^6.0.0
603+
604+ svelte-toolbelt@0.9.3:
605+ resolution: {integrity: sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw==}
606+ engines: {node: '>=18', pnpm: '>=8.7.0'}
607+ peerDependencies:
608+ svelte: ^5.30.2
609+
610+ svelte@5.57.3:
611+ resolution: {integrity: sha512-5mAftM3SoDV9X0zdF386F9p2pv8iKOwi/KfBS1XnBAkVCKFzVOLHUoa8iGWAt4buoicLmyWVRpuuD5wplXOQsg==}
612+ engines: {node: '>=18'}
613+
614+ tinybench@6.2.2:
615+ resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==}
616+ engines: {node: '>=20.0.0'}
617+
618+ tinyexec@1.3.1:
619+ resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==}
620+ engines: {node: '>=18'}
621+
622+ tinyglobby@0.2.17:
623+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
624+ engines: {node: '>=12.0.0'}
625+
626+ tslib@2.8.1:
627+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
628+
629+ typescript@5.9.3:
630+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
631+ engines: {node: '>=14.17'}
632+ hasBin: true
633+
634+ vite@8.3.5:
635+ resolution: {integrity: sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==}
636+ engines: {node: ^20.19.0 || >=22.12.0}
637+ hasBin: true
638+ peerDependencies:
639+ '@types/node': ^20.19.0 || >=22.12.0
640+ '@vitejs/devtools': ^0.7.1 || ^0.8.0
641+ esbuild: ^0.27.0 || ^0.28.0
642+ jiti: '>=1.21.0'
643+ less: ^4.0.0
644+ sass: ^1.70.0
645+ sass-embedded: ^1.70.0
646+ stylus: '>=0.54.8'
647+ sugarss: ^5.0.0
648+ terser: ^5.16.0
649+ tsx: ^4.8.1
650+ yaml: ^2.4.2
651+ peerDependenciesMeta:
652+ '@types/node':
653+ optional: true
654+ '@vitejs/devtools':
655+ optional: true
656+ esbuild:
657+ optional: true
658+ jiti:
659+ optional: true
660+ less:
661+ optional: true
662+ sass:
663+ optional: true
664+ sass-embedded:
665+ optional: true
666+ stylus:
667+ optional: true
668+ sugarss:
669+ optional: true
670+ terser:
671+ optional: true
672+ tsx:
673+ optional: true
674+ yaml:
675+ optional: true
676+
677+ vitefu@1.1.3:
678+ resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==}
679+ peerDependencies:
680+ vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
681+ peerDependenciesMeta:
682+ vite:
683+ optional: true
684+
685+ vitest@5.0.4:
686+ resolution: {integrity: sha512-z0WxZRV2Sg48wF0FSgMq3TFG8JS38/Qr96vZVZdX6+Em9raQTvnXQvdTFJe2m3rfyJnEHeHQ/snceIdi8D1pVg==}
687+ engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0}
688+ hasBin: true
689+ peerDependencies:
690+ '@edge-runtime/vm': '*'
691+ '@opentelemetry/api': ^1.9.0
692+ '@types/node': ^22.0.0 || >=24.0.0
693+ '@vitest/browser-playwright': 5.0.4
694+ '@vitest/browser-preview': 5.0.4
695+ '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0
696+ '@vitest/coverage-istanbul': 5.0.4
697+ '@vitest/coverage-v8': 5.0.4
698+ '@vitest/ui': 5.0.4
699+ happy-dom: '*'
700+ jsdom: '*'
701+ vite: ^6.4.0 || ^7.0.0 || ^8.0.0
702+ peerDependenciesMeta:
703+ '@edge-runtime/vm':
704+ optional: true
705+ '@opentelemetry/api':
706+ optional: true
707+ '@types/node':
708+ optional: true
709+ '@vitest/browser-playwright':
710+ optional: true
711+ '@vitest/browser-preview':
712+ optional: true
713+ '@vitest/browser-webdriverio':
714+ optional: true
715+ '@vitest/coverage-istanbul':
716+ optional: true
717+ '@vitest/coverage-v8':
718+ optional: true
719+ '@vitest/ui':
720+ optional: true
721+ happy-dom:
722+ optional: true
723+ jsdom:
724+ optional: true
725+
726+ why-is-node-running@3.2.1:
727+ resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==}
728+ engines: {node: '>=20.11'}
729+ hasBin: true
730+
731+ zimmerframe@1.1.5:
732+ resolution: {integrity: sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==}
733+
734+snapshots:
735+
736+ '@jridgewell/gen-mapping@0.3.13':
737+ dependencies:
738+ '@jridgewell/sourcemap-codec': 1.6.0
739+ '@jridgewell/trace-mapping': 0.3.31
740+
741+ '@jridgewell/remapping@2.3.5':
742+ dependencies:
743+ '@jridgewell/gen-mapping': 0.3.13
744+ '@jridgewell/trace-mapping': 0.3.31
745+
746+ '@jridgewell/resolve-uri@3.1.2': {}
747+
748+ '@jridgewell/sourcemap-codec@1.6.0': {}
749+
750+ '@jridgewell/trace-mapping@0.3.31':
751+ dependencies:
752+ '@jridgewell/resolve-uri': 3.1.2
753+ '@jridgewell/sourcemap-codec': 1.6.0
754+
755+ '@oxc-project/types@0.155.0': {}
756+
757+ '@rolldown/binding-android-arm-eabi@1.2.15':
758+ optional: true
759+
760+ '@rolldown/binding-android-arm64@1.2.15':
761+ optional: true
762+
763+ '@rolldown/binding-darwin-arm64@1.2.15':
764+ optional: true
765+
766+ '@rolldown/binding-darwin-x64@1.2.15':
767+ optional: true
768+
769+ '@rolldown/binding-freebsd-x64@1.2.15':
770+ optional: true
771+
772+ '@rolldown/binding-linux-arm-gnueabihf@1.2.15':
773+ optional: true
774+
775+ '@rolldown/binding-linux-arm64-gnu@1.2.15':
776+ optional: true
777+
778+ '@rolldown/binding-linux-arm64-musl@1.2.15':
779+ optional: true
780+
781+ '@rolldown/binding-linux-ppc64-gnu@1.2.15':
782+ optional: true
783+
784+ '@rolldown/binding-linux-s390x-gnu@1.2.15':
785+ optional: true
786+
787+ '@rolldown/binding-linux-x64-gnu@1.2.15':
788+ optional: true
789+
790+ '@rolldown/binding-linux-x64-musl@1.2.15':
791+ optional: true
792+
793+ '@rolldown/binding-openharmony-arm64@1.2.15':
794+ optional: true
795+
796+ '@rolldown/binding-win32-arm64-msvc@1.2.15':
797+ optional: true
798+
799+ '@rolldown/binding-win32-x64-msvc@1.2.15':
800+ optional: true
801+
802+ '@rolldown/pluginutils@1.0.1': {}
803+
804+ '@sveltejs/acorn-typescript@1.0.13(acorn@8.19.0)':
805+ dependencies:
806+ acorn: 8.19.0
807+
808+ '@sveltejs/load-config@0.2.3': {}
809+
810+ '@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.3)(vite@8.3.5)':
811+ dependencies:
812+ deepmerge: 4.3.1
813+ magic-string: 1.4.5
814+ obug: 2.2.1
815+ svelte: 5.57.3
816+ vite: 8.3.5
817+ vitefu: 1.1.3(vite@8.3.5)
818+
819+ '@tsconfig/svelte@5.0.8': {}
820+
821+ '@types/chai@5.2.3':
822+ dependencies:
823+ '@types/deep-eql': 4.0.2
824+ assertion-error: 2.0.1
825+
826+ '@types/deep-eql@4.0.2': {}
827+
828+ '@types/estree@1.0.9': {}
829+
830+ '@vitest/mocker@5.0.4(vite@8.3.5)':
831+ dependencies:
832+ '@jridgewell/trace-mapping': 0.3.31
833+ '@vitest/spy': 5.0.4
834+ estree-walker: 3.0.3
835+ magic-string: 1.4.5
836+ optionalDependencies:
837+ vite: 8.3.5
838+
839+ '@vitest/spy@5.0.4': {}
840+
841+ '@wailsio/runtime@3.0.0': {}
842+
843+ acorn@8.19.0: {}
844+
845+ aria-query@5.3.1: {}
846+
847+ assertion-error@2.0.1: {}
848+
849+ axobject-query@4.1.0: {}
850+
851+ chai@6.3.0: {}
852+
853+ chokidar@4.0.3:
854+ dependencies:
855+ readdirp: 4.1.2
856+
857+ clsx@2.1.1: {}
858+
859+ deepmerge@4.3.1: {}
860+
861+ detect-libc@2.1.2: {}
862+
863+ devalue@5.9.4: {}
864+
865+ es-module-lexer@2.3.2: {}
866+
867+ esm-env@1.2.2: {}
868+
869+ esrap@2.4.0:
870+ dependencies:
871+ '@jridgewell/sourcemap-codec': 1.6.0
872+
873+ estree-walker@3.0.3:
874+ dependencies:
875+ '@types/estree': 1.0.9
876+
877+ expect-type@1.4.0: {}
878+
879+ fdir@6.5.0(picomatch@4.0.7):
880+ optionalDependencies:
881+ picomatch: 4.0.7
882+
883+ fsevents@2.3.3:
884+ optional: true
885+
886+ inline-style-parser@0.2.7: {}
887+
888+ is-reference@3.0.3:
889+ dependencies:
890+ '@types/estree': 1.0.9
891+
892+ lightningcss-android-arm64@1.33.0:
893+ optional: true
894+
895+ lightningcss-darwin-arm64@1.33.0:
896+ optional: true
897+
898+ lightningcss-darwin-x64@1.33.0:
899+ optional: true
900+
901+ lightningcss-freebsd-x64@1.33.0:
902+ optional: true
903+
904+ lightningcss-linux-arm-gnueabihf@1.33.0:
905+ optional: true
906+
907+ lightningcss-linux-arm64-gnu@1.33.0:
908+ optional: true
909+
910+ lightningcss-linux-arm64-musl@1.33.0:
911+ optional: true
912+
913+ lightningcss-linux-x64-gnu@1.33.0:
914+ optional: true
915+
916+ lightningcss-linux-x64-musl@1.33.0:
917+ optional: true
918+
919+ lightningcss-win32-arm64-msvc@1.33.0:
920+ optional: true
921+
922+ lightningcss-win32-x64-msvc@1.33.0:
923+ optional: true
924+
925+ lightningcss@1.33.0:
926+ dependencies:
927+ detect-libc: 2.1.2
928+ optionalDependencies:
929+ lightningcss-android-arm64: 1.33.0
930+ lightningcss-darwin-arm64: 1.33.0
931+ lightningcss-darwin-x64: 1.33.0
932+ lightningcss-freebsd-x64: 1.33.0
933+ lightningcss-linux-arm-gnueabihf: 1.33.0
934+ lightningcss-linux-arm64-gnu: 1.33.0
935+ lightningcss-linux-arm64-musl: 1.33.0
936+ lightningcss-linux-x64-gnu: 1.33.0
937+ lightningcss-linux-x64-musl: 1.33.0
938+ lightningcss-win32-arm64-msvc: 1.33.0
939+ lightningcss-win32-x64-msvc: 1.33.0
940+
941+ locate-character@3.0.0: {}
942+
943+ magic-string@0.30.21:
944+ dependencies:
945+ '@jridgewell/sourcemap-codec': 1.6.0
946+
947+ magic-string@1.4.5:
948+ dependencies:
949+ '@jridgewell/sourcemap-codec': 1.6.0
950+
951+ mri@1.2.0: {}
952+
953+ nanoid@3.3.20: {}
954+
955+ obug@2.2.1: {}
956+
957+ obug@3.0.0: {}
958+
959+ paneforge@1.0.2(svelte@5.57.3):
960+ dependencies:
961+ runed: 0.23.4(svelte@5.57.3)
962+ svelte: 5.57.3
963+ svelte-toolbelt: 0.9.3(svelte@5.57.3)
964+
965+ picocolors@1.1.1: {}
966+
967+ picomatch@4.0.7: {}
968+
969+ postcss@8.5.32:
970+ dependencies:
971+ nanoid: 3.3.20
972+ picocolors: 1.1.1
973+ source-map-js: 1.2.2
974+
975+ readdirp@4.1.2: {}
976+
977+ rolldown@1.2.15:
978+ dependencies:
979+ '@oxc-project/types': 0.155.0
980+ '@rolldown/pluginutils': 1.0.1
981+ optionalDependencies:
982+ '@rolldown/binding-android-arm-eabi': 1.2.15
983+ '@rolldown/binding-android-arm64': 1.2.15
984+ '@rolldown/binding-darwin-arm64': 1.2.15
985+ '@rolldown/binding-darwin-x64': 1.2.15
986+ '@rolldown/binding-freebsd-x64': 1.2.15
987+ '@rolldown/binding-linux-arm-gnueabihf': 1.2.15
988+ '@rolldown/binding-linux-arm64-gnu': 1.2.15
989+ '@rolldown/binding-linux-arm64-musl': 1.2.15
990+ '@rolldown/binding-linux-ppc64-gnu': 1.2.15
991+ '@rolldown/binding-linux-s390x-gnu': 1.2.15
992+ '@rolldown/binding-linux-x64-gnu': 1.2.15
993+ '@rolldown/binding-linux-x64-musl': 1.2.15
994+ '@rolldown/binding-openharmony-arm64': 1.2.15
995+ '@rolldown/binding-win32-arm64-msvc': 1.2.15
996+ '@rolldown/binding-win32-x64-msvc': 1.2.15
997+
998+ runed@0.23.4(svelte@5.57.3):
999+ dependencies:
1000+ esm-env: 1.2.2
1001+ svelte: 5.57.3
1002+
1003+ runed@0.29.2(svelte@5.57.3):
1004+ dependencies:
1005+ esm-env: 1.2.2
1006+ svelte: 5.57.3
1007+
1008+ sade@1.8.1:
1009+ dependencies:
1010+ mri: 1.2.0
1011+
1012+ source-map-js@1.2.2: {}
1013+
1014+ std-env@4.3.0: {}
1015+
1016+ style-to-object@1.0.14:
1017+ dependencies:
1018+ inline-style-parser: 0.2.7
1019+
1020+ svelte-check@4.7.7(picomatch@4.0.7)(svelte@5.57.3)(typescript@5.9.3):
1021+ dependencies:
1022+ '@jridgewell/trace-mapping': 0.3.31
1023+ '@sveltejs/load-config': 0.2.3
1024+ chokidar: 4.0.3
1025+ fdir: 6.5.0(picomatch@4.0.7)
1026+ picocolors: 1.1.1
1027+ sade: 1.8.1
1028+ svelte: 5.57.3
1029+ typescript: 5.9.3
1030+ transitivePeerDependencies:
1031+ - picomatch
1032+
1033+ svelte-toolbelt@0.9.3(svelte@5.57.3):
1034+ dependencies:
1035+ clsx: 2.1.1
1036+ runed: 0.29.2(svelte@5.57.3)
1037+ style-to-object: 1.0.14
1038+ svelte: 5.57.3
1039+
1040+ svelte@5.57.3:
1041+ dependencies:
1042+ '@jridgewell/remapping': 2.3.5
1043+ '@jridgewell/sourcemap-codec': 1.6.0
1044+ '@sveltejs/acorn-typescript': 1.0.13(acorn@8.19.0)
1045+ '@types/estree': 1.0.9
1046+ acorn: 8.19.0
1047+ aria-query: 5.3.1
1048+ axobject-query: 4.1.0
1049+ clsx: 2.1.1
1050+ devalue: 5.9.4
1051+ esm-env: 1.2.2
1052+ esrap: 2.4.0
1053+ is-reference: 3.0.3
1054+ locate-character: 3.0.0
1055+ magic-string: 0.30.21
1056+ zimmerframe: 1.1.5
1057+ transitivePeerDependencies:
1058+ - '@typescript-eslint/types'
1059+
1060+ tinybench@6.2.2: {}
1061+
1062+ tinyexec@1.3.1: {}
1063+
1064+ tinyglobby@0.2.17:
1065+ dependencies:
1066+ fdir: 6.5.0(picomatch@4.0.7)
1067+ picomatch: 4.0.7
1068+
1069+ tslib@2.8.1: {}
1070+
1071+ typescript@5.9.3: {}
1072+
1073+ vite@8.3.5:
1074+ dependencies:
1075+ lightningcss: 1.33.0
1076+ picomatch: 4.0.7
1077+ postcss: 8.5.32
1078+ rolldown: 1.2.15
1079+ tinyglobby: 0.2.17
1080+ optionalDependencies:
1081+ fsevents: 2.3.3
1082+
1083+ vitefu@1.1.3(vite@8.3.5):
1084+ optionalDependencies:
1085+ vite: 8.3.5
1086+
1087+ vitest@5.0.4(vite@8.3.5):
1088+ dependencies:
1089+ '@types/chai': 5.2.3
1090+ '@vitest/mocker': 5.0.4(vite@8.3.5)
1091+ chai: 6.3.0
1092+ es-module-lexer: 2.3.2
1093+ expect-type: 1.4.0
1094+ magic-string: 1.4.5
1095+ obug: 3.0.0
1096+ picomatch: 4.0.7
1097+ std-env: 4.3.0
1098+ tinybench: 6.2.2
1099+ tinyexec: 1.3.1
1100+ tinyglobby: 0.2.17
1101+ vite: 8.3.5
1102+ why-is-node-running: 3.2.1
1103+ transitivePeerDependencies:
1104+ - msw
1105+
1106+ why-is-node-running@3.2.1: {}
1107+
1108+ zimmerframe@1.1.5: {}
added frontend/pnpm-workspace.yaml +9 -0
new file mode 100644
@@ -0,0 +1,9 @@
1+# Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+#
3+# The repository folder is shared between macOS and a Linux sandbox. Installing
4+# the native binaries (rolldown, lightningcss, …) of both platforms lets one
5+# node_modules work on both machines.
6+supportedArchitectures:
7+ os: [darwin, linux]
8+ cpu: [arm64, x64]
9+ libc: [glibc]
new file mode 100644
@@ -0,0 +1,9 @@
1+# Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+#
3+# The repository folder is shared between macOS and a Linux sandbox. Installing
4+# the native binaries (rolldown, lightningcss, …) of both platforms lets one
5+# node_modules work on both machines.
6+supportedArchitectures:
7+ os: [darwin, linux]
8+ cpu: [arm64, x64]
9+ libc: [glibc]
added frontend/src/App.svelte +10 -0
new file mode 100644
@@ -0,0 +1,10 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import Shell from './lib/layout/Shell.svelte'
4+ import { panels } from './lib/panels'
5+ import { connect } from './lib/state.svelte'
6+
7+ $effect(() => connect())
8+</script>
9+
10+<Shell registry={panels} />
new file mode 100644
@@ -0,0 +1,10 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import Shell from './lib/layout/Shell.svelte'
4+ import { panels } from './lib/panels'
5+ import { connect } from './lib/state.svelte'
6+
7+ $effect(() => connect())
8+</script>
9+
10+<Shell registry={panels} />
added frontend/src/lib/events.test.ts +57 -0
new file mode 100644
@@ -0,0 +1,57 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+import { describe, expect, it, vi } from 'vitest'
4+
5+const handlers = new Map<string, (event: { data: unknown }) => void>()
6+const unsubscribe = vi.fn()
7+vi.mock('@wailsio/runtime', () => ({
8+ Events: {
9+ On: (name: string, handler: (event: { data: unknown }) => void) => {
10+ handlers.set(name, handler)
11+ return unsubscribe
12+ },
13+ },
14+}))
15+
16+const { createFrameBatcher, subscribe } = await import('./events')
17+
18+describe('subscribe', () => {
19+ it('hands the payload to the handler and returns the unsubscribe function', () => {
20+ const received: unknown[] = []
21+ const stop = subscribe<{ agent: string }>('agent:state', (payload) => received.push(payload))
22+ handlers.get('agent:state')!({ data: { agent: 'alice' } })
23+ expect(received).toEqual([{ agent: 'alice' }])
24+ stop()
25+ expect(unsubscribe).toHaveBeenCalledOnce()
26+ })
27+})
28+
29+describe('createFrameBatcher', () => {
30+ it('flushes a burst of items once, in order', () => {
31+ const frames: Array<() => void> = []
32+ const flushed: number[][] = []
33+ const batcher = createFrameBatcher<number>((items) => flushed.push(items), (callback) => frames.push(callback))
34+
35+ for (let i = 0; i < 500; i++) batcher.push(i)
36+ expect(frames).toHaveLength(1)
37+ expect(flushed).toHaveLength(0)
38+
39+ frames[0]()
40+ expect(flushed).toHaveLength(1)
41+ expect(flushed[0]).toHaveLength(500)
42+ expect(flushed[0][499]).toBe(499)
43+ })
44+
45+ it('schedules a new frame for items pushed after a flush', () => {
46+ const frames: Array<() => void> = []
47+ const flushed: string[][] = []
48+ const batcher = createFrameBatcher<string>((items) => flushed.push(items), (callback) => frames.push(callback))
49+
50+ batcher.push('a')
51+ frames[0]()
52+ batcher.push('b')
53+ expect(frames).toHaveLength(2)
54+ frames[1]()
55+ expect(flushed).toEqual([['a'], ['b']])
56+ })
57+})
new file mode 100644
@@ -0,0 +1,57 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+import { describe, expect, it, vi } from 'vitest'
4+
5+const handlers = new Map<string, (event: { data: unknown }) => void>()
6+const unsubscribe = vi.fn()
7+vi.mock('@wailsio/runtime', () => ({
8+ Events: {
9+ On: (name: string, handler: (event: { data: unknown }) => void) => {
10+ handlers.set(name, handler)
11+ return unsubscribe
12+ },
13+ },
14+}))
15+
16+const { createFrameBatcher, subscribe } = await import('./events')
17+
18+describe('subscribe', () => {
19+ it('hands the payload to the handler and returns the unsubscribe function', () => {
20+ const received: unknown[] = []
21+ const stop = subscribe<{ agent: string }>('agent:state', (payload) => received.push(payload))
22+ handlers.get('agent:state')!({ data: { agent: 'alice' } })
23+ expect(received).toEqual([{ agent: 'alice' }])
24+ stop()
25+ expect(unsubscribe).toHaveBeenCalledOnce()
26+ })
27+})
28+
29+describe('createFrameBatcher', () => {
30+ it('flushes a burst of items once, in order', () => {
31+ const frames: Array<() => void> = []
32+ const flushed: number[][] = []
33+ const batcher = createFrameBatcher<number>((items) => flushed.push(items), (callback) => frames.push(callback))
34+
35+ for (let i = 0; i < 500; i++) batcher.push(i)
36+ expect(frames).toHaveLength(1)
37+ expect(flushed).toHaveLength(0)
38+
39+ frames[0]()
40+ expect(flushed).toHaveLength(1)
41+ expect(flushed[0]).toHaveLength(500)
42+ expect(flushed[0][499]).toBe(499)
43+ })
44+
45+ it('schedules a new frame for items pushed after a flush', () => {
46+ const frames: Array<() => void> = []
47+ const flushed: string[][] = []
48+ const batcher = createFrameBatcher<string>((items) => flushed.push(items), (callback) => frames.push(callback))
49+
50+ batcher.push('a')
51+ frames[0]()
52+ batcher.push('b')
53+ expect(frames).toHaveLength(2)
54+ frames[1]()
55+ expect(flushed).toEqual([['a'], ['b']])
56+ })
57+})
added frontend/src/lib/events.ts +47 -0
new file mode 100644
@@ -0,0 +1,47 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// The Go → interface event bridge (issue #13): subscribing with automatic
4+// clean-up, and batching bursts of events into one update per frame.
5+
6+import { Events } from '@wailsio/runtime'
7+
8+/**
9+ * Subscribes to a Go event and returns the function that unsubscribes.
10+ * Return it from a Svelte `$effect` so the subscription ends with the component.
11+ *
12+ * @example
13+ * $effect(() => subscribe(AgentState, (payload) => updateAgent(payload)))
14+ */
15+export function subscribe<T>(name: string, handler: (payload: T) => void): () => void {
16+ return Events.On(name, (event: { data: T }) => handler(event.data))
17+}
18+
19+/**
20+ * Collects items and hands them over in one batch per animation frame, so a
21+ * burst of events (50 log lines, 500 streamed chunks per second) causes one
22+ * re-render instead of one per event.
23+ *
24+ * @example
25+ * const batcher = createFrameBatcher<string>((lines) => logs.push(...lines))
26+ * subscribe<AgentLogPayload>(AgentLog, (payload) => batcher.push(payload.line))
27+ */
28+export function createFrameBatcher<T>(
29+ flush: (items: T[]) => void,
30+ schedule: (callback: () => void) => unknown = (callback) => requestAnimationFrame(callback),
31+) {
32+ let pending: T[] = []
33+ let scheduled = false
34+ return {
35+ push(item: T) {
36+ pending.push(item)
37+ if (scheduled) return
38+ scheduled = true
39+ schedule(() => {
40+ scheduled = false
41+ const items = pending
42+ pending = []
43+ flush(items)
44+ })
45+ },
46+ }
47+}
new file mode 100644
@@ -0,0 +1,47 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// The Go → interface event bridge (issue #13): subscribing with automatic
4+// clean-up, and batching bursts of events into one update per frame.
5+
6+import { Events } from '@wailsio/runtime'
7+
8+/**
9+ * Subscribes to a Go event and returns the function that unsubscribes.
10+ * Return it from a Svelte `$effect` so the subscription ends with the component.
11+ *
12+ * @example
13+ * $effect(() => subscribe(AgentState, (payload) => updateAgent(payload)))
14+ */
15+export function subscribe<T>(name: string, handler: (payload: T) => void): () => void {
16+ return Events.On(name, (event: { data: T }) => handler(event.data))
17+}
18+
19+/**
20+ * Collects items and hands them over in one batch per animation frame, so a
21+ * burst of events (50 log lines, 500 streamed chunks per second) causes one
22+ * re-render instead of one per event.
23+ *
24+ * @example
25+ * const batcher = createFrameBatcher<string>((lines) => logs.push(...lines))
26+ * subscribe<AgentLogPayload>(AgentLog, (payload) => batcher.push(payload.line))
27+ */
28+export function createFrameBatcher<T>(
29+ flush: (items: T[]) => void,
30+ schedule: (callback: () => void) => unknown = (callback) => requestAnimationFrame(callback),
31+) {
32+ let pending: T[] = []
33+ let scheduled = false
34+ return {
35+ push(item: T) {
36+ pending.push(item)
37+ if (scheduled) return
38+ scheduled = true
39+ schedule(() => {
40+ scheduled = false
41+ const items = pending
42+ pending = []
43+ flush(items)
44+ })
45+ },
46+ }
47+}
added frontend/src/lib/layout/Dock.svelte +30 -0
new file mode 100644
@@ -0,0 +1,30 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import type { Dock, Registry } from '../panels/registry'
4+
5+ let { registry, dock, insetTop = false }: { registry: Registry; dock: Dock; insetTop?: boolean } = $props()
6+ const panels = $derived(registry.inDock(dock))
7+</script>
8+
9+<!-- A dock stacks its panels, each under its heading. insetTop leaves room for
10+ the macOS window buttons at the top of the window. -->
11+<div class="dock {dock}" class:inset={insetTop}>
12+ {#each panels as panel (panel.id)}
13+ <section aria-label={panel.title}>
14+ {#if dock !== 'center'}
15+ <h2>{panel.title}</h2>
16+ {/if}
17+ <panel.component />
18+ </section>
19+ {/each}
20+</div>
21+
22+<style>
23+ .dock { height: 100%; overflow-y: auto; background: var(--panel); display: flex; flex-direction: column; }
24+ .dock.center { background: var(--bg); overflow: hidden; }
25+ .dock.inset { padding-top: var(--titlebar-inset); }
26+ section { padding: 10px 6px 6px; }
27+ .center section { padding: 0; height: 100%; }
28+ .bottom section { flex: 1; display: flex; flex-direction: column; min-height: 0; }
29+ h2 { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin: 0 8px 6px; }
30+</style>
new file mode 100644
@@ -0,0 +1,30 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import type { Dock, Registry } from '../panels/registry'
4+
5+ let { registry, dock, insetTop = false }: { registry: Registry; dock: Dock; insetTop?: boolean } = $props()
6+ const panels = $derived(registry.inDock(dock))
7+</script>
8+
9+<!-- A dock stacks its panels, each under its heading. insetTop leaves room for
10+ the macOS window buttons at the top of the window. -->
11+<div class="dock {dock}" class:inset={insetTop}>
12+ {#each panels as panel (panel.id)}
13+ <section aria-label={panel.title}>
14+ {#if dock !== 'center'}
15+ <h2>{panel.title}</h2>
16+ {/if}
17+ <panel.component />
18+ </section>
19+ {/each}
20+</div>
21+
22+<style>
23+ .dock { height: 100%; overflow-y: auto; background: var(--panel); display: flex; flex-direction: column; }
24+ .dock.center { background: var(--bg); overflow: hidden; }
25+ .dock.inset { padding-top: var(--titlebar-inset); }
26+ section { padding: 10px 6px 6px; }
27+ .center section { padding: 0; height: 100%; }
28+ .bottom section { flex: 1; display: flex; flex-direction: column; min-height: 0; }
29+ h2 { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin: 0 8px 6px; }
30+</style>
added frontend/src/lib/layout/Shell.svelte +71 -0
new file mode 100644
@@ -0,0 +1,71 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import { Pane, PaneGroup, PaneResizer } from 'paneforge'
4+ import type { Registry } from '../panels/registry'
5+ import Dock from './Dock.svelte'
6+ import StatusBar from './StatusBar.svelte'
7+
8+ let { registry }: { registry: Registry } = $props()
9+
10+ // The docks that can be folded; the conversation in the centre cannot.
11+ let left: Pane | undefined = $state()
12+ let right: Pane | undefined = $state()
13+ let bottom: Pane | undefined = $state()
14+ let collapsed = $state({ left: false, right: true, bottom: true })
15+
16+ // A dock that starts folded has no previous size to return to: open it at
17+ // a comfortable size rather than at its minimum.
18+ const openSize = 25
19+
20+ function toggle(pane: Pane | undefined) {
21+ if (!pane) return
22+ if (!pane.isCollapsed()) {
23+ pane.collapse()
24+ return
25+ }
26+ pane.expand()
27+ if (pane.getSize() < openSize) pane.resize(openSize)
28+ }
29+</script>
30+
31+<!-- Layout of issue #12 / wireframe 1: the conversation in the centre, channels
32+ and agents on the left, Settings on the right and the logs at the bottom
33+ folded by default. autoSaveId keeps the sizes from one launch to the next. -->
34+<div class="shell">
35+ <div class="main">
36+ <PaneGroup direction="horizontal" autoSaveId="sidekicks-shell">
37+ <Pane bind:this={left} defaultSize={20} minSize={14} collapsible collapsedSize={0}
38+ onCollapse={() => (collapsed.left = true)} onExpand={() => (collapsed.left = false)}>
39+ <Dock {registry} dock="left" insetTop />
40+ </Pane>
41+ <PaneResizer class="resizer vertical" />
42+ <Pane defaultSize={55} minSize={30}>
43+ <PaneGroup direction="vertical" autoSaveId="sidekicks-shell-center">
44+ <Pane defaultSize={100} minSize={30}>
45+ <Dock {registry} dock="center" />
46+ </Pane>
47+ <PaneResizer class="resizer horizontal" />
48+ <Pane bind:this={bottom} defaultSize={0} minSize={12} collapsible collapsedSize={0}
49+ onCollapse={() => (collapsed.bottom = true)} onExpand={() => (collapsed.bottom = false)}>
50+ <Dock {registry} dock="bottom" />
51+ </Pane>
52+ </PaneGroup>
53+ </Pane>
54+ <PaneResizer class="resizer vertical" />
55+ <Pane bind:this={right} defaultSize={0} minSize={18} collapsible collapsedSize={0}
56+ onCollapse={() => (collapsed.right = true)} onExpand={() => (collapsed.right = false)}>
57+ <Dock {registry} dock="right" />
58+ </Pane>
59+ </PaneGroup>
60+ </div>
61+ <StatusBar {collapsed} onToggle={(dock) => toggle({ left, right, bottom }[dock])} />
62+</div>
63+
64+<style>
65+ .shell { height: 100%; display: flex; flex-direction: column; }
66+ .main { flex: 1; min-height: 0; }
67+ :global(.resizer) { background: var(--border); }
68+ :global(.resizer.vertical) { width: 1px; }
69+ :global(.resizer.horizontal) { height: 1px; }
70+ :global(.resizer:hover), :global(.resizer[data-active]) { background: var(--accent); }
71+</style>
new file mode 100644
@@ -0,0 +1,71 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import { Pane, PaneGroup, PaneResizer } from 'paneforge'
4+ import type { Registry } from '../panels/registry'
5+ import Dock from './Dock.svelte'
6+ import StatusBar from './StatusBar.svelte'
7+
8+ let { registry }: { registry: Registry } = $props()
9+
10+ // The docks that can be folded; the conversation in the centre cannot.
11+ let left: Pane | undefined = $state()
12+ let right: Pane | undefined = $state()
13+ let bottom: Pane | undefined = $state()
14+ let collapsed = $state({ left: false, right: true, bottom: true })
15+
16+ // A dock that starts folded has no previous size to return to: open it at
17+ // a comfortable size rather than at its minimum.
18+ const openSize = 25
19+
20+ function toggle(pane: Pane | undefined) {
21+ if (!pane) return
22+ if (!pane.isCollapsed()) {
23+ pane.collapse()
24+ return
25+ }
26+ pane.expand()
27+ if (pane.getSize() < openSize) pane.resize(openSize)
28+ }
29+</script>
30+
31+<!-- Layout of issue #12 / wireframe 1: the conversation in the centre, channels
32+ and agents on the left, Settings on the right and the logs at the bottom
33+ folded by default. autoSaveId keeps the sizes from one launch to the next. -->
34+<div class="shell">
35+ <div class="main">
36+ <PaneGroup direction="horizontal" autoSaveId="sidekicks-shell">
37+ <Pane bind:this={left} defaultSize={20} minSize={14} collapsible collapsedSize={0}
38+ onCollapse={() => (collapsed.left = true)} onExpand={() => (collapsed.left = false)}>
39+ <Dock {registry} dock="left" insetTop />
40+ </Pane>
41+ <PaneResizer class="resizer vertical" />
42+ <Pane defaultSize={55} minSize={30}>
43+ <PaneGroup direction="vertical" autoSaveId="sidekicks-shell-center">
44+ <Pane defaultSize={100} minSize={30}>
45+ <Dock {registry} dock="center" />
46+ </Pane>
47+ <PaneResizer class="resizer horizontal" />
48+ <Pane bind:this={bottom} defaultSize={0} minSize={12} collapsible collapsedSize={0}
49+ onCollapse={() => (collapsed.bottom = true)} onExpand={() => (collapsed.bottom = false)}>
50+ <Dock {registry} dock="bottom" />
51+ </Pane>
52+ </PaneGroup>
53+ </Pane>
54+ <PaneResizer class="resizer vertical" />
55+ <Pane bind:this={right} defaultSize={0} minSize={18} collapsible collapsedSize={0}
56+ onCollapse={() => (collapsed.right = true)} onExpand={() => (collapsed.right = false)}>
57+ <Dock {registry} dock="right" />
58+ </Pane>
59+ </PaneGroup>
60+ </div>
61+ <StatusBar {collapsed} onToggle={(dock) => toggle({ left, right, bottom }[dock])} />
62+</div>
63+
64+<style>
65+ .shell { height: 100%; display: flex; flex-direction: column; }
66+ .main { flex: 1; min-height: 0; }
67+ :global(.resizer) { background: var(--border); }
68+ :global(.resizer.vertical) { width: 1px; }
69+ :global(.resizer.horizontal) { height: 1px; }
70+ :global(.resizer:hover), :global(.resizer[data-active]) { background: var(--accent); }
71+</style>
added frontend/src/lib/layout/StatusBar.svelte +35 -0
new file mode 100644
@@ -0,0 +1,35 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import { runningCount, appState } from '../state.svelte'
4+
5+ type FoldableDock = 'left' | 'right' | 'bottom'
6+ let { collapsed, onToggle }: { collapsed: Record<FoldableDock, boolean>; onToggle: (dock: FoldableDock) => void } = $props()
7+
8+ const toggles: Array<{ dock: FoldableDock; label: string }> = [
9+ { dock: 'left', label: 'Agents' },
10+ { dock: 'bottom', label: 'Journal' },
11+ { dock: 'right', label: 'Settings' },
12+ ]
13+</script>
14+
15+<!-- Wireframe 1, note 5: the running agents (memory) and the open configuration. -->
16+<footer>
17+ <span class="status" class:invalid={appState.config && !appState.config.valid}>
18+ ● {runningCount()} agent{runningCount() > 1 ? 's' : ''} actif{runningCount() > 1 ? 's' : ''}
19+ · {appState.config?.path ?? '…'}
20+ </span>
21+ <span class="toggles">
22+ {#each toggles as toggle (toggle.dock)}
23+ <button class:on={!collapsed[toggle.dock]} aria-pressed={!collapsed[toggle.dock]} onclick={() => onToggle(toggle.dock)}>{toggle.label}</button>
24+ {/each}
25+ </span>
26+</footer>
27+
28+<style>
29+ footer { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 3px 10px; border-top: 1px solid var(--border); background: var(--panel-2); color: var(--muted); font-size: 12px; }
30+ .status { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
31+ .status.invalid { color: var(--danger); }
32+ .toggles { display: flex; gap: 4px; flex-shrink: 0; }
33+ button { font-size: 12px; padding: 1px 8px; background: none; border-color: transparent; color: var(--muted); }
34+ button.on { color: var(--text); border-color: var(--border); background: var(--panel); }
35+</style>
new file mode 100644
@@ -0,0 +1,35 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import { runningCount, appState } from '../state.svelte'
4+
5+ type FoldableDock = 'left' | 'right' | 'bottom'
6+ let { collapsed, onToggle }: { collapsed: Record<FoldableDock, boolean>; onToggle: (dock: FoldableDock) => void } = $props()
7+
8+ const toggles: Array<{ dock: FoldableDock; label: string }> = [
9+ { dock: 'left', label: 'Agents' },
10+ { dock: 'bottom', label: 'Journal' },
11+ { dock: 'right', label: 'Settings' },
12+ ]
13+</script>
14+
15+<!-- Wireframe 1, note 5: the running agents (memory) and the open configuration. -->
16+<footer>
17+ <span class="status" class:invalid={appState.config && !appState.config.valid}>
18+ ● {runningCount()} agent{runningCount() > 1 ? 's' : ''} actif{runningCount() > 1 ? 's' : ''}
19+ · {appState.config?.path ?? '…'}
20+ </span>
21+ <span class="toggles">
22+ {#each toggles as toggle (toggle.dock)}
23+ <button class:on={!collapsed[toggle.dock]} aria-pressed={!collapsed[toggle.dock]} onclick={() => onToggle(toggle.dock)}>{toggle.label}</button>
24+ {/each}
25+ </span>
26+</footer>
27+
28+<style>
29+ footer { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 3px 10px; border-top: 1px solid var(--border); background: var(--panel-2); color: var(--muted); font-size: 12px; }
30+ .status { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
31+ .status.invalid { color: var(--danger); }
32+ .toggles { display: flex; gap: 4px; flex-shrink: 0; }
33+ button { font-size: 12px; padding: 1px 8px; background: none; border-color: transparent; color: var(--muted); }
34+ button.on { color: var(--text); border-color: var(--border); background: var(--panel); }
35+</style>
added frontend/src/lib/panels/AgentsPanel.svelte +58 -0
new file mode 100644
@@ -0,0 +1,58 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import { AgentService } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/app'
4+ import { act, appState } from '../state.svelte'
5+
6+ const labels: Record<string, string> = {
7+ stopped: 'arrêté',
8+ starting: 'démarre…',
9+ ready: 'prêt',
10+ busy: 'occupé',
11+ error: 'erreur',
12+ }
13+</script>
14+
15+{#if appState.agents.length === 0}
16+ <p class="empty">Aucun agent. Ajoutez-en un dans <strong>Settings</strong>.</p>
17+{:else}
18+ <ul>
19+ {#each appState.agents as agent (agent.name)}
20+ <li title={agent.description || agent.title}>
21+ <span class="dot {agent.state}" aria-hidden="true"></span>
22+ <span class="name">{agent.icon} {agent.title}</span>
23+ <span class="state">{labels[agent.state] ?? agent.state}{agent.outdated ? ' · à redémarrer' : ''}</span>
24+ <span class="actions">
25+ {#if agent.state === 'stopped' || agent.state === 'error'}
26+ <button onclick={() => act(() => AgentService.Start(agent.name))} aria-label="Démarrer {agent.title}">▶</button>
27+ {:else}
28+ <button onclick={() => act(() => AgentService.Restart(agent.name))} aria-label="Redémarrer {agent.title}">↻</button>
29+ <button onclick={() => act(() => AgentService.Stop(agent.name))} aria-label="Arrêter {agent.title}">■</button>
30+ {/if}
31+ </span>
32+ {#if agent.error}
33+ <p class="error">{agent.error}</p>
34+ {/if}
35+ </li>
36+ {/each}
37+ </ul>
38+{/if}
39+{#if appState.actionError}
40+ <p class="error" role="alert">{appState.actionError}</p>
41+{/if}
42+
43+<style>
44+ ul { list-style: none; margin: 0; padding: 0; }
45+ li { display: grid; grid-template-columns: 14px 1fr auto; align-items: center; gap: 2px 6px; padding: 5px 8px; border-radius: 6px; }
46+ li:hover { background: var(--panel-2); }
47+ .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
48+ .dot.ready { background: var(--ok); }
49+ .dot.busy, .dot.starting { background: var(--warn); }
50+ .dot.error { background: var(--danger); }
51+ .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
52+ .state { grid-column: 2; color: var(--muted); font-size: 12px; }
53+ .actions { grid-column: 3; grid-row: 1 / span 2; display: flex; gap: 4px; opacity: 0; }
54+ li:hover .actions, li:focus-within .actions { opacity: 1; }
55+ .actions button { padding: 1px 7px; font-size: 11px; }
56+ .error { grid-column: 2 / span 2; margin: 2px 0 0; color: var(--danger); font-size: 12px; white-space: pre-wrap; }
57+ .empty { color: var(--muted); padding: 0 8px; }
58+</style>
new file mode 100644
@@ -0,0 +1,58 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import { AgentService } from '../../../bindings/git.rickub.com/bots-garden/sidekicks/internal/app'
4+ import { act, appState } from '../state.svelte'
5+
6+ const labels: Record<string, string> = {
7+ stopped: 'arrêté',
8+ starting: 'démarre…',
9+ ready: 'prêt',
10+ busy: 'occupé',
11+ error: 'erreur',
12+ }
13+</script>
14+
15+{#if appState.agents.length === 0}
16+ <p class="empty">Aucun agent. Ajoutez-en un dans <strong>Settings</strong>.</p>
17+{:else}
18+ <ul>
19+ {#each appState.agents as agent (agent.name)}
20+ <li title={agent.description || agent.title}>
21+ <span class="dot {agent.state}" aria-hidden="true"></span>
22+ <span class="name">{agent.icon} {agent.title}</span>
23+ <span class="state">{labels[agent.state] ?? agent.state}{agent.outdated ? ' · à redémarrer' : ''}</span>
24+ <span class="actions">
25+ {#if agent.state === 'stopped' || agent.state === 'error'}
26+ <button onclick={() => act(() => AgentService.Start(agent.name))} aria-label="Démarrer {agent.title}">▶</button>
27+ {:else}
28+ <button onclick={() => act(() => AgentService.Restart(agent.name))} aria-label="Redémarrer {agent.title}">↻</button>
29+ <button onclick={() => act(() => AgentService.Stop(agent.name))} aria-label="Arrêter {agent.title}">■</button>
30+ {/if}
31+ </span>
32+ {#if agent.error}
33+ <p class="error">{agent.error}</p>
34+ {/if}
35+ </li>
36+ {/each}
37+ </ul>
38+{/if}
39+{#if appState.actionError}
40+ <p class="error" role="alert">{appState.actionError}</p>
41+{/if}
42+
43+<style>
44+ ul { list-style: none; margin: 0; padding: 0; }
45+ li { display: grid; grid-template-columns: 14px 1fr auto; align-items: center; gap: 2px 6px; padding: 5px 8px; border-radius: 6px; }
46+ li:hover { background: var(--panel-2); }
47+ .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
48+ .dot.ready { background: var(--ok); }
49+ .dot.busy, .dot.starting { background: var(--warn); }
50+ .dot.error { background: var(--danger); }
51+ .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
52+ .state { grid-column: 2; color: var(--muted); font-size: 12px; }
53+ .actions { grid-column: 3; grid-row: 1 / span 2; display: flex; gap: 4px; opacity: 0; }
54+ li:hover .actions, li:focus-within .actions { opacity: 1; }
55+ .actions button { padding: 1px 7px; font-size: 11px; }
56+ .error { grid-column: 2 / span 2; margin: 2px 0 0; color: var(--danger); font-size: 12px; white-space: pre-wrap; }
57+ .empty { color: var(--muted); padding: 0 8px; }
58+</style>
added frontend/src/lib/panels/ChannelsPanel.svelte +29 -0
new file mode 100644
@@ -0,0 +1,29 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import { appState } from '../state.svelte'
4+
5+ let selected = $state('')
6+ const channels = $derived(appState.config?.channels ?? [])
7+</script>
8+
9+{#if channels.length === 0}
10+ <p class="empty">Aucun channel déclaré.</p>
11+{:else}
12+ <ul>
13+ {#each channels as channel (channel.name)}
14+ <li>
15+ <button class:active={selected === channel.name} onclick={() => (selected = channel.name)} title={channel.description}>
16+ # {channel.name}
17+ </button>
18+ </li>
19+ {/each}
20+ </ul>
21+{/if}
22+
23+<style>
24+ ul { list-style: none; margin: 0; padding: 0; }
25+ button { display: block; width: 100%; text-align: left; border: none; background: none; padding: 5px 8px; border-radius: 6px; }
26+ button:hover, button.active { background: var(--panel-2); }
27+ button.active { font-weight: 600; }
28+ .empty { color: var(--muted); padding: 0 8px; }
29+</style>
new file mode 100644
@@ -0,0 +1,29 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import { appState } from '../state.svelte'
4+
5+ let selected = $state('')
6+ const channels = $derived(appState.config?.channels ?? [])
7+</script>
8+
9+{#if channels.length === 0}
10+ <p class="empty">Aucun channel déclaré.</p>
11+{:else}
12+ <ul>
13+ {#each channels as channel (channel.name)}
14+ <li>
15+ <button class:active={selected === channel.name} onclick={() => (selected = channel.name)} title={channel.description}>
16+ # {channel.name}
17+ </button>
18+ </li>
19+ {/each}
20+ </ul>
21+{/if}
22+
23+<style>
24+ ul { list-style: none; margin: 0; padding: 0; }
25+ button { display: block; width: 100%; text-align: left; border: none; background: none; padding: 5px 8px; border-radius: 6px; }
26+ button:hover, button.active { background: var(--panel-2); }
27+ button.active { font-weight: 600; }
28+ .empty { color: var(--muted); padding: 0 8px; }
29+</style>
added frontend/src/lib/panels/ConversationPanel.svelte +30 -0
new file mode 100644
@@ -0,0 +1,30 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import { appState } from '../state.svelte'
4+</script>
5+
6+<!-- Conversations arrive with milestone M2 (issues #14 to #17); until then the
7+ centre explains where things stand. -->
8+<section>
9+ {#if appState.config && !appState.config.valid}
10+ <div class="banner" role="alert">
11+ <strong>La configuration contient des erreurs.</strong>
12+ Ouvrez <strong>Settings</strong> pour les voir ; Sidekicks continue avec la dernière configuration valide.
13+ </div>
14+ {/if}
15+ <div class="welcome">
16+ <h1>Sidekicks</h1>
17+ {#if appState.agents.length === 0}
18+ <p>Commencez par ajouter un agent dans le fichier de configuration (panneau <strong>Settings</strong>).</p>
19+ {:else}
20+ <p>{appState.agents.length} agent{appState.agents.length > 1 ? 's' : ''} configuré{appState.agents.length > 1 ? 's' : ''}. Les conversations arrivent bientôt ; en attendant, démarrez un agent depuis le panneau <strong>Agents</strong>.</p>
21+ {/if}
22+ </div>
23+</section>
24+
25+<style>
26+ section { height: 100%; display: flex; flex-direction: column; background: var(--bg); }
27+ .banner { margin: 12px; padding: 10px 14px; border: 1px solid var(--danger); border-radius: var(--radius); color: var(--danger); }
28+ .welcome { margin: auto; max-width: 460px; text-align: center; color: var(--muted); padding: 24px; }
29+ h1 { color: var(--text); font-size: 22px; margin: 0 0 8px; }
30+</style>
new file mode 100644
@@ -0,0 +1,30 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import { appState } from '../state.svelte'
4+</script>
5+
6+<!-- Conversations arrive with milestone M2 (issues #14 to #17); until then the
7+ centre explains where things stand. -->
8+<section>
9+ {#if appState.config && !appState.config.valid}
10+ <div class="banner" role="alert">
11+ <strong>La configuration contient des erreurs.</strong>
12+ Ouvrez <strong>Settings</strong> pour les voir ; Sidekicks continue avec la dernière configuration valide.
13+ </div>
14+ {/if}
15+ <div class="welcome">
16+ <h1>Sidekicks</h1>
17+ {#if appState.agents.length === 0}
18+ <p>Commencez par ajouter un agent dans le fichier de configuration (panneau <strong>Settings</strong>).</p>
19+ {:else}
20+ <p>{appState.agents.length} agent{appState.agents.length > 1 ? 's' : ''} configuré{appState.agents.length > 1 ? 's' : ''}. Les conversations arrivent bientôt ; en attendant, démarrez un agent depuis le panneau <strong>Agents</strong>.</p>
21+ {/if}
22+ </div>
23+</section>
24+
25+<style>
26+ section { height: 100%; display: flex; flex-direction: column; background: var(--bg); }
27+ .banner { margin: 12px; padding: 10px 14px; border: 1px solid var(--danger); border-radius: var(--radius); color: var(--danger); }
28+ .welcome { margin: auto; max-width: 460px; text-align: center; color: var(--muted); padding: 24px; }
29+ h1 { color: var(--text); font-size: 22px; margin: 0 0 8px; }
30+</style>
added frontend/src/lib/panels/LogsPanel.svelte +28 -0
new file mode 100644
@@ -0,0 +1,28 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import { appState } from '../state.svelte'
4+
5+ let selected = $state('')
6+ const agentName = $derived(selected || appState.agents[0]?.name || '')
7+ const lines = $derived(appState.logs[agentName] ?? [])
8+</script>
9+
10+<div class="logs">
11+ <label>
12+ Journal de
13+ <select value={agentName} onchange={(event) => (selected = event.currentTarget.value)}>
14+ {#each appState.agents as agent (agent.name)}
15+ <option value={agent.name}>{agent.title}</option>
16+ {/each}
17+ </select>
18+ </label>
19+ <pre>{#if lines.length === 0}<span class="empty">Rien pour l'instant : le journal se remplit quand l'agent démarre.</span>{:else}{lines.join('\n')}{/if}</pre>
20+</div>
21+
22+<style>
23+ .logs { height: 100%; display: flex; flex-direction: column; }
24+ label { padding: 0 8px 6px; color: var(--muted); font-size: 12px; }
25+ select { font: inherit; color: var(--text); background: var(--panel-2); border: 1px solid var(--border); border-radius: 4px; }
26+ pre { flex: 1; margin: 0; padding: 8px; overflow: auto; font-family: var(--mono); font-size: 12px; background: var(--panel-2); white-space: pre-wrap; }
27+ .empty { color: var(--muted); }
28+</style>
new file mode 100644
@@ -0,0 +1,28 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import { appState } from '../state.svelte'
4+
5+ let selected = $state('')
6+ const agentName = $derived(selected || appState.agents[0]?.name || '')
7+ const lines = $derived(appState.logs[agentName] ?? [])
8+</script>
9+
10+<div class="logs">
11+ <label>
12+ Journal de
13+ <select value={agentName} onchange={(event) => (selected = event.currentTarget.value)}>
14+ {#each appState.agents as agent (agent.name)}
15+ <option value={agent.name}>{agent.title}</option>
16+ {/each}
17+ </select>
18+ </label>
19+ <pre>{#if lines.length === 0}<span class="empty">Rien pour l'instant : le journal se remplit quand l'agent démarre.</span>{:else}{lines.join('\n')}{/if}</pre>
20+</div>
21+
22+<style>
23+ .logs { height: 100%; display: flex; flex-direction: column; }
24+ label { padding: 0 8px 6px; color: var(--muted); font-size: 12px; }
25+ select { font: inherit; color: var(--text); background: var(--panel-2); border: 1px solid var(--border); border-radius: 4px; }
26+ pre { flex: 1; margin: 0; padding: 8px; overflow: auto; font-family: var(--mono); font-size: 12px; background: var(--panel-2); white-space: pre-wrap; }
27+ .empty { color: var(--muted); }
28+</style>
added frontend/src/lib/panels/SettingsPanel.svelte +53 -0
new file mode 100644
@@ -0,0 +1,53 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import { appState } from '../state.svelte'
4+
5+ const sources: Record<string, string> = {
6+ flag: 'option --config',
7+ environment: 'variable SIDEKICKS_CONFIG',
8+ 'last-opened': 'dernier fichier ouvert',
9+ default: 'emplacement par défaut',
10+ }
11+</script>
12+
13+<!-- The YAML editor arrives with issue #18 (milestone M2). -->
14+{#if appState.config}
15+ <dl>
16+ <dt>Fichier</dt>
17+ <dd class="path">{appState.config.path}</dd>
18+ <dt>Choisi par</dt>
19+ <dd>{sources[appState.config.source] ?? appState.config.source}{appState.config.created ? ' — créé pour vous' : ''}</dd>
20+ </dl>
21+ {#if appState.config.valid}
22+ <p class="ok">✓ Configuration valide</p>
23+ {:else}
24+ <p class="ko">La configuration n'est pas valide :</p>
25+ <ul class="problems">
26+ {#each appState.config.problems ?? [] as problem}
27+ <li>{problem}</li>
28+ {/each}
29+ </ul>
30+ {/if}
31+ {#if (appState.config.warnings ?? []).length > 0}
32+ <ul class="warnings">
33+ {#each appState.config.warnings ?? [] as warning}
34+ <li>⚠ {warning}</li>
35+ {/each}
36+ </ul>
37+ {/if}
38+ <p class="hint">Modifiez ce fichier avec votre éditeur : Sidekicks le recharge à chaque enregistrement.</p>
39+{/if}
40+
41+<style>
42+ dl { margin: 0 8px; display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; font-size: 13px; }
43+ dt { color: var(--muted); }
44+ dd { margin: 0; }
45+ .path { font-family: var(--mono); font-size: 12px; word-break: break-all; }
46+ .ok { color: var(--ok); margin: 12px 8px; font-weight: 600; }
47+ .ko { color: var(--danger); margin: 12px 8px 4px; font-weight: 600; }
48+ ul { margin: 0 8px; padding-left: 18px; font-size: 13px; }
49+ .problems li { color: var(--danger); margin-bottom: 4px; }
50+ .warnings { margin-top: 8px; list-style: none; padding-left: 0; }
51+ .warnings li { color: var(--warn); margin-bottom: 4px; }
52+ .hint { color: var(--muted); font-size: 12px; margin: 16px 8px; }
53+</style>
new file mode 100644
@@ -0,0 +1,53 @@
1+<!-- Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE -->
2+<script lang="ts">
3+ import { appState } from '../state.svelte'
4+
5+ const sources: Record<string, string> = {
6+ flag: 'option --config',
7+ environment: 'variable SIDEKICKS_CONFIG',
8+ 'last-opened': 'dernier fichier ouvert',
9+ default: 'emplacement par défaut',
10+ }
11+</script>
12+
13+<!-- The YAML editor arrives with issue #18 (milestone M2). -->
14+{#if appState.config}
15+ <dl>
16+ <dt>Fichier</dt>
17+ <dd class="path">{appState.config.path}</dd>
18+ <dt>Choisi par</dt>
19+ <dd>{sources[appState.config.source] ?? appState.config.source}{appState.config.created ? ' — créé pour vous' : ''}</dd>
20+ </dl>
21+ {#if appState.config.valid}
22+ <p class="ok">✓ Configuration valide</p>
23+ {:else}
24+ <p class="ko">La configuration n'est pas valide :</p>
25+ <ul class="problems">
26+ {#each appState.config.problems ?? [] as problem}
27+ <li>{problem}</li>
28+ {/each}
29+ </ul>
30+ {/if}
31+ {#if (appState.config.warnings ?? []).length > 0}
32+ <ul class="warnings">
33+ {#each appState.config.warnings ?? [] as warning}
34+ <li>⚠ {warning}</li>
35+ {/each}
36+ </ul>
37+ {/if}
38+ <p class="hint">Modifiez ce fichier avec votre éditeur : Sidekicks le recharge à chaque enregistrement.</p>
39+{/if}
40+
41+<style>
42+ dl { margin: 0 8px; display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; font-size: 13px; }
43+ dt { color: var(--muted); }
44+ dd { margin: 0; }
45+ .path { font-family: var(--mono); font-size: 12px; word-break: break-all; }
46+ .ok { color: var(--ok); margin: 12px 8px; font-weight: 600; }
47+ .ko { color: var(--danger); margin: 12px 8px 4px; font-weight: 600; }
48+ ul { margin: 0 8px; padding-left: 18px; font-size: 13px; }
49+ .problems li { color: var(--danger); margin-bottom: 4px; }
50+ .warnings { margin-top: 8px; list-style: none; padding-left: 0; }
51+ .warnings li { color: var(--warn); margin-bottom: 4px; }
52+ .hint { color: var(--muted); font-size: 12px; margin: 16px 8px; }
53+</style>
added frontend/src/lib/panels/index.ts +19 -0
new file mode 100644
@@ -0,0 +1,19 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// The panels of the application. A new feature adds its panel here; the shell
4+// does not change.
5+
6+import { createRegistry } from './registry'
7+import AgentsPanel from './AgentsPanel.svelte'
8+import ChannelsPanel from './ChannelsPanel.svelte'
9+import ConversationPanel from './ConversationPanel.svelte'
10+import LogsPanel from './LogsPanel.svelte'
11+import SettingsPanel from './SettingsPanel.svelte'
12+
13+export const panels = createRegistry()
14+
15+panels.register({ id: 'channels', title: 'Channels', dock: 'left', order: 10, component: ChannelsPanel })
16+panels.register({ id: 'agents', title: 'Agents', dock: 'left', order: 20, component: AgentsPanel })
17+panels.register({ id: 'conversation', title: 'Conversation', dock: 'center', order: 10, component: ConversationPanel })
18+panels.register({ id: 'settings', title: 'Settings', dock: 'right', order: 10, component: SettingsPanel })
19+panels.register({ id: 'logs', title: 'Journal', dock: 'bottom', order: 10, component: LogsPanel })
new file mode 100644
@@ -0,0 +1,19 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// The panels of the application. A new feature adds its panel here; the shell
4+// does not change.
5+
6+import { createRegistry } from './registry'
7+import AgentsPanel from './AgentsPanel.svelte'
8+import ChannelsPanel from './ChannelsPanel.svelte'
9+import ConversationPanel from './ConversationPanel.svelte'
10+import LogsPanel from './LogsPanel.svelte'
11+import SettingsPanel from './SettingsPanel.svelte'
12+
13+export const panels = createRegistry()
14+
15+panels.register({ id: 'channels', title: 'Channels', dock: 'left', order: 10, component: ChannelsPanel })
16+panels.register({ id: 'agents', title: 'Agents', dock: 'left', order: 20, component: AgentsPanel })
17+panels.register({ id: 'conversation', title: 'Conversation', dock: 'center', order: 10, component: ConversationPanel })
18+panels.register({ id: 'settings', title: 'Settings', dock: 'right', order: 10, component: SettingsPanel })
19+panels.register({ id: 'logs', title: 'Journal', dock: 'bottom', order: 10, component: LogsPanel })
added frontend/src/lib/panels/registry.test.ts +26 -0
new file mode 100644
@@ -0,0 +1,26 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+import { describe, expect, it } from 'vitest'
4+import type { Component } from 'svelte'
5+import { createRegistry } from './registry'
6+
7+const component = (() => {}) as unknown as Component
8+
9+describe('panel registry', () => {
10+ it('returns the panels of a dock, ordered', () => {
11+ const registry = createRegistry()
12+ registry.register({ id: 'agents', title: 'Agents', dock: 'left', order: 20, component })
13+ registry.register({ id: 'channels', title: 'Channels', dock: 'left', order: 10, component })
14+ registry.register({ id: 'settings', title: 'Settings', dock: 'right', order: 10, component })
15+
16+ expect(registry.inDock('left').map((panel) => panel.id)).toEqual(['channels', 'agents'])
17+ expect(registry.inDock('right').map((panel) => panel.id)).toEqual(['settings'])
18+ expect(registry.inDock('bottom')).toEqual([])
19+ })
20+
21+ it('refuses two panels with the same id', () => {
22+ const registry = createRegistry()
23+ registry.register({ id: 'agents', title: 'Agents', dock: 'left', order: 1, component })
24+ expect(() => registry.register({ id: 'agents', title: 'Again', dock: 'right', order: 1, component })).toThrow(/already registered/)
25+ })
26+})
new file mode 100644
@@ -0,0 +1,26 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+import { describe, expect, it } from 'vitest'
4+import type { Component } from 'svelte'
5+import { createRegistry } from './registry'
6+
7+const component = (() => {}) as unknown as Component
8+
9+describe('panel registry', () => {
10+ it('returns the panels of a dock, ordered', () => {
11+ const registry = createRegistry()
12+ registry.register({ id: 'agents', title: 'Agents', dock: 'left', order: 20, component })
13+ registry.register({ id: 'channels', title: 'Channels', dock: 'left', order: 10, component })
14+ registry.register({ id: 'settings', title: 'Settings', dock: 'right', order: 10, component })
15+
16+ expect(registry.inDock('left').map((panel) => panel.id)).toEqual(['channels', 'agents'])
17+ expect(registry.inDock('right').map((panel) => panel.id)).toEqual(['settings'])
18+ expect(registry.inDock('bottom')).toEqual([])
19+ })
20+
21+ it('refuses two panels with the same id', () => {
22+ const registry = createRegistry()
23+ registry.register({ id: 'agents', title: 'Agents', dock: 'left', order: 1, component })
24+ expect(() => registry.register({ id: 'agents', title: 'Again', dock: 'right', order: 1, component })).toThrow(/already registered/)
25+ })
26+})
added frontend/src/lib/panels/registry.ts +46 -0
new file mode 100644
@@ -0,0 +1,46 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// The panel registry (issue #12): a feature declares its panel and the dock it
4+// lives in; the shell renders each dock from the registry, so adding a panel
5+// never means editing the layout.
6+
7+import type { Component } from 'svelte'
8+
9+/** Where a panel lives: the docks around the central area, or the centre. */
10+export type Dock = 'left' | 'center' | 'right' | 'bottom'
11+
12+export interface PanelDefinition {
13+ /** Unique identifier, e.g. "agents". */
14+ id: string
15+ /** Heading shown above the panel. */
16+ title: string
17+ dock: Dock
18+ /** Smaller first within a dock. */
19+ order: number
20+ component: Component
21+}
22+
23+/**
24+ * Creates an empty registry.
25+ *
26+ * @example
27+ * const registry = createRegistry()
28+ * registry.register({ id: 'agents', title: 'Agents', dock: 'left', order: 20, component: AgentsPanel })
29+ * registry.inDock('left') // → [ { id: 'agents', … } ]
30+ */
31+export function createRegistry() {
32+ const panels = new Map<string, PanelDefinition>()
33+ return {
34+ register(panel: PanelDefinition) {
35+ if (panels.has(panel.id)) {
36+ throw new Error(`panel "${panel.id}" is already registered`)
37+ }
38+ panels.set(panel.id, panel)
39+ },
40+ inDock(dock: Dock): PanelDefinition[] {
41+ return [...panels.values()].filter((panel) => panel.dock === dock).sort((a, b) => a.order - b.order)
42+ },
43+ }
44+}
45+
46+export type Registry = ReturnType<typeof createRegistry>
new file mode 100644
@@ -0,0 +1,46 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// The panel registry (issue #12): a feature declares its panel and the dock it
4+// lives in; the shell renders each dock from the registry, so adding a panel
5+// never means editing the layout.
6+
7+import type { Component } from 'svelte'
8+
9+/** Where a panel lives: the docks around the central area, or the centre. */
10+export type Dock = 'left' | 'center' | 'right' | 'bottom'
11+
12+export interface PanelDefinition {
13+ /** Unique identifier, e.g. "agents". */
14+ id: string
15+ /** Heading shown above the panel. */
16+ title: string
17+ dock: Dock
18+ /** Smaller first within a dock. */
19+ order: number
20+ component: Component
21+}
22+
23+/**
24+ * Creates an empty registry.
25+ *
26+ * @example
27+ * const registry = createRegistry()
28+ * registry.register({ id: 'agents', title: 'Agents', dock: 'left', order: 20, component: AgentsPanel })
29+ * registry.inDock('left') // → [ { id: 'agents', … } ]
30+ */
31+export function createRegistry() {
32+ const panels = new Map<string, PanelDefinition>()
33+ return {
34+ register(panel: PanelDefinition) {
35+ if (panels.has(panel.id)) {
36+ throw new Error(`panel "${panel.id}" is already registered`)
37+ }
38+ panels.set(panel.id, panel)
39+ },
40+ inDock(dock: Dock): PanelDefinition[] {
41+ return [...panels.values()].filter((panel) => panel.dock === dock).sort((a, b) => a.order - b.order)
42+ },
43+ }
44+}
45+
46+export type Registry = ReturnType<typeof createRegistry>
added frontend/src/lib/state.svelte.ts +77 -0
new file mode 100644
@@ -0,0 +1,77 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// The state shared by the panels: the configuration and the agents, loaded
4+// from the Go services and kept up to date by the Go events.
5+
6+import { AgentService, ConfigService } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/app'
7+import type { ConfigView } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/app'
8+import type { Status } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/agents'
9+import type { AgentLogPayload, AgentStatePayload } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/events'
10+import { AgentLog, AgentState, ConfigChanged } from '../../bindings/events'
11+import { createFrameBatcher, subscribe } from './events'
12+import { applyTheme } from './theme'
13+
14+const maxLogLines = 500
15+
16+export const appState = $state({
17+ config: null as ConfigView | null,
18+ agents: [] as Status[],
19+ /** The last stderr lines of each agent, by agent name. */
20+ logs: {} as Record<string, string[]>,
21+ /** The last error returned by an action, shown to the user. */
22+ actionError: '',
23+})
24+
25+async function refreshConfig() {
26+ appState.config = await ConfigService.Current()
27+ applyTheme(document.documentElement, appState.config.theme)
28+}
29+
30+async function refreshAgents() {
31+ appState.agents = (await AgentService.List()) ?? []
32+}
33+
34+/** Runs an agent action and shows its error instead of throwing. */
35+export async function act(action: () => Promise<void>) {
36+ appState.actionError = ''
37+ try {
38+ await action()
39+ } catch (error) {
40+ appState.actionError = error instanceof Error ? error.message : String(error)
41+ }
42+}
43+
44+/** Loads the state and follows the Go events; returns the clean-up function. */
45+export function connect(): () => void {
46+ void refreshConfig()
47+ void refreshAgents()
48+
49+ const logBatcher = createFrameBatcher<AgentLogPayload>((payloads) => {
50+ for (const { agent, line } of payloads) {
51+ const lines = [...(appState.logs[agent] ?? []), line]
52+ appState.logs[agent] = lines.slice(-maxLogLines)
53+ }
54+ })
55+ const stops = [
56+ subscribe(ConfigChanged, () => {
57+ void refreshConfig()
58+ void refreshAgents()
59+ }),
60+ subscribe<AgentStatePayload>(AgentState, (payload) => {
61+ const agent = appState.agents.find((candidate) => candidate.name === payload.agent)
62+ if (!agent) {
63+ void refreshAgents()
64+ return
65+ }
66+ agent.state = payload.state as Status['state']
67+ agent.error = payload.error ?? ''
68+ }),
69+ subscribe<AgentLogPayload>(AgentLog, (payload) => logBatcher.push(payload)),
70+ ]
71+ return () => stops.forEach((stop) => stop())
72+}
73+
74+/** Agents running now: starting, ready or busy. */
75+export function runningCount(): number {
76+ return appState.agents.filter((agent) => ['starting', 'ready', 'busy'].includes(agent.state)).length
77+}
new file mode 100644
@@ -0,0 +1,77 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// The state shared by the panels: the configuration and the agents, loaded
4+// from the Go services and kept up to date by the Go events.
5+
6+import { AgentService, ConfigService } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/app'
7+import type { ConfigView } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/app'
8+import type { Status } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/agents'
9+import type { AgentLogPayload, AgentStatePayload } from '../../bindings/git.rickub.com/bots-garden/sidekicks/internal/events'
10+import { AgentLog, AgentState, ConfigChanged } from '../../bindings/events'
11+import { createFrameBatcher, subscribe } from './events'
12+import { applyTheme } from './theme'
13+
14+const maxLogLines = 500
15+
16+export const appState = $state({
17+ config: null as ConfigView | null,
18+ agents: [] as Status[],
19+ /** The last stderr lines of each agent, by agent name. */
20+ logs: {} as Record<string, string[]>,
21+ /** The last error returned by an action, shown to the user. */
22+ actionError: '',
23+})
24+
25+async function refreshConfig() {
26+ appState.config = await ConfigService.Current()
27+ applyTheme(document.documentElement, appState.config.theme)
28+}
29+
30+async function refreshAgents() {
31+ appState.agents = (await AgentService.List()) ?? []
32+}
33+
34+/** Runs an agent action and shows its error instead of throwing. */
35+export async function act(action: () => Promise<void>) {
36+ appState.actionError = ''
37+ try {
38+ await action()
39+ } catch (error) {
40+ appState.actionError = error instanceof Error ? error.message : String(error)
41+ }
42+}
43+
44+/** Loads the state and follows the Go events; returns the clean-up function. */
45+export function connect(): () => void {
46+ void refreshConfig()
47+ void refreshAgents()
48+
49+ const logBatcher = createFrameBatcher<AgentLogPayload>((payloads) => {
50+ for (const { agent, line } of payloads) {
51+ const lines = [...(appState.logs[agent] ?? []), line]
52+ appState.logs[agent] = lines.slice(-maxLogLines)
53+ }
54+ })
55+ const stops = [
56+ subscribe(ConfigChanged, () => {
57+ void refreshConfig()
58+ void refreshAgents()
59+ }),
60+ subscribe<AgentStatePayload>(AgentState, (payload) => {
61+ const agent = appState.agents.find((candidate) => candidate.name === payload.agent)
62+ if (!agent) {
63+ void refreshAgents()
64+ return
65+ }
66+ agent.state = payload.state as Status['state']
67+ agent.error = payload.error ?? ''
68+ }),
69+ subscribe<AgentLogPayload>(AgentLog, (payload) => logBatcher.push(payload)),
70+ ]
71+ return () => stops.forEach((stop) => stop())
72+}
73+
74+/** Agents running now: starting, ready or busy. */
75+export function runningCount(): number {
76+ return appState.agents.filter((agent) => ['starting', 'ready', 'busy'].includes(agent.state)).length
77+}
added frontend/src/lib/theme.test.ts +58 -0
new file mode 100644
@@ -0,0 +1,58 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+import { describe, expect, it } from 'vitest'
4+import { applyTheme, isMac, markPlatform } from './theme'
5+
6+function element() {
7+ const classes = new Set<string>()
8+ return {
9+ dataset: {} as Record<string, string>,
10+ classList: { toggle: (name: string, on: boolean) => (on ? classes.add(name) : classes.delete(name)), contains: (name: string) => classes.has(name) },
11+ } as unknown as HTMLElement
12+}
13+
14+describe('applyTheme', () => {
15+ it('forces light and dark', () => {
16+ const root = element()
17+ applyTheme(root, 'dark')
18+ expect(root.dataset.theme).toBe('dark')
19+ applyTheme(root, 'light')
20+ expect(root.dataset.theme).toBe('light')
21+ })
22+
23+ it('follows the system otherwise', () => {
24+ const root = element()
25+ applyTheme(root, 'dark')
26+ applyTheme(root, 'system')
27+ expect(root.dataset.theme).toBeUndefined()
28+ applyTheme(root, 'blue')
29+ expect(root.dataset.theme).toBeUndefined()
30+ })
31+})
32+
33+describe('markPlatform', () => {
34+ it('adds and removes the macos class', () => {
35+ const root = element()
36+ markPlatform(root, true)
37+ expect(root.classList.contains('macos')).toBe(true)
38+ markPlatform(root, false)
39+ expect(root.classList.contains('macos')).toBe(false)
40+ })
41+})
42+
43+describe('isMac', () => {
44+ const safari = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)'
45+ const webkitGtk = 'Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15'
46+
47+ it('trusts the Wails runtime when it already knows', () => {
48+ expect(isMac(true, webkitGtk)).toBe(true)
49+ })
50+
51+ it('falls back on the user agent while the runtime environment is not set yet', () => {
52+ expect(isMac(false, safari)).toBe(true)
53+ })
54+
55+ it('is false on Linux', () => {
56+ expect(isMac(false, webkitGtk)).toBe(false)
57+ })
58+})
new file mode 100644
@@ -0,0 +1,58 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+import { describe, expect, it } from 'vitest'
4+import { applyTheme, isMac, markPlatform } from './theme'
5+
6+function element() {
7+ const classes = new Set<string>()
8+ return {
9+ dataset: {} as Record<string, string>,
10+ classList: { toggle: (name: string, on: boolean) => (on ? classes.add(name) : classes.delete(name)), contains: (name: string) => classes.has(name) },
11+ } as unknown as HTMLElement
12+}
13+
14+describe('applyTheme', () => {
15+ it('forces light and dark', () => {
16+ const root = element()
17+ applyTheme(root, 'dark')
18+ expect(root.dataset.theme).toBe('dark')
19+ applyTheme(root, 'light')
20+ expect(root.dataset.theme).toBe('light')
21+ })
22+
23+ it('follows the system otherwise', () => {
24+ const root = element()
25+ applyTheme(root, 'dark')
26+ applyTheme(root, 'system')
27+ expect(root.dataset.theme).toBeUndefined()
28+ applyTheme(root, 'blue')
29+ expect(root.dataset.theme).toBeUndefined()
30+ })
31+})
32+
33+describe('markPlatform', () => {
34+ it('adds and removes the macos class', () => {
35+ const root = element()
36+ markPlatform(root, true)
37+ expect(root.classList.contains('macos')).toBe(true)
38+ markPlatform(root, false)
39+ expect(root.classList.contains('macos')).toBe(false)
40+ })
41+})
42+
43+describe('isMac', () => {
44+ const safari = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)'
45+ const webkitGtk = 'Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15'
46+
47+ it('trusts the Wails runtime when it already knows', () => {
48+ expect(isMac(true, webkitGtk)).toBe(true)
49+ })
50+
51+ it('falls back on the user agent while the runtime environment is not set yet', () => {
52+ expect(isMac(false, safari)).toBe(true)
53+ })
54+
55+ it('is false on Linux', () => {
56+ expect(isMac(false, webkitGtk)).toBe(false)
57+ })
58+})
added frontend/src/lib/theme.ts +42 -0
new file mode 100644
@@ -0,0 +1,42 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// Theme and platform classes on the root element. The colours themselves are
4+// CSS variables in style.css.
5+
6+export type Theme = 'system' | 'light' | 'dark'
7+
8+/**
9+ * Applies ui.theme from the configuration: "light" and "dark" force a theme,
10+ * "system" (or anything unknown) follows the operating system.
11+ *
12+ * @example
13+ * applyTheme(document.documentElement, config.theme)
14+ */
15+export function applyTheme(root: HTMLElement, theme: string) {
16+ if (theme === 'light' || theme === 'dark') {
17+ root.dataset.theme = theme
18+ } else {
19+ delete root.dataset.theme
20+ }
21+}
22+
23+/**
24+ * Tells whether the interface runs on macOS. The Wails runtime fills its
25+ * environment asynchronously, so System.IsMac() can still be false when the
26+ * page starts; the user agent ("Macintosh" in WKWebView) is the fallback, as
27+ * the runtime itself does for window dragging.
28+ *
29+ * @example
30+ * isMac(System.IsMac(), navigator.userAgent)
31+ */
32+export function isMac(runtimeSaysMac: boolean, userAgent: string): boolean {
33+ return runtimeSaysMac || /Macintosh|Mac OS X/.test(userAgent)
34+}
35+
36+/**
37+ * Marks the root element on macOS, where the hidden title bar puts the
38+ * window buttons over the content (see --titlebar-inset in style.css).
39+ */
40+export function markPlatform(root: HTMLElement, isMac: boolean) {
41+ root.classList.toggle('macos', isMac)
42+}
new file mode 100644
@@ -0,0 +1,42 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// Theme and platform classes on the root element. The colours themselves are
4+// CSS variables in style.css.
5+
6+export type Theme = 'system' | 'light' | 'dark'
7+
8+/**
9+ * Applies ui.theme from the configuration: "light" and "dark" force a theme,
10+ * "system" (or anything unknown) follows the operating system.
11+ *
12+ * @example
13+ * applyTheme(document.documentElement, config.theme)
14+ */
15+export function applyTheme(root: HTMLElement, theme: string) {
16+ if (theme === 'light' || theme === 'dark') {
17+ root.dataset.theme = theme
18+ } else {
19+ delete root.dataset.theme
20+ }
21+}
22+
23+/**
24+ * Tells whether the interface runs on macOS. The Wails runtime fills its
25+ * environment asynchronously, so System.IsMac() can still be false when the
26+ * page starts; the user agent ("Macintosh" in WKWebView) is the fallback, as
27+ * the runtime itself does for window dragging.
28+ *
29+ * @example
30+ * isMac(System.IsMac(), navigator.userAgent)
31+ */
32+export function isMac(runtimeSaysMac: boolean, userAgent: string): boolean {
33+ return runtimeSaysMac || /Macintosh|Mac OS X/.test(userAgent)
34+}
35+
36+/**
37+ * Marks the root element on macOS, where the hidden title bar puts the
38+ * window buttons over the content (see --titlebar-inset in style.css).
39+ */
40+export function markPlatform(root: HTMLElement, isMac: boolean) {
41+ root.classList.toggle('macos', isMac)
42+}
added frontend/src/main.ts +11 -0
new file mode 100644
@@ -0,0 +1,11 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+import { mount } from 'svelte'
4+import { System } from '@wailsio/runtime'
5+import App from './App.svelte'
6+import { isMac, markPlatform } from './lib/theme'
7+import './style.css'
8+
9+markPlatform(document.documentElement, isMac(System.IsMac(), navigator.userAgent))
10+
11+mount(App, { target: document.getElementById('app')! })
new file mode 100644
@@ -0,0 +1,11 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+import { mount } from 'svelte'
4+import { System } from '@wailsio/runtime'
5+import App from './App.svelte'
6+import { isMac, markPlatform } from './lib/theme'
7+import './style.css'
8+
9+markPlatform(document.documentElement, isMac(System.IsMac(), navigator.userAgent))
10+
11+mount(App, { target: document.getElementById('app')! })
added frontend/src/style.css +94 -0
new file mode 100644
@@ -0,0 +1,94 @@
1+/* Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE */
2+
3+/*
4+ * Design tokens (ADR 0002): sober, Zed-like. The system theme is used unless
5+ * the configuration forces one with ui.theme (data-theme on <html>).
6+ */
7+:root {
8+ --bg: #f6f6f7;
9+ --panel: #ffffff;
10+ --panel-2: #f0f1f3;
11+ --border: #e2e3e6;
12+ --text: #1d1f24;
13+ --muted: #6b6f7a;
14+ --accent: #3b6fe0;
15+ --danger: #c9354f;
16+ --ok: #3e9b4f;
17+ --warn: #c98a1b;
18+ --radius: 8px;
19+ --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
20+ --mono: "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
21+ /* Height of the area covered by the macOS window buttons; 0 elsewhere. */
22+ --titlebar-inset: 0px;
23+ color-scheme: light;
24+}
25+
26+@media (prefers-color-scheme: dark) {
27+ :root:not([data-theme="light"]) {
28+ --bg: #18191d;
29+ --panel: #1f2025;
30+ --panel-2: #26282e;
31+ --border: #2f3138;
32+ --text: #e6e7eb;
33+ --muted: #8b8f9a;
34+ --accent: #7aa2f7;
35+ --danger: #f7768e;
36+ --ok: #9ece6a;
37+ --warn: #e0af68;
38+ color-scheme: dark;
39+ }
40+}
41+
42+:root[data-theme="dark"] {
43+ --bg: #18191d;
44+ --panel: #1f2025;
45+ --panel-2: #26282e;
46+ --border: #2f3138;
47+ --text: #e6e7eb;
48+ --muted: #8b8f9a;
49+ --accent: #7aa2f7;
50+ --danger: #f7768e;
51+ --ok: #9ece6a;
52+ --warn: #e0af68;
53+ color-scheme: dark;
54+}
55+
56+/* Must match titleBarInset in main.go. */
57+:root.macos {
58+ --titlebar-inset: 38px;
59+}
60+
61+* {
62+ box-sizing: border-box;
63+}
64+
65+html,
66+body,
67+#app {
68+ margin: 0;
69+ height: 100%;
70+ background: var(--bg);
71+ color: var(--text);
72+ font-family: var(--font);
73+ font-size: 14px;
74+ -webkit-font-smoothing: antialiased;
75+}
76+
77+button {
78+ font: inherit;
79+ color: var(--text);
80+ background: var(--panel-2);
81+ border: 1px solid var(--border);
82+ border-radius: 6px;
83+ padding: 3px 10px;
84+ cursor: pointer;
85+}
86+
87+button:hover {
88+ border-color: var(--accent);
89+}
90+
91+button:disabled {
92+ opacity: 0.5;
93+ cursor: default;
94+}
new file mode 100644
@@ -0,0 +1,94 @@
1+/* Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE */
2+
3+/*
4+ * Design tokens (ADR 0002): sober, Zed-like. The system theme is used unless
5+ * the configuration forces one with ui.theme (data-theme on <html>).
6+ */
7+:root {
8+ --bg: #f6f6f7;
9+ --panel: #ffffff;
10+ --panel-2: #f0f1f3;
11+ --border: #e2e3e6;
12+ --text: #1d1f24;
13+ --muted: #6b6f7a;
14+ --accent: #3b6fe0;
15+ --danger: #c9354f;
16+ --ok: #3e9b4f;
17+ --warn: #c98a1b;
18+ --radius: 8px;
19+ --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
20+ --mono: "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
21+ /* Height of the area covered by the macOS window buttons; 0 elsewhere. */
22+ --titlebar-inset: 0px;
23+ color-scheme: light;
24+}
25+
26+@media (prefers-color-scheme: dark) {
27+ :root:not([data-theme="light"]) {
28+ --bg: #18191d;
29+ --panel: #1f2025;
30+ --panel-2: #26282e;
31+ --border: #2f3138;
32+ --text: #e6e7eb;
33+ --muted: #8b8f9a;
34+ --accent: #7aa2f7;
35+ --danger: #f7768e;
36+ --ok: #9ece6a;
37+ --warn: #e0af68;
38+ color-scheme: dark;
39+ }
40+}
41+
42+:root[data-theme="dark"] {
43+ --bg: #18191d;
44+ --panel: #1f2025;
45+ --panel-2: #26282e;
46+ --border: #2f3138;
47+ --text: #e6e7eb;
48+ --muted: #8b8f9a;
49+ --accent: #7aa2f7;
50+ --danger: #f7768e;
51+ --ok: #9ece6a;
52+ --warn: #e0af68;
53+ color-scheme: dark;
54+}
55+
56+/* Must match titleBarInset in main.go. */
57+:root.macos {
58+ --titlebar-inset: 38px;
59+}
60+
61+* {
62+ box-sizing: border-box;
63+}
64+
65+html,
66+body,
67+#app {
68+ margin: 0;
69+ height: 100%;
70+ background: var(--bg);
71+ color: var(--text);
72+ font-family: var(--font);
73+ font-size: 14px;
74+ -webkit-font-smoothing: antialiased;
75+}
76+
77+button {
78+ font: inherit;
79+ color: var(--text);
80+ background: var(--panel-2);
81+ border: 1px solid var(--border);
82+ border-radius: 6px;
83+ padding: 3px 10px;
84+ cursor: pointer;
85+}
86+
87+button:hover {
88+ border-color: var(--accent);
89+}
90+
91+button:disabled {
92+ opacity: 0.5;
93+ cursor: default;
94+}
added frontend/src/vite-env.d.ts +2 -0
new file mode 100644
@@ -0,0 +1,2 @@
1+/// <reference types="svelte" />
2+/// <reference types="vite/client" />
new file mode 100644
@@ -0,0 +1,2 @@
1+/// <reference types="svelte" />
2+/// <reference types="vite/client" />
added frontend/svelte.config.js +7 -0
new file mode 100644
@@ -0,0 +1,7 @@
1+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
2+
3+export default {
4+ // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
5+ // for more information about preprocessors
6+ preprocess: vitePreprocess(),
7+}
new file mode 100644
@@ -0,0 +1,7 @@
1+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
2+
3+export default {
4+ // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
5+ // for more information about preprocessors
6+ preprocess: vitePreprocess(),
7+}
added frontend/tsconfig.json +19 -0
new file mode 100644
@@ -0,0 +1,19 @@
1+{
2+ "extends": "@tsconfig/svelte/tsconfig.json",
3+ "compilerOptions": {
4+ "target": "ESNext",
5+ "useDefineForClassFields": true,
6+ "module": "ESNext",
7+ "resolveJsonModule": true,
8+ /**
9+ * Typecheck JS in `.svelte` and `.js` files by default.
10+ * Disable checkJs if you'd like to use dynamic types in JS.
11+ * Note that setting allowJs false does not prevent the use
12+ * of JS in `.svelte` files.
13+ */
14+ "allowJs": true,
15+ "checkJs": true,
16+ "isolatedModules": true
17+ },
18+ "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte", "bindings"],
19+}
new file mode 100644
@@ -0,0 +1,19 @@
1+{
2+ "extends": "@tsconfig/svelte/tsconfig.json",
3+ "compilerOptions": {
4+ "target": "ESNext",
5+ "useDefineForClassFields": true,
6+ "module": "ESNext",
7+ "resolveJsonModule": true,
8+ /**
9+ * Typecheck JS in `.svelte` and `.js` files by default.
10+ * Disable checkJs if you'd like to use dynamic types in JS.
11+ * Note that setting allowJs false does not prevent the use
12+ * of JS in `.svelte` files.
13+ */
14+ "allowJs": true,
15+ "checkJs": true,
16+ "isolatedModules": true
17+ },
18+ "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte", "bindings"],
19+}
added frontend/vite.config.ts +13 -0
new file mode 100644
@@ -0,0 +1,13 @@
1+import { defineConfig } from "vite";
2+import { svelte } from "@sveltejs/vite-plugin-svelte";
3+import wails from "@wailsio/runtime/plugins/vite";
4+
5+// https://vitejs.dev/config/
6+export default defineConfig({
7+ server: {
8+ host: "127.0.0.1",
9+ port: Number(process.env.WAILS_VITE_PORT) || 9245,
10+ strictPort: true,
11+ },
12+ plugins: [svelte(), wails("./bindings")],
13+});
new file mode 100644
@@ -0,0 +1,13 @@
1+import { defineConfig } from "vite";
2+import { svelte } from "@sveltejs/vite-plugin-svelte";
3+import wails from "@wailsio/runtime/plugins/vite";
4+
5+// https://vitejs.dev/config/
6+export default defineConfig({
7+ server: {
8+ host: "127.0.0.1",
9+ port: Number(process.env.WAILS_VITE_PORT) || 9245,
10+ strictPort: true,
11+ },
12+ plugins: [svelte(), wails("./bindings")],
13+});
modified go.mod +14 -2
@@ -1,10 +1,22 @@
11 module git.rickub.com/bots-garden/sidekicks
22
3-go 1.26
3+go 1.26.0
44
55 require (
6+ github.com/coder/acp-go-sdk v0.13.5
7+ github.com/fsnotify/fsnotify v1.10.1
68 github.com/santhosh-tekuri/jsonschema/v6 v6.0.3
9+ github.com/wailsapp/wails/v3 v3.0.0
710 go.yaml.in/yaml/v3 v3.0.5
11+ golang.org/x/text v0.42.0
812 )
913
10-require golang.org/x/text v0.14.0 // indirect
14+require (
15+ github.com/adrg/xdg v0.5.3 // indirect
16+ github.com/coder/websocket v1.8.14 // indirect
17+ github.com/go-ole/go-ole v1.3.0 // indirect
18+ github.com/godbus/dbus/v5 v5.2.2 // indirect
19+ github.com/mattn/go-colorable v0.1.14 // indirect
20+ github.com/mattn/go-isatty v0.0.20 // indirect
21+ golang.org/x/sys v0.48.0 // indirect
22+)
@@ -1,10 +1,22 @@
1 module git.rickub.com/bots-garden/sidekicks1 module git.rickub.com/bots-garden/sidekicks
2 2
3-go 1.263+go 1.26.0
4 4
5 require (5 require (
6+ github.com/coder/acp-go-sdk v0.13.5
7+ github.com/fsnotify/fsnotify v1.10.1
6 github.com/santhosh-tekuri/jsonschema/v6 v6.0.38 github.com/santhosh-tekuri/jsonschema/v6 v6.0.3
9+ github.com/wailsapp/wails/v3 v3.0.0
7 go.yaml.in/yaml/v3 v3.0.510 go.yaml.in/yaml/v3 v3.0.5
11+ golang.org/x/text v0.42.0
8 )12 )
9 13
10-require golang.org/x/text v0.14.0 // indirect14+require (
15+ github.com/adrg/xdg v0.5.3 // indirect
16+ github.com/coder/websocket v1.8.14 // indirect
17+ github.com/go-ole/go-ole v1.3.0 // indirect
18+ github.com/godbus/dbus/v5 v5.2.2 // indirect
19+ github.com/mattn/go-colorable v0.1.14 // indirect
20+ github.com/mattn/go-isatty v0.0.20 // indirect
21+ golang.org/x/sys v0.48.0 // indirect
22+)
modified go.sum +40 -4
@@ -1,8 +1,44 @@
1-github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
2-github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
1+github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
2+github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
3+github.com/coder/acp-go-sdk v0.13.5 h1:LI9jq5xon7xslaYlnoktvTVyDlE37yIk2daT7N9ASYk=
4+github.com/coder/acp-go-sdk v0.13.5/go.mod h1:yKzM/3R9uELp4+nBAwwtkS0aN1FOFjo11CNPy37yFko=
5+github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
6+github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
7+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
8+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9+github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
10+github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
11+github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
12+github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
13+github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
14+github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
15+github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
16+github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
17+github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
18+github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
19+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
20+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
21+github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
22+github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
23+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
24+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
25+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
26+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
27+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
28+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
329 github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
430 github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
31+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
32+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
33+github.com/wailsapp/wails/v3 v3.0.0 h1:lBe89kFu23bVz1dc1H9v0srkHQgWsy7BAKpH4sLQuDw=
34+github.com/wailsapp/wails/v3 v3.0.0/go.mod h1:U3p3/QfSer2XxXM3XWhwis9O7LvvuNKN/AcGoEFD+K8=
535 go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
636 go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
7-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
8-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
37+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
38+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
39+golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
40+golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
41+golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
42+golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
43+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
44+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -1,8 +1,44 @@
1-github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=1+github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
2-github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=2+github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
3+github.com/coder/acp-go-sdk v0.13.5 h1:LI9jq5xon7xslaYlnoktvTVyDlE37yIk2daT7N9ASYk=
4+github.com/coder/acp-go-sdk v0.13.5/go.mod h1:yKzM/3R9uELp4+nBAwwtkS0aN1FOFjo11CNPy37yFko=
5+github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
6+github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
7+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
8+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9+github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
10+github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
11+github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
12+github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
13+github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
14+github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
15+github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
16+github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
17+github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
18+github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
19+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
20+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
21+github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
22+github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
23+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
24+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
25+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
26+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
27+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
28+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
3 github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=29 github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
4 github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=30 github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
31+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
32+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
33+github.com/wailsapp/wails/v3 v3.0.0 h1:lBe89kFu23bVz1dc1H9v0srkHQgWsy7BAKpH4sLQuDw=
34+github.com/wailsapp/wails/v3 v3.0.0/go.mod h1:U3p3/QfSer2XxXM3XWhwis9O7LvvuNKN/AcGoEFD+K8=
5 go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=35 go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
6 go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=36 go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
7-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=37+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
8-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=38+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
39+golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
40+golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
41+golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
42+golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
43+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
44+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
added internal/acp/client.go +52 -0
new file mode 100644
@@ -0,0 +1,52 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package acp
4+
5+import (
6+ "context"
7+
8+ sdk "github.com/coder/acp-go-sdk"
9+)
10+
11+// client answers the requests an agent sends to Sidekicks. In milestone M1
12+// there are no sessions yet, so nothing should arrive: session updates are
13+// ignored, permission requests are cancelled, and file and terminal access —
14+// which Sidekicks does not advertise (ADR 0001, decision 5) — is refused.
15+// Sessions (issue #14) and permissions (issue #17) will route these calls.
16+type client struct{}
17+
18+func (*client) SessionUpdate(context.Context, sdk.SessionNotification) error { return nil }
19+
20+func (*client) RequestPermission(context.Context, sdk.RequestPermissionRequest) (sdk.RequestPermissionResponse, error) {
21+ return sdk.RequestPermissionResponse{
22+ Outcome: sdk.RequestPermissionOutcome{Cancelled: &sdk.RequestPermissionOutcomeCancelled{}},
23+ }, nil
24+}
25+
26+func (*client) ReadTextFile(context.Context, sdk.ReadTextFileRequest) (sdk.ReadTextFileResponse, error) {
27+ return sdk.ReadTextFileResponse{}, sdk.NewMethodNotFound("fs/read_text_file")
28+}
29+
30+func (*client) WriteTextFile(context.Context, sdk.WriteTextFileRequest) (sdk.WriteTextFileResponse, error) {
31+ return sdk.WriteTextFileResponse{}, sdk.NewMethodNotFound("fs/write_text_file")
32+}
33+
34+func (*client) CreateTerminal(context.Context, sdk.CreateTerminalRequest) (sdk.CreateTerminalResponse, error) {
35+ return sdk.CreateTerminalResponse{}, sdk.NewMethodNotFound("terminal/create")
36+}
37+
38+func (*client) KillTerminal(context.Context, sdk.KillTerminalRequest) (sdk.KillTerminalResponse, error) {
39+ return sdk.KillTerminalResponse{}, sdk.NewMethodNotFound("terminal/kill")
40+}
41+
42+func (*client) TerminalOutput(context.Context, sdk.TerminalOutputRequest) (sdk.TerminalOutputResponse, error) {
43+ return sdk.TerminalOutputResponse{}, sdk.NewMethodNotFound("terminal/output")
44+}
45+
46+func (*client) ReleaseTerminal(context.Context, sdk.ReleaseTerminalRequest) (sdk.ReleaseTerminalResponse, error) {
47+ return sdk.ReleaseTerminalResponse{}, sdk.NewMethodNotFound("terminal/release")
48+}
49+
50+func (*client) WaitForTerminalExit(context.Context, sdk.WaitForTerminalExitRequest) (sdk.WaitForTerminalExitResponse, error) {
51+ return sdk.WaitForTerminalExitResponse{}, sdk.NewMethodNotFound("terminal/wait_for_exit")
52+}
new file mode 100644
@@ -0,0 +1,52 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package acp
4+
5+import (
6+ "context"
7+
8+ sdk "github.com/coder/acp-go-sdk"
9+)
10+
11+// client answers the requests an agent sends to Sidekicks. In milestone M1
12+// there are no sessions yet, so nothing should arrive: session updates are
13+// ignored, permission requests are cancelled, and file and terminal access —
14+// which Sidekicks does not advertise (ADR 0001, decision 5) — is refused.
15+// Sessions (issue #14) and permissions (issue #17) will route these calls.
16+type client struct{}
17+
18+func (*client) SessionUpdate(context.Context, sdk.SessionNotification) error { return nil }
19+
20+func (*client) RequestPermission(context.Context, sdk.RequestPermissionRequest) (sdk.RequestPermissionResponse, error) {
21+ return sdk.RequestPermissionResponse{
22+ Outcome: sdk.RequestPermissionOutcome{Cancelled: &sdk.RequestPermissionOutcomeCancelled{}},
23+ }, nil
24+}
25+
26+func (*client) ReadTextFile(context.Context, sdk.ReadTextFileRequest) (sdk.ReadTextFileResponse, error) {
27+ return sdk.ReadTextFileResponse{}, sdk.NewMethodNotFound("fs/read_text_file")
28+}
29+
30+func (*client) WriteTextFile(context.Context, sdk.WriteTextFileRequest) (sdk.WriteTextFileResponse, error) {
31+ return sdk.WriteTextFileResponse{}, sdk.NewMethodNotFound("fs/write_text_file")
32+}
33+
34+func (*client) CreateTerminal(context.Context, sdk.CreateTerminalRequest) (sdk.CreateTerminalResponse, error) {
35+ return sdk.CreateTerminalResponse{}, sdk.NewMethodNotFound("terminal/create")
36+}
37+
38+func (*client) KillTerminal(context.Context, sdk.KillTerminalRequest) (sdk.KillTerminalResponse, error) {
39+ return sdk.KillTerminalResponse{}, sdk.NewMethodNotFound("terminal/kill")
40+}
41+
42+func (*client) TerminalOutput(context.Context, sdk.TerminalOutputRequest) (sdk.TerminalOutputResponse, error) {
43+ return sdk.TerminalOutputResponse{}, sdk.NewMethodNotFound("terminal/output")
44+}
45+
46+func (*client) ReleaseTerminal(context.Context, sdk.ReleaseTerminalRequest) (sdk.ReleaseTerminalResponse, error) {
47+ return sdk.ReleaseTerminalResponse{}, sdk.NewMethodNotFound("terminal/release")
48+}
49+
50+func (*client) WaitForTerminalExit(context.Context, sdk.WaitForTerminalExitRequest) (sdk.WaitForTerminalExitResponse, error) {
51+ return sdk.WaitForTerminalExitResponse{}, sdk.NewMethodNotFound("terminal/wait_for_exit")
52+}
added internal/acp/conn.go +331 -0
new file mode 100644
@@ -0,0 +1,331 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// Package acp starts an agent as a child process and talks to it with the
4+// Agent Client Protocol, version 1 (ADR 0001), over its stdin and stdout.
5+//
6+// It wraps github.com/coder/acp-go-sdk: the SDK's types never leave this
7+// package, so the rest of Sidekicks depends on Sidekicks' own types only.
8+//
9+// conn, err := acp.Start(ctx, acp.Spec{Name: "alice", Command: "mm", Args: []string{"-acp"}, Dir: workspace}, acp.Options{
10+// OnStderr: func(line string) { log.Println("alice:", line) },
11+// })
12+// if err != nil { … }
13+// defer conn.Close(context.Background())
14+// fmt.Println(conn.Info().Title)
15+package acp
16+
17+import (
18+ "bufio"
19+ "context"
20+ "errors"
21+ "fmt"
22+ "io"
23+ "log/slog"
24+ "os"
25+ "os/exec"
26+ "strings"
27+ "sync"
28+ "time"
29+
30+ sdk "github.com/coder/acp-go-sdk"
31+)
32+
33+// ProtocolVersion is the ACP version Sidekicks speaks.
34+const ProtocolVersion = 1
35+
36+// Default durations, used when Options leaves them at zero.
37+const (
38+ DefaultInitializeTimeout = 30 * time.Second
39+ // DefaultCloseGrace is how long Close waits for the agent to exit after its
40+ // stdin is closed, before killing it.
41+ DefaultCloseGrace = 5 * time.Second
42+)
43+
44+// ErrUnsupportedProtocol is returned by Start when the agent answers with a
45+// protocol version other than ProtocolVersion.
46+var ErrUnsupportedProtocol = errors.New("unsupported ACP protocol version")
47+
48+// Spec describes how to start an agent.
49+type Spec struct {
50+ // Name identifies the agent in errors and logs.
51+ Name string
52+ Command string
53+ Args []string
54+ // Env is added to the environment of Sidekicks.
55+ Env map[string]string
56+ // Dir is the working directory of the process: the agent's workspace.
57+ Dir string
58+}
59+
60+// Options tunes Start. The zero value is usable.
61+type Options struct {
62+ // OnStderr receives each line the agent writes on its standard error.
63+ OnStderr func(line string)
64+ InitializeTimeout time.Duration
65+ CloseGrace time.Duration
66+}
67+
68+// Info is what the agent said about itself during the handshake.
69+type Info struct {
70+ Name string
71+ Title string
72+ Version string
73+ ProtocolVersion int
74+ // LoadSession tells whether the agent can resume a session (mm cannot).
75+ LoadSession bool
76+ // The kinds of content the agent accepts in a prompt besides text.
77+ PromptImage, PromptAudio, PromptEmbeddedContext bool
78+}
79+
80+// Conn is a running agent after a successful handshake.
81+type Conn struct {
82+ spec Spec
83+ info Info
84+ process *exec.Cmd
85+ stdin io.WriteCloser
86+ connection *sdk.ClientSideConnection
87+ closeGrace time.Duration
88+ stderr *tail
89+
90+ done chan struct{}
91+ mu sync.Mutex
92+ closing bool
93+ exitErr error
94+}
95+
96+// Start launches the agent and performs the ACP handshake (initialize). It
97+// returns once the agent is ready, or an error that includes the last lines
98+// the agent wrote on stderr, which usually say why it failed.
99+func Start(ctx context.Context, spec Spec, options Options) (*Conn, error) {
100+ conn := &Conn{
101+ spec: spec,
102+ done: make(chan struct{}),
103+ closeGrace: orDuration(options.CloseGrace, DefaultCloseGrace),
104+ stderr: newTail(20),
105+ }
106+ if err := conn.launch(options.OnStderr); err != nil {
107+ return nil, fmt.Errorf("start agent %s: %w", spec.Name, err)
108+ }
109+ initializeCtx, cancel := context.WithTimeout(ctx, orDuration(options.InitializeTimeout, DefaultInitializeTimeout))
110+ defer cancel()
111+ if err := conn.initialize(initializeCtx); err != nil {
112+ conn.kill()
113+ <-conn.done
114+ return nil, conn.startError(err)
115+ }
116+ return conn, nil
117+}
118+
119+func (c *Conn) launch(onStderr func(string)) error {
120+ process := exec.Command(c.spec.Command, c.spec.Args...)
121+ process.Dir = c.spec.Dir
122+ process.Env = os.Environ()
123+ for key, value := range c.spec.Env {
124+ process.Env = append(process.Env, key+"="+value)
125+ }
126+ isolateProcessGroup(process)
127+
128+ stdin, err := process.StdinPipe()
129+ if err != nil {
130+ return err
131+ }
132+ stdout, err := process.StdoutPipe()
133+ if err != nil {
134+ return err
135+ }
136+ stderr, err := process.StderrPipe()
137+ if err != nil {
138+ return err
139+ }
140+ if err := process.Start(); err != nil {
141+ return err
142+ }
143+ c.process, c.stdin = process, stdin
144+
145+ stderrDone := make(chan struct{})
146+ go c.readStderr(stderr, onStderr, stderrDone)
147+ go c.wait(stderrDone)
148+ c.connection = sdk.NewClientSideConnection(&client{}, stdin, stdout)
149+ // The SDK logs its own diagnostics ("connection closed"…); the agent's
150+ // stderr already tells what matters.
151+ c.connection.SetLogger(slog.New(slog.NewTextHandler(io.Discard, nil)))
152+ return nil
153+}
154+
155+func (c *Conn) readStderr(stderr io.Reader, onStderr func(string), done chan<- struct{}) {
156+ defer close(done)
157+ scanner := bufio.NewScanner(stderr)
158+ scanner.Buffer(make([]byte, 64*1024), 1024*1024)
159+ for scanner.Scan() {
160+ line := scanner.Text()
161+ c.stderr.add(line)
162+ if onStderr != nil {
163+ onStderr(line)
164+ }
165+ }
166+}
167+
168+func (c *Conn) wait(stderrDone <-chan struct{}) {
169+ // Read stderr to the end first: Wait closes the pipe.
170+ <-stderrDone
171+ err := c.process.Wait()
172+ c.mu.Lock()
173+ if !c.closing {
174+ c.exitErr = exitError(err)
175+ }
176+ c.mu.Unlock()
177+ close(c.done)
178+}
179+
180+func exitError(err error) error {
181+ if err == nil {
182+ return errors.New("the agent stopped")
183+ }
184+ return fmt.Errorf("the agent stopped: %w", err)
185+}
186+
187+func (c *Conn) initialize(ctx context.Context) error {
188+ type result struct {
189+ response sdk.InitializeResponse
190+ err error
191+ }
192+ answer := make(chan result, 1)
193+ go func() {
194+ response, err := c.connection.Initialize(ctx, sdk.InitializeRequest{
195+ ProtocolVersion: sdk.ProtocolVersion(ProtocolVersion),
196+ ClientCapabilities: sdk.ClientCapabilities{},
197+ ClientInfo: &sdk.Implementation{Name: "sidekicks", Version: "0.0.1"},
198+ })
199+ answer <- result{response, err}
200+ }()
201+ select {
202+ case got := <-answer:
203+ if got.err != nil {
204+ return got.err
205+ }
206+ c.info = infoFrom(got.response)
207+ if c.info.ProtocolVersion != ProtocolVersion {
208+ return fmt.Errorf("%w: the agent speaks version %d, Sidekicks speaks version %d", ErrUnsupportedProtocol, c.info.ProtocolVersion, ProtocolVersion)
209+ }
210+ return nil
211+ case <-c.done:
212+ return c.Err()
213+ case <-ctx.Done():
214+ return fmt.Errorf("no answer to initialize: %w", ctx.Err())
215+ }
216+}
217+
218+func infoFrom(response sdk.InitializeResponse) Info {
219+ info := Info{
220+ ProtocolVersion: int(response.ProtocolVersion),
221+ LoadSession: response.AgentCapabilities.LoadSession,
222+ PromptImage: response.AgentCapabilities.PromptCapabilities.Image,
223+ PromptAudio: response.AgentCapabilities.PromptCapabilities.Audio,
224+ PromptEmbeddedContext: response.AgentCapabilities.PromptCapabilities.EmbeddedContext,
225+ }
226+ if agent := response.AgentInfo; agent != nil {
227+ info.Name, info.Version = agent.Name, agent.Version
228+ if agent.Title != nil {
229+ info.Title = *agent.Title
230+ }
231+ }
232+ if info.Title == "" {
233+ info.Title = info.Name
234+ }
235+ return info
236+}
237+
238+func (c *Conn) startError(err error) error {
239+ message := fmt.Sprintf("start agent %s: %v", c.spec.Name, err)
240+ if lines := c.stderr.lines(); len(lines) > 0 {
241+ message += "\n" + strings.Join(lines, "\n")
242+ }
243+ return &StartError{Agent: c.spec.Name, message: message, cause: err}
244+}
245+
246+// StartError is returned by Start; its message ends with the agent's last
247+// stderr lines.
248+type StartError struct {
249+ Agent string
250+ message string
251+ cause error
252+}
253+
254+func (e *StartError) Error() string { return e.message }
255+func (e *StartError) Unwrap() error { return e.cause }
256+
257+// Info returns what the agent declared during the handshake.
258+func (c *Conn) Info() Info { return c.info }
259+
260+// Done is closed when the agent process has exited, for any reason.
261+func (c *Conn) Done() <-chan struct{} { return c.done }
262+
263+// Err tells why the agent exited when it was not closed by Close: a crash or
264+// an unexpected exit. It is nil while the agent runs and after Close.
265+func (c *Conn) Err() error {
266+ c.mu.Lock()
267+ defer c.mu.Unlock()
268+ return c.exitErr
269+}
270+
271+// Stderr returns the last lines the agent wrote on its standard error.
272+func (c *Conn) Stderr() []string { return c.stderr.lines() }
273+
274+// Close asks the agent to stop by closing its stdin, which is how an ACP
275+// agent learns its client is gone, then kills it (and its children) if it is
276+// still running after the grace period or when ctx is done.
277+func (c *Conn) Close(ctx context.Context) error {
278+ c.mu.Lock()
279+ c.closing = true
280+ c.mu.Unlock()
281+ _ = c.stdin.Close()
282+
283+ grace := time.NewTimer(c.closeGrace)
284+ defer grace.Stop()
285+ select {
286+ case <-c.done:
287+ return nil
288+ case <-grace.C:
289+ case <-ctx.Done():
290+ }
291+ c.kill()
292+ <-c.done
293+ return nil
294+}
295+
296+func (c *Conn) kill() {
297+ if c.process != nil && c.process.Process != nil {
298+ killProcessGroup(c.process)
299+ }
300+}
301+
302+func orDuration(value, fallback time.Duration) time.Duration {
303+ if value > 0 {
304+ return value
305+ }
306+ return fallback
307+}
308+
309+// tail keeps the last lines written to it.
310+type tail struct {
311+ mu sync.Mutex
312+ limit int
313+ items []string
314+}
315+
316+func newTail(limit int) *tail { return &tail{limit: limit} }
317+
318+func (t *tail) add(line string) {
319+ t.mu.Lock()
320+ defer t.mu.Unlock()
321+ t.items = append(t.items, line)
322+ if len(t.items) > t.limit {
323+ t.items = t.items[len(t.items)-t.limit:]
324+ }
325+}
326+
327+func (t *tail) lines() []string {
328+ t.mu.Lock()
329+ defer t.mu.Unlock()
330+ return append([]string(nil), t.items...)
331+}
new file mode 100644
@@ -0,0 +1,331 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// Package acp starts an agent as a child process and talks to it with the
4+// Agent Client Protocol, version 1 (ADR 0001), over its stdin and stdout.
5+//
6+// It wraps github.com/coder/acp-go-sdk: the SDK's types never leave this
7+// package, so the rest of Sidekicks depends on Sidekicks' own types only.
8+//
9+// conn, err := acp.Start(ctx, acp.Spec{Name: "alice", Command: "mm", Args: []string{"-acp"}, Dir: workspace}, acp.Options{
10+// OnStderr: func(line string) { log.Println("alice:", line) },
11+// })
12+// if err != nil { … }
13+// defer conn.Close(context.Background())
14+// fmt.Println(conn.Info().Title)
15+package acp
16+
17+import (
18+ "bufio"
19+ "context"
20+ "errors"
21+ "fmt"
22+ "io"
23+ "log/slog"
24+ "os"
25+ "os/exec"
26+ "strings"
27+ "sync"
28+ "time"
29+
30+ sdk "github.com/coder/acp-go-sdk"
31+)
32+
33+// ProtocolVersion is the ACP version Sidekicks speaks.
34+const ProtocolVersion = 1
35+
36+// Default durations, used when Options leaves them at zero.
37+const (
38+ DefaultInitializeTimeout = 30 * time.Second
39+ // DefaultCloseGrace is how long Close waits for the agent to exit after its
40+ // stdin is closed, before killing it.
41+ DefaultCloseGrace = 5 * time.Second
42+)
43+
44+// ErrUnsupportedProtocol is returned by Start when the agent answers with a
45+// protocol version other than ProtocolVersion.
46+var ErrUnsupportedProtocol = errors.New("unsupported ACP protocol version")
47+
48+// Spec describes how to start an agent.
49+type Spec struct {
50+ // Name identifies the agent in errors and logs.
51+ Name string
52+ Command string
53+ Args []string
54+ // Env is added to the environment of Sidekicks.
55+ Env map[string]string
56+ // Dir is the working directory of the process: the agent's workspace.
57+ Dir string
58+}
59+
60+// Options tunes Start. The zero value is usable.
61+type Options struct {
62+ // OnStderr receives each line the agent writes on its standard error.
63+ OnStderr func(line string)
64+ InitializeTimeout time.Duration
65+ CloseGrace time.Duration
66+}
67+
68+// Info is what the agent said about itself during the handshake.
69+type Info struct {
70+ Name string
71+ Title string
72+ Version string
73+ ProtocolVersion int
74+ // LoadSession tells whether the agent can resume a session (mm cannot).
75+ LoadSession bool
76+ // The kinds of content the agent accepts in a prompt besides text.
77+ PromptImage, PromptAudio, PromptEmbeddedContext bool
78+}
79+
80+// Conn is a running agent after a successful handshake.
81+type Conn struct {
82+ spec Spec
83+ info Info
84+ process *exec.Cmd
85+ stdin io.WriteCloser
86+ connection *sdk.ClientSideConnection
87+ closeGrace time.Duration
88+ stderr *tail
89+
90+ done chan struct{}
91+ mu sync.Mutex
92+ closing bool
93+ exitErr error
94+}
95+
96+// Start launches the agent and performs the ACP handshake (initialize). It
97+// returns once the agent is ready, or an error that includes the last lines
98+// the agent wrote on stderr, which usually say why it failed.
99+func Start(ctx context.Context, spec Spec, options Options) (*Conn, error) {
100+ conn := &Conn{
101+ spec: spec,
102+ done: make(chan struct{}),
103+ closeGrace: orDuration(options.CloseGrace, DefaultCloseGrace),
104+ stderr: newTail(20),
105+ }
106+ if err := conn.launch(options.OnStderr); err != nil {
107+ return nil, fmt.Errorf("start agent %s: %w", spec.Name, err)
108+ }
109+ initializeCtx, cancel := context.WithTimeout(ctx, orDuration(options.InitializeTimeout, DefaultInitializeTimeout))
110+ defer cancel()
111+ if err := conn.initialize(initializeCtx); err != nil {
112+ conn.kill()
113+ <-conn.done
114+ return nil, conn.startError(err)
115+ }
116+ return conn, nil
117+}
118+
119+func (c *Conn) launch(onStderr func(string)) error {
120+ process := exec.Command(c.spec.Command, c.spec.Args...)
121+ process.Dir = c.spec.Dir
122+ process.Env = os.Environ()
123+ for key, value := range c.spec.Env {
124+ process.Env = append(process.Env, key+"="+value)
125+ }
126+ isolateProcessGroup(process)
127+
128+ stdin, err := process.StdinPipe()
129+ if err != nil {
130+ return err
131+ }
132+ stdout, err := process.StdoutPipe()
133+ if err != nil {
134+ return err
135+ }
136+ stderr, err := process.StderrPipe()
137+ if err != nil {
138+ return err
139+ }
140+ if err := process.Start(); err != nil {
141+ return err
142+ }
143+ c.process, c.stdin = process, stdin
144+
145+ stderrDone := make(chan struct{})
146+ go c.readStderr(stderr, onStderr, stderrDone)
147+ go c.wait(stderrDone)
148+ c.connection = sdk.NewClientSideConnection(&client{}, stdin, stdout)
149+ // The SDK logs its own diagnostics ("connection closed"…); the agent's
150+ // stderr already tells what matters.
151+ c.connection.SetLogger(slog.New(slog.NewTextHandler(io.Discard, nil)))
152+ return nil
153+}
154+
155+func (c *Conn) readStderr(stderr io.Reader, onStderr func(string), done chan<- struct{}) {
156+ defer close(done)
157+ scanner := bufio.NewScanner(stderr)
158+ scanner.Buffer(make([]byte, 64*1024), 1024*1024)
159+ for scanner.Scan() {
160+ line := scanner.Text()
161+ c.stderr.add(line)
162+ if onStderr != nil {
163+ onStderr(line)
164+ }
165+ }
166+}
167+
168+func (c *Conn) wait(stderrDone <-chan struct{}) {
169+ // Read stderr to the end first: Wait closes the pipe.
170+ <-stderrDone
171+ err := c.process.Wait()
172+ c.mu.Lock()
173+ if !c.closing {
174+ c.exitErr = exitError(err)
175+ }
176+ c.mu.Unlock()
177+ close(c.done)
178+}
179+
180+func exitError(err error) error {
181+ if err == nil {
182+ return errors.New("the agent stopped")
183+ }
184+ return fmt.Errorf("the agent stopped: %w", err)
185+}
186+
187+func (c *Conn) initialize(ctx context.Context) error {
188+ type result struct {
189+ response sdk.InitializeResponse
190+ err error
191+ }
192+ answer := make(chan result, 1)
193+ go func() {
194+ response, err := c.connection.Initialize(ctx, sdk.InitializeRequest{
195+ ProtocolVersion: sdk.ProtocolVersion(ProtocolVersion),
196+ ClientCapabilities: sdk.ClientCapabilities{},
197+ ClientInfo: &sdk.Implementation{Name: "sidekicks", Version: "0.0.1"},
198+ })
199+ answer <- result{response, err}
200+ }()
201+ select {
202+ case got := <-answer:
203+ if got.err != nil {
204+ return got.err
205+ }
206+ c.info = infoFrom(got.response)
207+ if c.info.ProtocolVersion != ProtocolVersion {
208+ return fmt.Errorf("%w: the agent speaks version %d, Sidekicks speaks version %d", ErrUnsupportedProtocol, c.info.ProtocolVersion, ProtocolVersion)
209+ }
210+ return nil
211+ case <-c.done:
212+ return c.Err()
213+ case <-ctx.Done():
214+ return fmt.Errorf("no answer to initialize: %w", ctx.Err())
215+ }
216+}
217+
218+func infoFrom(response sdk.InitializeResponse) Info {
219+ info := Info{
220+ ProtocolVersion: int(response.ProtocolVersion),
221+ LoadSession: response.AgentCapabilities.LoadSession,
222+ PromptImage: response.AgentCapabilities.PromptCapabilities.Image,
223+ PromptAudio: response.AgentCapabilities.PromptCapabilities.Audio,
224+ PromptEmbeddedContext: response.AgentCapabilities.PromptCapabilities.EmbeddedContext,
225+ }
226+ if agent := response.AgentInfo; agent != nil {
227+ info.Name, info.Version = agent.Name, agent.Version
228+ if agent.Title != nil {
229+ info.Title = *agent.Title
230+ }
231+ }
232+ if info.Title == "" {
233+ info.Title = info.Name
234+ }
235+ return info
236+}
237+
238+func (c *Conn) startError(err error) error {
239+ message := fmt.Sprintf("start agent %s: %v", c.spec.Name, err)
240+ if lines := c.stderr.lines(); len(lines) > 0 {
241+ message += "\n" + strings.Join(lines, "\n")
242+ }
243+ return &StartError{Agent: c.spec.Name, message: message, cause: err}
244+}
245+
246+// StartError is returned by Start; its message ends with the agent's last
247+// stderr lines.
248+type StartError struct {
249+ Agent string
250+ message string
251+ cause error
252+}
253+
254+func (e *StartError) Error() string { return e.message }
255+func (e *StartError) Unwrap() error { return e.cause }
256+
257+// Info returns what the agent declared during the handshake.
258+func (c *Conn) Info() Info { return c.info }
259+
260+// Done is closed when the agent process has exited, for any reason.
261+func (c *Conn) Done() <-chan struct{} { return c.done }
262+
263+// Err tells why the agent exited when it was not closed by Close: a crash or
264+// an unexpected exit. It is nil while the agent runs and after Close.
265+func (c *Conn) Err() error {
266+ c.mu.Lock()
267+ defer c.mu.Unlock()
268+ return c.exitErr
269+}
270+
271+// Stderr returns the last lines the agent wrote on its standard error.
272+func (c *Conn) Stderr() []string { return c.stderr.lines() }
273+
274+// Close asks the agent to stop by closing its stdin, which is how an ACP
275+// agent learns its client is gone, then kills it (and its children) if it is
276+// still running after the grace period or when ctx is done.
277+func (c *Conn) Close(ctx context.Context) error {
278+ c.mu.Lock()
279+ c.closing = true
280+ c.mu.Unlock()
281+ _ = c.stdin.Close()
282+
283+ grace := time.NewTimer(c.closeGrace)
284+ defer grace.Stop()
285+ select {
286+ case <-c.done:
287+ return nil
288+ case <-grace.C:
289+ case <-ctx.Done():
290+ }
291+ c.kill()
292+ <-c.done
293+ return nil
294+}
295+
296+func (c *Conn) kill() {
297+ if c.process != nil && c.process.Process != nil {
298+ killProcessGroup(c.process)
299+ }
300+}
301+
302+func orDuration(value, fallback time.Duration) time.Duration {
303+ if value > 0 {
304+ return value
305+ }
306+ return fallback
307+}
308+
309+// tail keeps the last lines written to it.
310+type tail struct {
311+ mu sync.Mutex
312+ limit int
313+ items []string
314+}
315+
316+func newTail(limit int) *tail { return &tail{limit: limit} }
317+
318+func (t *tail) add(line string) {
319+ t.mu.Lock()
320+ defer t.mu.Unlock()
321+ t.items = append(t.items, line)
322+ if len(t.items) > t.limit {
323+ t.items = t.items[len(t.items)-t.limit:]
324+ }
325+}
326+
327+func (t *tail) lines() []string {
328+ t.mu.Lock()
329+ defer t.mu.Unlock()
330+ return append([]string(nil), t.items...)
331+}
added internal/acp/conn_realagents_test.go +42 -0
new file mode 100644
@@ -0,0 +1,42 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+//go:build realagents
4+
5+// Tests against the real mm test agents. They need mm and llmman, start the
6+// agents through scripts/agents.sh (two agents at most, model unloaded at
7+// exit) and only run with: go test -tags realagents ./internal/acp
8+package acp_test
9+
10+import (
11+ "context"
12+ "path/filepath"
13+ "testing"
14+ "time"
15+
16+ "git.rickub.com/bots-garden/sidekicks/internal/acp"
17+)
18+
19+func TestHandshakeWithAlice(t *testing.T) {
20+ root, err := filepath.Abs("../..")
21+ if err != nil {
22+ t.Fatal(err)
23+ }
24+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
25+ defer cancel()
26+ conn, err := acp.Start(ctx, acp.Spec{
27+ Name: "alice",
28+ Command: filepath.Join(root, "scripts", "agents.sh"),
29+ Args: []string{"run", "alice"},
30+ Dir: filepath.Join(root, "agents", "alice"),
31+ }, acp.Options{OnStderr: func(line string) { t.Log("alice:", line) }})
32+ if err != nil {
33+ t.Fatal(err)
34+ }
35+ info := conn.Info()
36+ if info.Name != "Alice" || info.ProtocolVersion != 1 || info.LoadSession {
37+ t.Errorf("unexpected handshake: %+v", info)
38+ }
39+ if err := conn.Close(ctx); err != nil {
40+ t.Fatal(err)
41+ }
42+}
new file mode 100644
@@ -0,0 +1,42 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+//go:build realagents
4+
5+// Tests against the real mm test agents. They need mm and llmman, start the
6+// agents through scripts/agents.sh (two agents at most, model unloaded at
7+// exit) and only run with: go test -tags realagents ./internal/acp
8+package acp_test
9+
10+import (
11+ "context"
12+ "path/filepath"
13+ "testing"
14+ "time"
15+
16+ "git.rickub.com/bots-garden/sidekicks/internal/acp"
17+)
18+
19+func TestHandshakeWithAlice(t *testing.T) {
20+ root, err := filepath.Abs("../..")
21+ if err != nil {
22+ t.Fatal(err)
23+ }
24+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
25+ defer cancel()
26+ conn, err := acp.Start(ctx, acp.Spec{
27+ Name: "alice",
28+ Command: filepath.Join(root, "scripts", "agents.sh"),
29+ Args: []string{"run", "alice"},
30+ Dir: filepath.Join(root, "agents", "alice"),
31+ }, acp.Options{OnStderr: func(line string) { t.Log("alice:", line) }})
32+ if err != nil {
33+ t.Fatal(err)
34+ }
35+ info := conn.Info()
36+ if info.Name != "Alice" || info.ProtocolVersion != 1 || info.LoadSession {
37+ t.Errorf("unexpected handshake: %+v", info)
38+ }
39+ if err := conn.Close(ctx); err != nil {
40+ t.Fatal(err)
41+ }
42+}
added internal/acp/conn_test.go +245 -0
new file mode 100644
@@ -0,0 +1,245 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package acp_test
4+
5+import (
6+ "context"
7+ "errors"
8+ "fmt"
9+ "os"
10+ "strings"
11+ "sync"
12+ "testing"
13+ "time"
14+
15+ "git.rickub.com/bots-garden/sidekicks/internal/acp"
16+ sdk "github.com/coder/acp-go-sdk"
17+)
18+
19+// The test binary doubles as a fake agent: started with fakeAgentVariable
20+// set, it serves ACP on stdio in the requested mode instead of running tests.
21+const fakeAgentVariable = "SIDEKICKS_FAKE_AGENT"
22+
23+func TestMain(m *testing.M) {
24+ if mode := os.Getenv(fakeAgentVariable); mode != "" {
25+ os.Exit(runFakeAgent(mode))
26+ }
27+ os.Exit(m.Run())
28+}
29+
30+func fakeSpec(t *testing.T, mode string) acp.Spec {
31+ t.Helper()
32+ return acp.Spec{Name: "fake", Command: os.Args[0], Env: map[string]string{fakeAgentVariable: mode}, Dir: t.TempDir()}
33+}
34+
35+func TestStartPerformsTheHandshake(t *testing.T) {
36+ var mu sync.Mutex
37+ var lines []string
38+ conn, err := acp.Start(context.Background(), fakeSpec(t, "ready"), acp.Options{OnStderr: func(line string) {
39+ mu.Lock()
40+ defer mu.Unlock()
41+ lines = append(lines, line)
42+ }})
43+ if err != nil {
44+ t.Fatal(err)
45+ }
46+ defer func() { _ = conn.Close(context.Background()) }()
47+
48+ info := conn.Info()
49+ if info.Name != "fake" || info.Title != "Fake agent" || info.Version != "9.9.9" || info.ProtocolVersion != 1 || !info.LoadSession || !info.PromptImage {
50+ t.Fatalf("unexpected info: %+v", info)
51+ }
52+ waitFor(t, func() bool {
53+ mu.Lock()
54+ defer mu.Unlock()
55+ return len(lines) > 0 && lines[0] == "fake agent: ready"
56+ })
57+ if got := conn.Stderr(); len(got) == 0 || got[0] != "fake agent: ready" {
58+ t.Fatalf("stderr tail: %v", got)
59+ }
60+}
61+
62+func TestCloseStopsTheAgentGracefully(t *testing.T) {
63+ conn, err := acp.Start(context.Background(), fakeSpec(t, "ready"), acp.Options{})
64+ if err != nil {
65+ t.Fatal(err)
66+ }
67+ start := time.Now()
68+ if err := conn.Close(context.Background()); err != nil {
69+ t.Fatal(err)
70+ }
71+ if elapsed := time.Since(start); elapsed > 2*time.Second {
72+ t.Fatalf("closing stdin should be enough, took %s", elapsed)
73+ }
74+ if conn.Err() != nil {
75+ t.Fatalf("a closed agent has no error, got %v", conn.Err())
76+ }
77+ select {
78+ case <-conn.Done():
79+ default:
80+ t.Fatal("Done must be closed after Close")
81+ }
82+}
83+
84+func TestCloseKillsAnAgentThatIgnoresStdin(t *testing.T) {
85+ conn, err := acp.Start(context.Background(), fakeSpec(t, "stubborn"), acp.Options{CloseGrace: 200 * time.Millisecond})
86+ if err != nil {
87+ t.Fatal(err)
88+ }
89+ start := time.Now()
90+ _ = conn.Close(context.Background())
91+ if elapsed := time.Since(start); elapsed > 3*time.Second {
92+ t.Fatalf("the agent should have been killed after the grace period, took %s", elapsed)
93+ }
94+}
95+
96+func TestACrashIsReported(t *testing.T) {
97+ conn, err := acp.Start(context.Background(), fakeSpec(t, "crash"), acp.Options{})
98+ if err != nil {
99+ t.Fatal(err)
100+ }
101+ select {
102+ case <-conn.Done():
103+ case <-time.After(5 * time.Second):
104+ t.Fatal("the crash was not noticed")
105+ }
106+ if conn.Err() == nil || !strings.Contains(conn.Err().Error(), "exit status 3") {
107+ t.Fatalf("want the exit status in the error, got %v", conn.Err())
108+ }
109+}
110+
111+func TestAnotherProtocolVersionIsRefused(t *testing.T) {
112+ _, err := acp.Start(context.Background(), fakeSpec(t, "version-2"), acp.Options{})
113+ if !errors.Is(err, acp.ErrUnsupportedProtocol) {
114+ t.Fatalf("want ErrUnsupportedProtocol, got %v", err)
115+ }
116+}
117+
118+func TestAStartFailureShowsTheAgentStderr(t *testing.T) {
119+ _, err := acp.Start(context.Background(), fakeSpec(t, "exit-early"), acp.Options{})
120+ var startError *acp.StartError
121+ if !errors.As(err, &startError) {
122+ t.Fatalf("want a *StartError, got %v", err)
123+ }
124+ if !strings.Contains(err.Error(), "fake agent: no model configured") {
125+ t.Fatalf("the agent's stderr should be in the error: %v", err)
126+ }
127+}
128+
129+func TestAMissingCommandFails(t *testing.T) {
130+ _, err := acp.Start(context.Background(), acp.Spec{Name: "ghost", Command: "no-such-agent-xyz"}, acp.Options{})
131+ if err == nil || !strings.Contains(err.Error(), "start agent ghost") {
132+ t.Fatalf("want a start error, got %v", err)
133+ }
134+}
135+
136+func TestInitializeTimesOut(t *testing.T) {
137+ start := time.Now()
138+ _, err := acp.Start(context.Background(), fakeSpec(t, "silent"), acp.Options{InitializeTimeout: 300 * time.Millisecond})
139+ if err == nil || !strings.Contains(err.Error(), "no answer to initialize") {
140+ t.Fatalf("want a timeout, got %v", err)
141+ }
142+ if time.Since(start) > 5*time.Second {
143+ t.Fatal("the silent agent was not killed after the timeout")
144+ }
145+}
146+
147+func ExampleStart() {
148+ // The test binary plays the agent here; with mm it would be
149+ // acp.Spec{Name: "alice", Command: "mm", Args: []string{"-acp", "-config", "agent.yaml"}, Dir: "/path/to/alice"}.
150+ spec := acp.Spec{Name: "fake", Command: os.Args[0], Env: map[string]string{fakeAgentVariable: "ready"}}
151+ conn, err := acp.Start(context.Background(), spec, acp.Options{})
152+ if err != nil {
153+ fmt.Println(err)
154+ return
155+ }
156+ defer func() { _ = conn.Close(context.Background()) }()
157+ fmt.Println(conn.Info().Title, conn.Info().ProtocolVersion)
158+ // Output: Fake agent 1
159+}
160+
161+func waitFor(t *testing.T, condition func() bool) {
162+ t.Helper()
163+ deadline := time.Now().Add(3 * time.Second)
164+ for !condition() {
165+ if time.Now().After(deadline) {
166+ t.Fatal("condition not met in time")
167+ }
168+ time.Sleep(10 * time.Millisecond)
169+ }
170+}
171+
172+// ---------------------------------------------------------------- the fake agent
173+
174+func runFakeAgent(mode string) int {
175+ switch mode {
176+ case "exit-early":
177+ fmt.Fprintln(os.Stderr, "fake agent: no model configured")
178+ return 2
179+ case "silent":
180+ time.Sleep(time.Minute)
181+ return 0
182+ }
183+ fmt.Fprintln(os.Stderr, "fake agent: ready")
184+ agent := &fakeAgent{mode: mode}
185+ connection := sdk.NewAgentSideConnection(agent, os.Stdout, os.Stdin)
186+ if mode == "crash" {
187+ go func() {
188+ time.Sleep(200 * time.Millisecond)
189+ os.Exit(3)
190+ }()
191+ }
192+ <-connection.Done()
193+ if mode == "stubborn" {
194+ time.Sleep(time.Minute)
195+ }
196+ return 0
197+}
198+
199+type fakeAgent struct{ mode string }
200+
201+func (a *fakeAgent) Initialize(context.Context, sdk.InitializeRequest) (sdk.InitializeResponse, error) {
202+ version := 1
203+ if a.mode == "version-2" {
204+ version = 2
205+ }
206+ title := "Fake agent"
207+ return sdk.InitializeResponse{
208+ ProtocolVersion: sdk.ProtocolVersion(version),
209+ AgentCapabilities: sdk.AgentCapabilities{
210+ LoadSession: true,
211+ PromptCapabilities: sdk.PromptCapabilities{Image: true},
212+ },
213+ AgentInfo: &sdk.Implementation{Name: "fake", Title: &title, Version: "9.9.9"},
214+ AuthMethods: []sdk.AuthMethod{},
215+ }, nil
216+}
217+
218+func (*fakeAgent) Authenticate(context.Context, sdk.AuthenticateRequest) (sdk.AuthenticateResponse, error) {
219+ return sdk.AuthenticateResponse{}, sdk.NewMethodNotFound("authenticate")
220+}
221+func (*fakeAgent) Logout(context.Context, sdk.LogoutRequest) (sdk.LogoutResponse, error) {
222+ return sdk.LogoutResponse{}, sdk.NewMethodNotFound("logout")
223+}
224+func (*fakeAgent) Cancel(context.Context, sdk.CancelNotification) error { return nil }
225+func (*fakeAgent) CloseSession(context.Context, sdk.CloseSessionRequest) (sdk.CloseSessionResponse, error) {
226+ return sdk.CloseSessionResponse{}, sdk.NewMethodNotFound("session/close")
227+}
228+func (*fakeAgent) ListSessions(context.Context, sdk.ListSessionsRequest) (sdk.ListSessionsResponse, error) {
229+ return sdk.ListSessionsResponse{}, sdk.NewMethodNotFound("session/list")
230+}
231+func (*fakeAgent) NewSession(context.Context, sdk.NewSessionRequest) (sdk.NewSessionResponse, error) {
232+ return sdk.NewSessionResponse{}, sdk.NewMethodNotFound("session/new")
233+}
234+func (*fakeAgent) Prompt(context.Context, sdk.PromptRequest) (sdk.PromptResponse, error) {
235+ return sdk.PromptResponse{}, sdk.NewMethodNotFound("session/prompt")
236+}
237+func (*fakeAgent) ResumeSession(context.Context, sdk.ResumeSessionRequest) (sdk.ResumeSessionResponse, error) {
238+ return sdk.ResumeSessionResponse{}, sdk.NewMethodNotFound("session/resume")
239+}
240+func (*fakeAgent) SetSessionConfigOption(context.Context, sdk.SetSessionConfigOptionRequest) (sdk.SetSessionConfigOptionResponse, error) {
241+ return sdk.SetSessionConfigOptionResponse{}, sdk.NewMethodNotFound("session/set_config_option")
242+}
243+func (*fakeAgent) SetSessionMode(context.Context, sdk.SetSessionModeRequest) (sdk.SetSessionModeResponse, error) {
244+ return sdk.SetSessionModeResponse{}, sdk.NewMethodNotFound("session/set_mode")
245+}
new file mode 100644
@@ -0,0 +1,245 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package acp_test
4+
5+import (
6+ "context"
7+ "errors"
8+ "fmt"
9+ "os"
10+ "strings"
11+ "sync"
12+ "testing"
13+ "time"
14+
15+ "git.rickub.com/bots-garden/sidekicks/internal/acp"
16+ sdk "github.com/coder/acp-go-sdk"
17+)
18+
19+// The test binary doubles as a fake agent: started with fakeAgentVariable
20+// set, it serves ACP on stdio in the requested mode instead of running tests.
21+const fakeAgentVariable = "SIDEKICKS_FAKE_AGENT"
22+
23+func TestMain(m *testing.M) {
24+ if mode := os.Getenv(fakeAgentVariable); mode != "" {
25+ os.Exit(runFakeAgent(mode))
26+ }
27+ os.Exit(m.Run())
28+}
29+
30+func fakeSpec(t *testing.T, mode string) acp.Spec {
31+ t.Helper()
32+ return acp.Spec{Name: "fake", Command: os.Args[0], Env: map[string]string{fakeAgentVariable: mode}, Dir: t.TempDir()}
33+}
34+
35+func TestStartPerformsTheHandshake(t *testing.T) {
36+ var mu sync.Mutex
37+ var lines []string
38+ conn, err := acp.Start(context.Background(), fakeSpec(t, "ready"), acp.Options{OnStderr: func(line string) {
39+ mu.Lock()
40+ defer mu.Unlock()
41+ lines = append(lines, line)
42+ }})
43+ if err != nil {
44+ t.Fatal(err)
45+ }
46+ defer func() { _ = conn.Close(context.Background()) }()
47+
48+ info := conn.Info()
49+ if info.Name != "fake" || info.Title != "Fake agent" || info.Version != "9.9.9" || info.ProtocolVersion != 1 || !info.LoadSession || !info.PromptImage {
50+ t.Fatalf("unexpected info: %+v", info)
51+ }
52+ waitFor(t, func() bool {
53+ mu.Lock()
54+ defer mu.Unlock()
55+ return len(lines) > 0 && lines[0] == "fake agent: ready"
56+ })
57+ if got := conn.Stderr(); len(got) == 0 || got[0] != "fake agent: ready" {
58+ t.Fatalf("stderr tail: %v", got)
59+ }
60+}
61+
62+func TestCloseStopsTheAgentGracefully(t *testing.T) {
63+ conn, err := acp.Start(context.Background(), fakeSpec(t, "ready"), acp.Options{})
64+ if err != nil {
65+ t.Fatal(err)
66+ }
67+ start := time.Now()
68+ if err := conn.Close(context.Background()); err != nil {
69+ t.Fatal(err)
70+ }
71+ if elapsed := time.Since(start); elapsed > 2*time.Second {
72+ t.Fatalf("closing stdin should be enough, took %s", elapsed)
73+ }
74+ if conn.Err() != nil {
75+ t.Fatalf("a closed agent has no error, got %v", conn.Err())
76+ }
77+ select {
78+ case <-conn.Done():
79+ default:
80+ t.Fatal("Done must be closed after Close")
81+ }
82+}
83+
84+func TestCloseKillsAnAgentThatIgnoresStdin(t *testing.T) {
85+ conn, err := acp.Start(context.Background(), fakeSpec(t, "stubborn"), acp.Options{CloseGrace: 200 * time.Millisecond})
86+ if err != nil {
87+ t.Fatal(err)
88+ }
89+ start := time.Now()
90+ _ = conn.Close(context.Background())
91+ if elapsed := time.Since(start); elapsed > 3*time.Second {
92+ t.Fatalf("the agent should have been killed after the grace period, took %s", elapsed)
93+ }
94+}
95+
96+func TestACrashIsReported(t *testing.T) {
97+ conn, err := acp.Start(context.Background(), fakeSpec(t, "crash"), acp.Options{})
98+ if err != nil {
99+ t.Fatal(err)
100+ }
101+ select {
102+ case <-conn.Done():
103+ case <-time.After(5 * time.Second):
104+ t.Fatal("the crash was not noticed")
105+ }
106+ if conn.Err() == nil || !strings.Contains(conn.Err().Error(), "exit status 3") {
107+ t.Fatalf("want the exit status in the error, got %v", conn.Err())
108+ }
109+}
110+
111+func TestAnotherProtocolVersionIsRefused(t *testing.T) {
112+ _, err := acp.Start(context.Background(), fakeSpec(t, "version-2"), acp.Options{})
113+ if !errors.Is(err, acp.ErrUnsupportedProtocol) {
114+ t.Fatalf("want ErrUnsupportedProtocol, got %v", err)
115+ }
116+}
117+
118+func TestAStartFailureShowsTheAgentStderr(t *testing.T) {
119+ _, err := acp.Start(context.Background(), fakeSpec(t, "exit-early"), acp.Options{})
120+ var startError *acp.StartError
121+ if !errors.As(err, &startError) {
122+ t.Fatalf("want a *StartError, got %v", err)
123+ }
124+ if !strings.Contains(err.Error(), "fake agent: no model configured") {
125+ t.Fatalf("the agent's stderr should be in the error: %v", err)
126+ }
127+}
128+
129+func TestAMissingCommandFails(t *testing.T) {
130+ _, err := acp.Start(context.Background(), acp.Spec{Name: "ghost", Command: "no-such-agent-xyz"}, acp.Options{})
131+ if err == nil || !strings.Contains(err.Error(), "start agent ghost") {
132+ t.Fatalf("want a start error, got %v", err)
133+ }
134+}
135+
136+func TestInitializeTimesOut(t *testing.T) {
137+ start := time.Now()
138+ _, err := acp.Start(context.Background(), fakeSpec(t, "silent"), acp.Options{InitializeTimeout: 300 * time.Millisecond})
139+ if err == nil || !strings.Contains(err.Error(), "no answer to initialize") {
140+ t.Fatalf("want a timeout, got %v", err)
141+ }
142+ if time.Since(start) > 5*time.Second {
143+ t.Fatal("the silent agent was not killed after the timeout")
144+ }
145+}
146+
147+func ExampleStart() {
148+ // The test binary plays the agent here; with mm it would be
149+ // acp.Spec{Name: "alice", Command: "mm", Args: []string{"-acp", "-config", "agent.yaml"}, Dir: "/path/to/alice"}.
150+ spec := acp.Spec{Name: "fake", Command: os.Args[0], Env: map[string]string{fakeAgentVariable: "ready"}}
151+ conn, err := acp.Start(context.Background(), spec, acp.Options{})
152+ if err != nil {
153+ fmt.Println(err)
154+ return
155+ }
156+ defer func() { _ = conn.Close(context.Background()) }()
157+ fmt.Println(conn.Info().Title, conn.Info().ProtocolVersion)
158+ // Output: Fake agent 1
159+}
160+
161+func waitFor(t *testing.T, condition func() bool) {
162+ t.Helper()
163+ deadline := time.Now().Add(3 * time.Second)
164+ for !condition() {
165+ if time.Now().After(deadline) {
166+ t.Fatal("condition not met in time")
167+ }
168+ time.Sleep(10 * time.Millisecond)
169+ }
170+}
171+
172+// ---------------------------------------------------------------- the fake agent
173+
174+func runFakeAgent(mode string) int {
175+ switch mode {
176+ case "exit-early":
177+ fmt.Fprintln(os.Stderr, "fake agent: no model configured")
178+ return 2
179+ case "silent":
180+ time.Sleep(time.Minute)
181+ return 0
182+ }
183+ fmt.Fprintln(os.Stderr, "fake agent: ready")
184+ agent := &fakeAgent{mode: mode}
185+ connection := sdk.NewAgentSideConnection(agent, os.Stdout, os.Stdin)
186+ if mode == "crash" {
187+ go func() {
188+ time.Sleep(200 * time.Millisecond)
189+ os.Exit(3)
190+ }()
191+ }
192+ <-connection.Done()
193+ if mode == "stubborn" {
194+ time.Sleep(time.Minute)
195+ }
196+ return 0
197+}
198+
199+type fakeAgent struct{ mode string }
200+
201+func (a *fakeAgent) Initialize(context.Context, sdk.InitializeRequest) (sdk.InitializeResponse, error) {
202+ version := 1
203+ if a.mode == "version-2" {
204+ version = 2
205+ }
206+ title := "Fake agent"
207+ return sdk.InitializeResponse{
208+ ProtocolVersion: sdk.ProtocolVersion(version),
209+ AgentCapabilities: sdk.AgentCapabilities{
210+ LoadSession: true,
211+ PromptCapabilities: sdk.PromptCapabilities{Image: true},
212+ },
213+ AgentInfo: &sdk.Implementation{Name: "fake", Title: &title, Version: "9.9.9"},
214+ AuthMethods: []sdk.AuthMethod{},
215+ }, nil
216+}
217+
218+func (*fakeAgent) Authenticate(context.Context, sdk.AuthenticateRequest) (sdk.AuthenticateResponse, error) {
219+ return sdk.AuthenticateResponse{}, sdk.NewMethodNotFound("authenticate")
220+}
221+func (*fakeAgent) Logout(context.Context, sdk.LogoutRequest) (sdk.LogoutResponse, error) {
222+ return sdk.LogoutResponse{}, sdk.NewMethodNotFound("logout")
223+}
224+func (*fakeAgent) Cancel(context.Context, sdk.CancelNotification) error { return nil }
225+func (*fakeAgent) CloseSession(context.Context, sdk.CloseSessionRequest) (sdk.CloseSessionResponse, error) {
226+ return sdk.CloseSessionResponse{}, sdk.NewMethodNotFound("session/close")
227+}
228+func (*fakeAgent) ListSessions(context.Context, sdk.ListSessionsRequest) (sdk.ListSessionsResponse, error) {
229+ return sdk.ListSessionsResponse{}, sdk.NewMethodNotFound("session/list")
230+}
231+func (*fakeAgent) NewSession(context.Context, sdk.NewSessionRequest) (sdk.NewSessionResponse, error) {
232+ return sdk.NewSessionResponse{}, sdk.NewMethodNotFound("session/new")
233+}
234+func (*fakeAgent) Prompt(context.Context, sdk.PromptRequest) (sdk.PromptResponse, error) {
235+ return sdk.PromptResponse{}, sdk.NewMethodNotFound("session/prompt")
236+}
237+func (*fakeAgent) ResumeSession(context.Context, sdk.ResumeSessionRequest) (sdk.ResumeSessionResponse, error) {
238+ return sdk.ResumeSessionResponse{}, sdk.NewMethodNotFound("session/resume")
239+}
240+func (*fakeAgent) SetSessionConfigOption(context.Context, sdk.SetSessionConfigOptionRequest) (sdk.SetSessionConfigOptionResponse, error) {
241+ return sdk.SetSessionConfigOptionResponse{}, sdk.NewMethodNotFound("session/set_config_option")
242+}
243+func (*fakeAgent) SetSessionMode(context.Context, sdk.SetSessionModeRequest) (sdk.SetSessionModeResponse, error) {
244+ return sdk.SetSessionModeResponse{}, sdk.NewMethodNotFound("session/set_mode")
245+}
added internal/acp/process_other.go +14 -0
new file mode 100644
@@ -0,0 +1,14 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+//go:build !unix
4+
5+package acp
6+
7+import "os/exec"
8+
9+// Outside Unix there are no process groups: only the agent itself is killed.
10+func isolateProcessGroup(*exec.Cmd) {}
11+
12+func killProcessGroup(process *exec.Cmd) {
13+ _ = process.Process.Kill()
14+}
new file mode 100644
@@ -0,0 +1,14 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+//go:build !unix
4+
5+package acp
6+
7+import "os/exec"
8+
9+// Outside Unix there are no process groups: only the agent itself is killed.
10+func isolateProcessGroup(*exec.Cmd) {}
11+
12+func killProcessGroup(process *exec.Cmd) {
13+ _ = process.Process.Kill()
14+}
added internal/acp/process_unix.go +20 -0
new file mode 100644
@@ -0,0 +1,20 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+//go:build unix
4+
5+package acp
6+
7+import (
8+ "os/exec"
9+ "syscall"
10+)
11+
12+// isolateProcessGroup starts the agent in its own process group, so that
13+// killing it also kills what it started: scripts/agents.sh runs mm as a child.
14+func isolateProcessGroup(process *exec.Cmd) {
15+ process.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
16+}
17+
18+func killProcessGroup(process *exec.Cmd) {
19+ _ = syscall.Kill(-process.Process.Pid, syscall.SIGKILL)
20+}
new file mode 100644
@@ -0,0 +1,20 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+//go:build unix
4+
5+package acp
6+
7+import (
8+ "os/exec"
9+ "syscall"
10+)
11+
12+// isolateProcessGroup starts the agent in its own process group, so that
13+// killing it also kills what it started: scripts/agents.sh runs mm as a child.
14+func isolateProcessGroup(process *exec.Cmd) {
15+ process.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
16+}
17+
18+func killProcessGroup(process *exec.Cmd) {
19+ _ = syscall.Kill(-process.Process.Pid, syscall.SIGKILL)
20+}
added internal/agents/manager.go +496 -0
new file mode 100644
@@ -0,0 +1,496 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// Package agents owns the agent processes: it starts an agent the first time
4+// it is needed, keeps at most a given number running (each local agent holds a
5+// model in memory), stops the ones left idle, and reports every state change.
6+//
7+// An agent can be in several channels at once but is a single process; the
8+// channels borrow it with Acquire and give it back with the release function:
9+//
10+// conn, release, err := manager.Acquire(ctx, "alice")
11+// if err != nil { … }
12+// defer release()
13+// … talk to conn …
14+package agents
15+
16+import (
17+ "context"
18+ "errors"
19+ "fmt"
20+ "reflect"
21+ "strings"
22+ "sync"
23+ "time"
24+
25+ "git.rickub.com/bots-garden/sidekicks/internal/acp"
26+ "git.rickub.com/bots-garden/sidekicks/internal/config"
27+ "git.rickub.com/bots-garden/sidekicks/internal/events"
28+)
29+
30+// State is where an agent is in its life.
31+type State string
32+
33+// The states, as shown in the Agents panel.
34+const (
35+ StateStopped State = "stopped"
36+ StateStarting State = "starting"
37+ StateReady State = "ready"
38+ StateBusy State = "busy"
39+ StateError State = "error"
40+)
41+
42+// SweepInterval is how often Run looks for idle agents.
43+const SweepInterval = 30 * time.Second
44+
45+// logLines is how many stderr lines are kept per agent.
46+const logLines = 200
47+
48+var (
49+ // ErrUnknownAgent is returned for a name that is not in the configuration.
50+ ErrUnknownAgent = errors.New("unknown agent")
51+ // ErrLimitReached is returned when every running agent is busy and the limit is reached.
52+ ErrLimitReached = errors.New("too many agents running")
53+)
54+
55+// Conn is a running agent, as the manager needs it. *acp.Conn implements it.
56+type Conn interface {
57+ Info() acp.Info
58+ Done() <-chan struct{}
59+ Err() error
60+ Close(ctx context.Context) error
61+}
62+
63+// Starter starts an agent. ACPStarter is the real one; tests pass a fake.
64+type Starter func(ctx context.Context, spec acp.Spec, onStderr func(line string)) (Conn, error)
65+
66+// ACPStarter starts the agent with the Agent Client Protocol.
67+func ACPStarter(ctx context.Context, spec acp.Spec, onStderr func(string)) (Conn, error) {
68+ conn, err := acp.Start(ctx, spec, acp.Options{OnStderr: onStderr})
69+ if err != nil {
70+ return nil, err
71+ }
72+ return conn, nil
73+}
74+
75+// Options configure a Manager.
76+type Options struct {
77+ // MaxRunning agents at the same time (limits.maxRunningAgents).
78+ MaxRunning int
79+ // IdleTimeout stops an agent unused for this long; 0 never does (limits.stopIdleAgentsAfter).
80+ IdleTimeout time.Duration
81+ // Emitter receives agent:state and agent:log events. It must not block
82+ // nor call the Manager: events are emitted in order, under the manager's lock.
83+ Emitter events.Emitter
84+ // Starter defaults to ACPStarter.
85+ Starter Starter
86+ // Now defaults to time.Now; tests control the clock with it.
87+ Now func() time.Time
88+}
89+
90+// Status is the public view of an agent.
91+type Status struct {
92+ Name string `json:"name"`
93+ Title string `json:"title"`
94+ Icon string `json:"icon"`
95+ Description string `json:"description"`
96+ State State `json:"state"`
97+ // Error explains StateError.
98+ Error string `json:"error,omitempty"`
99+ // Outdated: the configuration of this running agent changed; restart it to apply.
100+ Outdated bool `json:"outdated"`
101+}
102+
103+// Manager owns the agent processes. It is safe for concurrent use.
104+type Manager struct {
105+ mu sync.Mutex
106+ options Options
107+ order []string
108+ agents map[string]*agent
109+}
110+
111+type agent struct {
112+ definition config.Agent
113+ state State
114+ err string
115+ conn Conn
116+ users int
117+ lastUsed time.Time
118+ // starting is closed when the current start attempt ends.
119+ starting chan struct{}
120+ outdated bool
121+ // stopRequested: Stop was called while the agent was starting.
122+ stopRequested bool
123+ logs []string
124+}
125+
126+// NewManager returns a manager for the agents of the configuration. No agent
127+// is started until it is needed.
128+func NewManager(definitions []config.Agent, options Options) *Manager {
129+ if options.Emitter == nil {
130+ options.Emitter = events.Discard
131+ }
132+ if options.Starter == nil {
133+ options.Starter = ACPStarter
134+ }
135+ if options.Now == nil {
136+ options.Now = time.Now
137+ }
138+ if options.MaxRunning < 1 {
139+ options.MaxRunning = config.DefaultMaxRunningAgents
140+ }
141+ manager := &Manager{options: options, agents: map[string]*agent{}}
142+ for _, definition := range definitions {
143+ manager.order = append(manager.order, definition.Name)
144+ manager.agents[definition.Name] = &agent{definition: definition, state: StateStopped}
145+ }
146+ return manager
147+}
148+
149+// Statuses returns every agent, in the order of the configuration.
150+func (m *Manager) Statuses() []Status {
151+ m.mu.Lock()
152+ defer m.mu.Unlock()
153+ statuses := make([]Status, 0, len(m.order))
154+ for _, name := range m.order {
155+ statuses = append(statuses, m.agents[name].status())
156+ }
157+ return statuses
158+}
159+
160+func (a *agent) status() Status {
161+ return Status{
162+ Name: a.definition.Name, Title: a.definition.Title, Icon: a.definition.Icon,
163+ Description: a.definition.Description, State: a.state, Error: a.err, Outdated: a.outdated,
164+ }
165+}
166+
167+// Logs returns the last lines the agent wrote on its standard error.
168+func (m *Manager) Logs(name string) ([]string, error) {
169+ m.mu.Lock()
170+ defer m.mu.Unlock()
171+ a, ok := m.agents[name]
172+ if !ok {
173+ return nil, fmt.Errorf("%w: %s", ErrUnknownAgent, name)
174+ }
175+ return append([]string(nil), a.logs...), nil
176+}
177+
178+// Start starts the agent if it is not running yet, without using it.
179+func (m *Manager) Start(ctx context.Context, name string) error {
180+ _, err := m.ensureStarted(ctx, name)
181+ return err
182+}
183+
184+// Acquire starts the agent if needed and marks it busy until release is
185+// called. release can be called more than once.
186+func (m *Manager) Acquire(ctx context.Context, name string) (Conn, func(), error) {
187+ a, err := m.ensureStarted(ctx, name)
188+ if err != nil {
189+ return nil, nil, err
190+ }
191+ m.mu.Lock()
192+ defer m.mu.Unlock()
193+ if a.conn == nil {
194+ return nil, nil, fmt.Errorf("agent %s stopped while starting", name)
195+ }
196+ conn := a.conn
197+ a.users++
198+ a.lastUsed = m.options.Now()
199+ m.setState(a, StateBusy, "")
200+ var once sync.Once
201+ release := func() {
202+ once.Do(func() {
203+ m.mu.Lock()
204+ defer m.mu.Unlock()
205+ if a.conn != conn {
206+ return // stopped or restarted meanwhile
207+ }
208+ a.users--
209+ a.lastUsed = m.options.Now()
210+ if a.users == 0 {
211+ m.setState(a, StateReady, "")
212+ }
213+ })
214+ }
215+ return conn, release, nil
216+}
217+
218+// ensureStarted returns the agent once it runs, starting it if needed and
219+// waiting for a start already in progress.
220+func (m *Manager) ensureStarted(ctx context.Context, name string) (*agent, error) {
221+ m.mu.Lock()
222+ a, ok := m.agents[name]
223+ if !ok {
224+ m.mu.Unlock()
225+ return nil, fmt.Errorf("%w: %s", ErrUnknownAgent, name)
226+ }
227+ for a.state == StateStarting {
228+ starting := a.starting
229+ m.mu.Unlock()
230+ select {
231+ case <-starting:
232+ case <-ctx.Done():
233+ return nil, ctx.Err()
234+ }
235+ m.mu.Lock()
236+ }
237+ if a.state == StateReady || a.state == StateBusy {
238+ m.mu.Unlock()
239+ return a, nil
240+ }
241+ evicted, err := m.makeRoom(a)
242+ if err != nil {
243+ m.mu.Unlock()
244+ return nil, err
245+ }
246+ a.starting = make(chan struct{})
247+ m.setState(a, StateStarting, "")
248+ spec := specFor(a.definition)
249+ m.mu.Unlock()
250+
251+ for _, conn := range evicted {
252+ _ = conn.Close(ctx)
253+ }
254+ conn, startErr := m.options.Starter(ctx, spec, func(line string) { m.log(a, line) })
255+
256+ m.mu.Lock()
257+ defer m.mu.Unlock()
258+ defer close(a.starting)
259+ if startErr != nil {
260+ a.stopRequested = false
261+ m.setState(a, StateError, startErr.Error())
262+ return nil, startErr
263+ }
264+ if a.stopRequested {
265+ a.stopRequested = false
266+ m.setState(a, StateStopped, "")
267+ go func() { _ = conn.Close(context.Background()) }()
268+ return nil, fmt.Errorf("agent %s was stopped while starting", a.definition.Name)
269+ }
270+ a.conn, a.users, a.outdated = conn, 0, false
271+ a.lastUsed = m.options.Now()
272+ m.setState(a, StateReady, "")
273+ go m.watch(a, conn)
274+ return a, nil
275+}
276+
277+// makeRoom keeps the number of running agents under the limit by stopping
278+// the least recently used idle agents. It returns their connections, to be
279+// closed outside the lock.
280+func (m *Manager) makeRoom(starting *agent) ([]Conn, error) {
281+ var evicted []Conn
282+ for m.running() >= m.options.MaxRunning {
283+ victim := m.leastRecentlyUsedIdle(starting)
284+ if victim == nil {
285+ return evicted, fmt.Errorf("%w: %d agent(s) busy (%s), the limit is %d",
286+ ErrLimitReached, m.running(), strings.Join(m.runningNames(), ", "), m.options.MaxRunning)
287+ }
288+ evicted = append(evicted, victim.conn)
289+ victim.conn = nil
290+ m.setState(victim, StateStopped, "")
291+ }
292+ return evicted, nil
293+}
294+
295+func (m *Manager) running() int {
296+ count := 0
297+ for _, a := range m.agents {
298+ if a.state == StateStarting || a.state == StateReady || a.state == StateBusy {
299+ count++
300+ }
301+ }
302+ return count
303+}
304+
305+func (m *Manager) runningNames() []string {
306+ var names []string
307+ for _, name := range m.order {
308+ if state := m.agents[name].state; state == StateStarting || state == StateReady || state == StateBusy {
309+ names = append(names, name)
310+ }
311+ }
312+ return names
313+}
314+
315+func (m *Manager) leastRecentlyUsedIdle(except *agent) *agent {
316+ var victim *agent
317+ for _, name := range m.order {
318+ a := m.agents[name]
319+ if a == except || a.state != StateReady || a.users > 0 {
320+ continue
321+ }
322+ if victim == nil || a.lastUsed.Before(victim.lastUsed) {
323+ victim = a
324+ }
325+ }
326+ return victim
327+}
328+
329+// watch notices an agent that exits on its own (a crash): it goes to the
330+// error state, and is only restarted when someone asks (Restart, Acquire).
331+func (m *Manager) watch(a *agent, conn Conn) {
332+ <-conn.Done()
333+ m.mu.Lock()
334+ defer m.mu.Unlock()
335+ if a.conn != conn {
336+ return // stopped on purpose
337+ }
338+ reason := "the agent stopped"
339+ if err := conn.Err(); err != nil {
340+ reason = err.Error()
341+ }
342+ a.conn, a.users = nil, 0
343+ m.setState(a, StateError, reason)
344+}
345+
346+// Stop stops the agent, even if busy. Stopping a stopped agent does nothing.
347+func (m *Manager) Stop(ctx context.Context, name string) error {
348+ m.mu.Lock()
349+ a, ok := m.agents[name]
350+ if !ok {
351+ m.mu.Unlock()
352+ return fmt.Errorf("%w: %s", ErrUnknownAgent, name)
353+ }
354+ conn := a.conn
355+ a.conn, a.users = nil, 0
356+ if a.state == StateStarting {
357+ a.stopRequested = true
358+ } else {
359+ m.setState(a, StateStopped, "")
360+ }
361+ m.mu.Unlock()
362+ if conn == nil {
363+ return nil
364+ }
365+ return conn.Close(ctx)
366+}
367+
368+// Restart stops then starts the agent, e.g. after a crash or a configuration change.
369+func (m *Manager) Restart(ctx context.Context, name string) error {
370+ if err := m.Stop(ctx, name); err != nil {
371+ return err
372+ }
373+ return m.Start(ctx, name)
374+}
375+
376+// StopAll stops every running agent, typically when the application quits.
377+func (m *Manager) StopAll(ctx context.Context) {
378+ var wg sync.WaitGroup
379+ for _, status := range m.Statuses() {
380+ wg.Add(1)
381+ go func(name string) {
382+ defer wg.Done()
383+ _ = m.Stop(ctx, name)
384+ }(status.Name)
385+ }
386+ wg.Wait()
387+}
388+
389+// StopIdle stops the agents nobody has used for IdleTimeout and returns their names.
390+func (m *Manager) StopIdle(ctx context.Context) []string {
391+ if m.options.IdleTimeout <= 0 {
392+ return nil
393+ }
394+ m.mu.Lock()
395+ now := m.options.Now()
396+ var idle []string
397+ for _, name := range m.order {
398+ a := m.agents[name]
399+ if a.state == StateReady && a.users == 0 && now.Sub(a.lastUsed) >= m.options.IdleTimeout {
400+ idle = append(idle, name)
401+ }
402+ }
403+ m.mu.Unlock()
404+ for _, name := range idle {
405+ _ = m.Stop(ctx, name)
406+ }
407+ return idle
408+}
409+
410+// Run stops idle agents every SweepInterval until ctx is done.
411+func (m *Manager) Run(ctx context.Context) {
412+ ticker := time.NewTicker(SweepInterval)
413+ defer ticker.Stop()
414+ for {
415+ select {
416+ case <-ctx.Done():
417+ return
418+ case <-ticker.C:
419+ m.StopIdle(ctx)
420+ }
421+ }
422+}
423+
424+// Reconfigure applies a new configuration: new agents appear stopped, removed
425+// agents are stopped and forgotten, and running agents whose definition
426+// changed keep running but are marked Outdated until restarted.
427+func (m *Manager) Reconfigure(ctx context.Context, definitions []config.Agent, maxRunning int, idleTimeout time.Duration) {
428+ m.mu.Lock()
429+ if maxRunning > 0 {
430+ m.options.MaxRunning = maxRunning
431+ }
432+ m.options.IdleTimeout = idleTimeout
433+ kept := map[string]bool{}
434+ var order []string
435+ for _, definition := range definitions {
436+ kept[definition.Name] = true
437+ order = append(order, definition.Name)
438+ a, exists := m.agents[definition.Name]
439+ if !exists {
440+ m.agents[definition.Name] = &agent{definition: definition, state: StateStopped}
441+ m.emitState(m.agents[definition.Name])
442+ continue
443+ }
444+ if !reflect.DeepEqual(a.definition, definition) {
445+ a.outdated = a.conn != nil
446+ a.definition = definition
447+ m.emitState(a)
448+ }
449+ }
450+ var removed []Conn
451+ for name, a := range m.agents {
452+ if kept[name] {
453+ continue
454+ }
455+ if a.conn != nil {
456+ removed = append(removed, a.conn)
457+ }
458+ delete(m.agents, name)
459+ }
460+ m.order = order
461+ m.mu.Unlock()
462+ for _, conn := range removed {
463+ _ = conn.Close(ctx)
464+ }
465+}
466+
467+func specFor(definition config.Agent) acp.Spec {
468+ return acp.Spec{
469+ Name: definition.Name, Command: definition.Command, Args: definition.Args,
470+ Env: definition.Env, Dir: definition.Workspace,
471+ }
472+}
473+
474+func (m *Manager) log(a *agent, line string) {
475+ m.mu.Lock()
476+ defer m.mu.Unlock()
477+ a.logs = append(a.logs, line)
478+ if len(a.logs) > logLines {
479+ a.logs = a.logs[len(a.logs)-logLines:]
480+ }
481+ m.options.Emitter.Emit(events.AgentLog, events.AgentLogPayload{Agent: a.definition.Name, Line: line})
482+}
483+
484+// setState changes the state and emits agent:state when it actually changes.
485+// The caller holds m.mu.
486+func (m *Manager) setState(a *agent, state State, reason string) {
487+ if a.state == state && a.err == reason {
488+ return
489+ }
490+ a.state, a.err = state, reason
491+ m.emitState(a)
492+}
493+
494+func (m *Manager) emitState(a *agent) {
495+ m.options.Emitter.Emit(events.AgentState, events.AgentStatePayload{Agent: a.definition.Name, State: string(a.state), Error: a.err})
496+}
new file mode 100644
@@ -0,0 +1,496 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// Package agents owns the agent processes: it starts an agent the first time
4+// it is needed, keeps at most a given number running (each local agent holds a
5+// model in memory), stops the ones left idle, and reports every state change.
6+//
7+// An agent can be in several channels at once but is a single process; the
8+// channels borrow it with Acquire and give it back with the release function:
9+//
10+// conn, release, err := manager.Acquire(ctx, "alice")
11+// if err != nil { … }
12+// defer release()
13+// … talk to conn …
14+package agents
15+
16+import (
17+ "context"
18+ "errors"
19+ "fmt"
20+ "reflect"
21+ "strings"
22+ "sync"
23+ "time"
24+
25+ "git.rickub.com/bots-garden/sidekicks/internal/acp"
26+ "git.rickub.com/bots-garden/sidekicks/internal/config"
27+ "git.rickub.com/bots-garden/sidekicks/internal/events"
28+)
29+
30+// State is where an agent is in its life.
31+type State string
32+
33+// The states, as shown in the Agents panel.
34+const (
35+ StateStopped State = "stopped"
36+ StateStarting State = "starting"
37+ StateReady State = "ready"
38+ StateBusy State = "busy"
39+ StateError State = "error"
40+)
41+
42+// SweepInterval is how often Run looks for idle agents.
43+const SweepInterval = 30 * time.Second
44+
45+// logLines is how many stderr lines are kept per agent.
46+const logLines = 200
47+
48+var (
49+ // ErrUnknownAgent is returned for a name that is not in the configuration.
50+ ErrUnknownAgent = errors.New("unknown agent")
51+ // ErrLimitReached is returned when every running agent is busy and the limit is reached.
52+ ErrLimitReached = errors.New("too many agents running")
53+)
54+
55+// Conn is a running agent, as the manager needs it. *acp.Conn implements it.
56+type Conn interface {
57+ Info() acp.Info
58+ Done() <-chan struct{}
59+ Err() error
60+ Close(ctx context.Context) error
61+}
62+
63+// Starter starts an agent. ACPStarter is the real one; tests pass a fake.
64+type Starter func(ctx context.Context, spec acp.Spec, onStderr func(line string)) (Conn, error)
65+
66+// ACPStarter starts the agent with the Agent Client Protocol.
67+func ACPStarter(ctx context.Context, spec acp.Spec, onStderr func(string)) (Conn, error) {
68+ conn, err := acp.Start(ctx, spec, acp.Options{OnStderr: onStderr})
69+ if err != nil {
70+ return nil, err
71+ }
72+ return conn, nil
73+}
74+
75+// Options configure a Manager.
76+type Options struct {
77+ // MaxRunning agents at the same time (limits.maxRunningAgents).
78+ MaxRunning int
79+ // IdleTimeout stops an agent unused for this long; 0 never does (limits.stopIdleAgentsAfter).
80+ IdleTimeout time.Duration
81+ // Emitter receives agent:state and agent:log events. It must not block
82+ // nor call the Manager: events are emitted in order, under the manager's lock.
83+ Emitter events.Emitter
84+ // Starter defaults to ACPStarter.
85+ Starter Starter
86+ // Now defaults to time.Now; tests control the clock with it.
87+ Now func() time.Time
88+}
89+
90+// Status is the public view of an agent.
91+type Status struct {
92+ Name string `json:"name"`
93+ Title string `json:"title"`
94+ Icon string `json:"icon"`
95+ Description string `json:"description"`
96+ State State `json:"state"`
97+ // Error explains StateError.
98+ Error string `json:"error,omitempty"`
99+ // Outdated: the configuration of this running agent changed; restart it to apply.
100+ Outdated bool `json:"outdated"`
101+}
102+
103+// Manager owns the agent processes. It is safe for concurrent use.
104+type Manager struct {
105+ mu sync.Mutex
106+ options Options
107+ order []string
108+ agents map[string]*agent
109+}
110+
111+type agent struct {
112+ definition config.Agent
113+ state State
114+ err string
115+ conn Conn
116+ users int
117+ lastUsed time.Time
118+ // starting is closed when the current start attempt ends.
119+ starting chan struct{}
120+ outdated bool
121+ // stopRequested: Stop was called while the agent was starting.
122+ stopRequested bool
123+ logs []string
124+}
125+
126+// NewManager returns a manager for the agents of the configuration. No agent
127+// is started until it is needed.
128+func NewManager(definitions []config.Agent, options Options) *Manager {
129+ if options.Emitter == nil {
130+ options.Emitter = events.Discard
131+ }
132+ if options.Starter == nil {
133+ options.Starter = ACPStarter
134+ }
135+ if options.Now == nil {
136+ options.Now = time.Now
137+ }
138+ if options.MaxRunning < 1 {
139+ options.MaxRunning = config.DefaultMaxRunningAgents
140+ }
141+ manager := &Manager{options: options, agents: map[string]*agent{}}
142+ for _, definition := range definitions {
143+ manager.order = append(manager.order, definition.Name)
144+ manager.agents[definition.Name] = &agent{definition: definition, state: StateStopped}
145+ }
146+ return manager
147+}
148+
149+// Statuses returns every agent, in the order of the configuration.
150+func (m *Manager) Statuses() []Status {
151+ m.mu.Lock()
152+ defer m.mu.Unlock()
153+ statuses := make([]Status, 0, len(m.order))
154+ for _, name := range m.order {
155+ statuses = append(statuses, m.agents[name].status())
156+ }
157+ return statuses
158+}
159+
160+func (a *agent) status() Status {
161+ return Status{
162+ Name: a.definition.Name, Title: a.definition.Title, Icon: a.definition.Icon,
163+ Description: a.definition.Description, State: a.state, Error: a.err, Outdated: a.outdated,
164+ }
165+}
166+
167+// Logs returns the last lines the agent wrote on its standard error.
168+func (m *Manager) Logs(name string) ([]string, error) {
169+ m.mu.Lock()
170+ defer m.mu.Unlock()
171+ a, ok := m.agents[name]
172+ if !ok {
173+ return nil, fmt.Errorf("%w: %s", ErrUnknownAgent, name)
174+ }
175+ return append([]string(nil), a.logs...), nil
176+}
177+
178+// Start starts the agent if it is not running yet, without using it.
179+func (m *Manager) Start(ctx context.Context, name string) error {
180+ _, err := m.ensureStarted(ctx, name)
181+ return err
182+}
183+
184+// Acquire starts the agent if needed and marks it busy until release is
185+// called. release can be called more than once.
186+func (m *Manager) Acquire(ctx context.Context, name string) (Conn, func(), error) {
187+ a, err := m.ensureStarted(ctx, name)
188+ if err != nil {
189+ return nil, nil, err
190+ }
191+ m.mu.Lock()
192+ defer m.mu.Unlock()
193+ if a.conn == nil {
194+ return nil, nil, fmt.Errorf("agent %s stopped while starting", name)
195+ }
196+ conn := a.conn
197+ a.users++
198+ a.lastUsed = m.options.Now()
199+ m.setState(a, StateBusy, "")
200+ var once sync.Once
201+ release := func() {
202+ once.Do(func() {
203+ m.mu.Lock()
204+ defer m.mu.Unlock()
205+ if a.conn != conn {
206+ return // stopped or restarted meanwhile
207+ }
208+ a.users--
209+ a.lastUsed = m.options.Now()
210+ if a.users == 0 {
211+ m.setState(a, StateReady, "")
212+ }
213+ })
214+ }
215+ return conn, release, nil
216+}
217+
218+// ensureStarted returns the agent once it runs, starting it if needed and
219+// waiting for a start already in progress.
220+func (m *Manager) ensureStarted(ctx context.Context, name string) (*agent, error) {
221+ m.mu.Lock()
222+ a, ok := m.agents[name]
223+ if !ok {
224+ m.mu.Unlock()
225+ return nil, fmt.Errorf("%w: %s", ErrUnknownAgent, name)
226+ }
227+ for a.state == StateStarting {
228+ starting := a.starting
229+ m.mu.Unlock()
230+ select {
231+ case <-starting:
232+ case <-ctx.Done():
233+ return nil, ctx.Err()
234+ }
235+ m.mu.Lock()
236+ }
237+ if a.state == StateReady || a.state == StateBusy {
238+ m.mu.Unlock()
239+ return a, nil
240+ }
241+ evicted, err := m.makeRoom(a)
242+ if err != nil {
243+ m.mu.Unlock()
244+ return nil, err
245+ }
246+ a.starting = make(chan struct{})
247+ m.setState(a, StateStarting, "")
248+ spec := specFor(a.definition)
249+ m.mu.Unlock()
250+
251+ for _, conn := range evicted {
252+ _ = conn.Close(ctx)
253+ }
254+ conn, startErr := m.options.Starter(ctx, spec, func(line string) { m.log(a, line) })
255+
256+ m.mu.Lock()
257+ defer m.mu.Unlock()
258+ defer close(a.starting)
259+ if startErr != nil {
260+ a.stopRequested = false
261+ m.setState(a, StateError, startErr.Error())
262+ return nil, startErr
263+ }
264+ if a.stopRequested {
265+ a.stopRequested = false
266+ m.setState(a, StateStopped, "")
267+ go func() { _ = conn.Close(context.Background()) }()
268+ return nil, fmt.Errorf("agent %s was stopped while starting", a.definition.Name)
269+ }
270+ a.conn, a.users, a.outdated = conn, 0, false
271+ a.lastUsed = m.options.Now()
272+ m.setState(a, StateReady, "")
273+ go m.watch(a, conn)
274+ return a, nil
275+}
276+
277+// makeRoom keeps the number of running agents under the limit by stopping
278+// the least recently used idle agents. It returns their connections, to be
279+// closed outside the lock.
280+func (m *Manager) makeRoom(starting *agent) ([]Conn, error) {
281+ var evicted []Conn
282+ for m.running() >= m.options.MaxRunning {
283+ victim := m.leastRecentlyUsedIdle(starting)
284+ if victim == nil {
285+ return evicted, fmt.Errorf("%w: %d agent(s) busy (%s), the limit is %d",
286+ ErrLimitReached, m.running(), strings.Join(m.runningNames(), ", "), m.options.MaxRunning)
287+ }
288+ evicted = append(evicted, victim.conn)
289+ victim.conn = nil
290+ m.setState(victim, StateStopped, "")
291+ }
292+ return evicted, nil
293+}
294+
295+func (m *Manager) running() int {
296+ count := 0
297+ for _, a := range m.agents {
298+ if a.state == StateStarting || a.state == StateReady || a.state == StateBusy {
299+ count++
300+ }
301+ }
302+ return count
303+}
304+
305+func (m *Manager) runningNames() []string {
306+ var names []string
307+ for _, name := range m.order {
308+ if state := m.agents[name].state; state == StateStarting || state == StateReady || state == StateBusy {
309+ names = append(names, name)
310+ }
311+ }
312+ return names
313+}
314+
315+func (m *Manager) leastRecentlyUsedIdle(except *agent) *agent {
316+ var victim *agent
317+ for _, name := range m.order {
318+ a := m.agents[name]
319+ if a == except || a.state != StateReady || a.users > 0 {
320+ continue
321+ }
322+ if victim == nil || a.lastUsed.Before(victim.lastUsed) {
323+ victim = a
324+ }
325+ }
326+ return victim
327+}
328+
329+// watch notices an agent that exits on its own (a crash): it goes to the
330+// error state, and is only restarted when someone asks (Restart, Acquire).
331+func (m *Manager) watch(a *agent, conn Conn) {
332+ <-conn.Done()
333+ m.mu.Lock()
334+ defer m.mu.Unlock()
335+ if a.conn != conn {
336+ return // stopped on purpose
337+ }
338+ reason := "the agent stopped"
339+ if err := conn.Err(); err != nil {
340+ reason = err.Error()
341+ }
342+ a.conn, a.users = nil, 0
343+ m.setState(a, StateError, reason)
344+}
345+
346+// Stop stops the agent, even if busy. Stopping a stopped agent does nothing.
347+func (m *Manager) Stop(ctx context.Context, name string) error {
348+ m.mu.Lock()
349+ a, ok := m.agents[name]
350+ if !ok {
351+ m.mu.Unlock()
352+ return fmt.Errorf("%w: %s", ErrUnknownAgent, name)
353+ }
354+ conn := a.conn
355+ a.conn, a.users = nil, 0
356+ if a.state == StateStarting {
357+ a.stopRequested = true
358+ } else {
359+ m.setState(a, StateStopped, "")
360+ }
361+ m.mu.Unlock()
362+ if conn == nil {
363+ return nil
364+ }
365+ return conn.Close(ctx)
366+}
367+
368+// Restart stops then starts the agent, e.g. after a crash or a configuration change.
369+func (m *Manager) Restart(ctx context.Context, name string) error {
370+ if err := m.Stop(ctx, name); err != nil {
371+ return err
372+ }
373+ return m.Start(ctx, name)
374+}
375+
376+// StopAll stops every running agent, typically when the application quits.
377+func (m *Manager) StopAll(ctx context.Context) {
378+ var wg sync.WaitGroup
379+ for _, status := range m.Statuses() {
380+ wg.Add(1)
381+ go func(name string) {
382+ defer wg.Done()
383+ _ = m.Stop(ctx, name)
384+ }(status.Name)
385+ }
386+ wg.Wait()
387+}
388+
389+// StopIdle stops the agents nobody has used for IdleTimeout and returns their names.
390+func (m *Manager) StopIdle(ctx context.Context) []string {
391+ if m.options.IdleTimeout <= 0 {
392+ return nil
393+ }
394+ m.mu.Lock()
395+ now := m.options.Now()
396+ var idle []string
397+ for _, name := range m.order {
398+ a := m.agents[name]
399+ if a.state == StateReady && a.users == 0 && now.Sub(a.lastUsed) >= m.options.IdleTimeout {
400+ idle = append(idle, name)
401+ }
402+ }
403+ m.mu.Unlock()
404+ for _, name := range idle {
405+ _ = m.Stop(ctx, name)
406+ }
407+ return idle
408+}
409+
410+// Run stops idle agents every SweepInterval until ctx is done.
411+func (m *Manager) Run(ctx context.Context) {
412+ ticker := time.NewTicker(SweepInterval)
413+ defer ticker.Stop()
414+ for {
415+ select {
416+ case <-ctx.Done():
417+ return
418+ case <-ticker.C:
419+ m.StopIdle(ctx)
420+ }
421+ }
422+}
423+
424+// Reconfigure applies a new configuration: new agents appear stopped, removed
425+// agents are stopped and forgotten, and running agents whose definition
426+// changed keep running but are marked Outdated until restarted.
427+func (m *Manager) Reconfigure(ctx context.Context, definitions []config.Agent, maxRunning int, idleTimeout time.Duration) {
428+ m.mu.Lock()
429+ if maxRunning > 0 {
430+ m.options.MaxRunning = maxRunning
431+ }
432+ m.options.IdleTimeout = idleTimeout
433+ kept := map[string]bool{}
434+ var order []string
435+ for _, definition := range definitions {
436+ kept[definition.Name] = true
437+ order = append(order, definition.Name)
438+ a, exists := m.agents[definition.Name]
439+ if !exists {
440+ m.agents[definition.Name] = &agent{definition: definition, state: StateStopped}
441+ m.emitState(m.agents[definition.Name])
442+ continue
443+ }
444+ if !reflect.DeepEqual(a.definition, definition) {
445+ a.outdated = a.conn != nil
446+ a.definition = definition
447+ m.emitState(a)
448+ }
449+ }
450+ var removed []Conn
451+ for name, a := range m.agents {
452+ if kept[name] {
453+ continue
454+ }
455+ if a.conn != nil {
456+ removed = append(removed, a.conn)
457+ }
458+ delete(m.agents, name)
459+ }
460+ m.order = order
461+ m.mu.Unlock()
462+ for _, conn := range removed {
463+ _ = conn.Close(ctx)
464+ }
465+}
466+
467+func specFor(definition config.Agent) acp.Spec {
468+ return acp.Spec{
469+ Name: definition.Name, Command: definition.Command, Args: definition.Args,
470+ Env: definition.Env, Dir: definition.Workspace,
471+ }
472+}
473+
474+func (m *Manager) log(a *agent, line string) {
475+ m.mu.Lock()
476+ defer m.mu.Unlock()
477+ a.logs = append(a.logs, line)
478+ if len(a.logs) > logLines {
479+ a.logs = a.logs[len(a.logs)-logLines:]
480+ }
481+ m.options.Emitter.Emit(events.AgentLog, events.AgentLogPayload{Agent: a.definition.Name, Line: line})
482+}
483+
484+// setState changes the state and emits agent:state when it actually changes.
485+// The caller holds m.mu.
486+func (m *Manager) setState(a *agent, state State, reason string) {
487+ if a.state == state && a.err == reason {
488+ return
489+ }
490+ a.state, a.err = state, reason
491+ m.emitState(a)
492+}
493+
494+func (m *Manager) emitState(a *agent) {
495+ m.options.Emitter.Emit(events.AgentState, events.AgentStatePayload{Agent: a.definition.Name, State: string(a.state), Error: a.err})
496+}
added internal/agents/manager_test.go +431 -0
new file mode 100644
@@ -0,0 +1,431 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package agents_test
4+
5+import (
6+ "context"
7+ "errors"
8+ "fmt"
9+ "sync"
10+ "testing"
11+ "time"
12+
13+ "git.rickub.com/bots-garden/sidekicks/internal/acp"
14+ "git.rickub.com/bots-garden/sidekicks/internal/agents"
15+ "git.rickub.com/bots-garden/sidekicks/internal/config"
16+ "git.rickub.com/bots-garden/sidekicks/internal/events"
17+)
18+
19+// ---------------------------------------------------------------- fakes
20+
21+type fakeConn struct {
22+ name string
23+ done chan struct{}
24+ once sync.Once
25+ mu sync.Mutex
26+ exitErr error
27+ closedBy string
28+}
29+
30+func newFakeConn(name string) *fakeConn { return &fakeConn{name: name, done: make(chan struct{})} }
31+
32+func (c *fakeConn) Info() acp.Info { return acp.Info{Name: c.name} }
33+func (c *fakeConn) Done() <-chan struct{} { return c.done }
34+func (c *fakeConn) Err() error {
35+ c.mu.Lock()
36+ defer c.mu.Unlock()
37+ return c.exitErr
38+}
39+func (c *fakeConn) Close(context.Context) error {
40+ c.mu.Lock()
41+ c.closedBy = "close"
42+ c.mu.Unlock()
43+ c.once.Do(func() { close(c.done) })
44+ return nil
45+}
46+func (c *fakeConn) crash(err error) {
47+ c.mu.Lock()
48+ c.exitErr = err
49+ c.mu.Unlock()
50+ c.once.Do(func() { close(c.done) })
51+}
52+func (c *fakeConn) closed() bool {
53+ c.mu.Lock()
54+ defer c.mu.Unlock()
55+ return c.closedBy == "close"
56+}
57+
58+// fakeStarter records starts; an agent named in fail cannot start, and gate,
59+// when set, holds every start until it is closed.
60+type fakeStarter struct {
61+ mu sync.Mutex
62+ starts map[string]int
63+ conns map[string]*fakeConn
64+ fail map[string]error
65+ gate chan struct{}
66+}
67+
68+func newFakeStarter() *fakeStarter {
69+ return &fakeStarter{starts: map[string]int{}, conns: map[string]*fakeConn{}, fail: map[string]error{}}
70+}
71+
72+func (s *fakeStarter) start(_ context.Context, spec acp.Spec, onStderr func(string)) (agents.Conn, error) {
73+ if s.gate != nil {
74+ <-s.gate
75+ }
76+ s.mu.Lock()
77+ defer s.mu.Unlock()
78+ s.starts[spec.Name]++
79+ if err := s.fail[spec.Name]; err != nil {
80+ return nil, err
81+ }
82+ onStderr(spec.Name + " started in " + spec.Dir)
83+ conn := newFakeConn(spec.Name)
84+ s.conns[spec.Name] = conn
85+ return conn, nil
86+}
87+
88+func (s *fakeStarter) count(name string) int {
89+ s.mu.Lock()
90+ defer s.mu.Unlock()
91+ return s.starts[name]
92+}
93+
94+func (s *fakeStarter) conn(name string) *fakeConn {
95+ s.mu.Lock()
96+ defer s.mu.Unlock()
97+ return s.conns[name]
98+}
99+
100+type clock struct {
101+ mu sync.Mutex
102+ now time.Time
103+}
104+
105+func (c *clock) Now() time.Time {
106+ c.mu.Lock()
107+ defer c.mu.Unlock()
108+ return c.now
109+}
110+func (c *clock) advance(d time.Duration) {
111+ c.mu.Lock()
112+ defer c.mu.Unlock()
113+ c.now = c.now.Add(d)
114+}
115+
116+func definitions(names ...string) []config.Agent {
117+ var result []config.Agent
118+ for _, name := range names {
119+ result = append(result, config.Agent{Name: name, Title: name, Command: "mm", Workspace: "/agents/" + name})
120+ }
121+ return result
122+}
123+
124+type fixture struct {
125+ manager *agents.Manager
126+ starter *fakeStarter
127+ recorder *events.Recorder
128+ clock *clock
129+}
130+
131+// maxRunning is 2 in every test, like the default limit.
132+const maxRunning = 2
133+
134+func setup(idle time.Duration, names ...string) *fixture {
135+ f := &fixture{starter: newFakeStarter(), recorder: &events.Recorder{}, clock: &clock{now: time.Date(2026, 10, 29, 9, 0, 0, 0, time.UTC)}}
136+ f.manager = agents.NewManager(definitions(names...), agents.Options{
137+ MaxRunning: maxRunning, IdleTimeout: idle, Emitter: f.recorder, Starter: f.starter.start, Now: f.clock.Now,
138+ })
139+ return f
140+}
141+
142+func (f *fixture) state(t *testing.T, name string) agents.State {
143+ t.Helper()
144+ for _, status := range f.manager.Statuses() {
145+ if status.Name == name {
146+ return status.State
147+ }
148+ }
149+ t.Fatalf("no agent %s", name)
150+ return ""
151+}
152+
153+func (f *fixture) statesOf(name string) []string {
154+ var states []string
155+ for _, payload := range f.recorder.Named(events.AgentState) {
156+ if p := payload.(events.AgentStatePayload); p.Agent == name {
157+ states = append(states, p.State)
158+ }
159+ }
160+ return states
161+}
162+
163+var ctx = context.Background()
164+
165+// ---------------------------------------------------------------- tests
166+
167+func TestAgentsStartOnlyWhenNeeded(t *testing.T) {
168+ f := setup(0, "alice", "bob")
169+ if f.starter.count("alice") != 0 || f.state(t, "alice") != agents.StateStopped {
170+ t.Fatal("no agent should start before it is needed")
171+ }
172+ _, release, err := f.manager.Acquire(ctx, "alice")
173+ if err != nil {
174+ t.Fatal(err)
175+ }
176+ if f.state(t, "alice") != agents.StateBusy {
177+ t.Fatalf("an acquired agent is busy, got %s", f.state(t, "alice"))
178+ }
179+ release()
180+ release() // releasing twice is harmless
181+ if f.state(t, "alice") != agents.StateReady {
182+ t.Fatalf("a released agent is ready, got %s", f.state(t, "alice"))
183+ }
184+ if got := fmt.Sprint(f.statesOf("alice")); got != "[starting ready busy ready]" {
185+ t.Fatalf("state events: %s", got)
186+ }
187+}
188+
189+func TestAnAgentIsStartedOnceForSeveralUsers(t *testing.T) {
190+ f := setup(0, "alice")
191+ f.starter.gate = make(chan struct{})
192+ var wg sync.WaitGroup
193+ for i := 0; i < 5; i++ {
194+ wg.Add(1)
195+ go func() {
196+ defer wg.Done()
197+ _, release, err := f.manager.Acquire(ctx, "alice")
198+ if err != nil {
199+ t.Error(err)
200+ return
201+ }
202+ defer release()
203+ }()
204+ }
205+ time.Sleep(50 * time.Millisecond)
206+ close(f.starter.gate)
207+ wg.Wait()
208+ if n := f.starter.count("alice"); n != 1 {
209+ t.Fatalf("alice started %d times", n)
210+ }
211+ if f.state(t, "alice") != agents.StateReady {
212+ t.Fatalf("all users released: want ready, got %s", f.state(t, "alice"))
213+ }
214+}
215+
216+func TestTheLeastRecentlyUsedIdleAgentMakesRoom(t *testing.T) {
217+ f := setup(0, "alice", "bob", "riker")
218+ for _, name := range []string{"alice", "bob"} {
219+ _, release, err := f.manager.Acquire(ctx, name)
220+ if err != nil {
221+ t.Fatal(err)
222+ }
223+ release()
224+ f.clock.advance(time.Minute)
225+ }
226+ if err := f.manager.Start(ctx, "riker"); err != nil {
227+ t.Fatal(err)
228+ }
229+ if f.state(t, "alice") != agents.StateStopped || !f.starter.conn("alice").closed() {
230+ t.Fatal("alice, used first, should have been stopped for riker")
231+ }
232+ if f.state(t, "bob") != agents.StateReady || f.state(t, "riker") != agents.StateReady {
233+ t.Fatal("bob and riker should run")
234+ }
235+}
236+
237+func TestTheLimitIsReachedWhenEveryAgentIsBusy(t *testing.T) {
238+ f := setup(0, "alice", "bob", "riker")
239+ for _, name := range []string{"alice", "bob"} {
240+ if _, _, err := f.manager.Acquire(ctx, name); err != nil {
241+ t.Fatal(err)
242+ }
243+ }
244+ err := f.manager.Start(ctx, "riker")
245+ if !errors.Is(err, agents.ErrLimitReached) {
246+ t.Fatalf("want ErrLimitReached, got %v", err)
247+ }
248+ if f.starter.count("riker") != 0 {
249+ t.Fatal("riker must not start")
250+ }
251+}
252+
253+func TestIdleAgentsAreStopped(t *testing.T) {
254+ f := setup(10*time.Minute, "alice", "bob")
255+ _, releaseAlice, _ := f.manager.Acquire(ctx, "alice")
256+ releaseAlice()
257+ _, releaseBob, _ := f.manager.Acquire(ctx, "bob") // still busy
258+ defer releaseBob()
259+
260+ f.clock.advance(9 * time.Minute)
261+ if stopped := f.manager.StopIdle(ctx); len(stopped) != 0 {
262+ t.Fatalf("too early: %v", stopped)
263+ }
264+ f.clock.advance(time.Minute)
265+ if stopped := fmt.Sprint(f.manager.StopIdle(ctx)); stopped != "[alice]" {
266+ t.Fatalf("only alice is idle, stopped %s", stopped)
267+ }
268+ if f.state(t, "bob") != agents.StateBusy {
269+ t.Fatal("a busy agent is never stopped for idleness")
270+ }
271+}
272+
273+func TestNoIdleStopWhenTheTimeoutIsZero(t *testing.T) {
274+ f := setup(0, "alice")
275+ _ = f.manager.Start(ctx, "alice")
276+ f.clock.advance(24 * time.Hour)
277+ if stopped := f.manager.StopIdle(ctx); len(stopped) != 0 {
278+ t.Fatal("stopIdleAgentsAfter: 0 never stops agents")
279+ }
280+}
281+
282+func TestACrashPutsTheAgentInErrorUntilRestarted(t *testing.T) {
283+ f := setup(0, "alice")
284+ if err := f.manager.Start(ctx, "alice"); err != nil {
285+ t.Fatal(err)
286+ }
287+ f.starter.conn("alice").crash(errors.New("the agent stopped: exit status 1"))
288+ waitUntil(t, func() bool { return f.state(t, "alice") == agents.StateError })
289+ if status := f.manager.Statuses()[0]; status.Error != "the agent stopped: exit status 1" {
290+ t.Fatalf("the error should explain the crash: %+v", status)
291+ }
292+ time.Sleep(20 * time.Millisecond)
293+ if f.starter.count("alice") != 1 {
294+ t.Fatal("a crashed agent is not restarted automatically")
295+ }
296+ if err := f.manager.Restart(ctx, "alice"); err != nil {
297+ t.Fatal(err)
298+ }
299+ if f.state(t, "alice") != agents.StateReady || f.starter.count("alice") != 2 {
300+ t.Fatal("restart should start it again")
301+ }
302+}
303+
304+func TestAFailedStartIsReported(t *testing.T) {
305+ f := setup(0, "ghost")
306+ f.starter.fail["ghost"] = errors.New("start agent ghost: exec: \"mm\": not found")
307+ if err := f.manager.Start(ctx, "ghost"); err == nil {
308+ t.Fatal("want the start error")
309+ }
310+ status := f.manager.Statuses()[0]
311+ if status.State != agents.StateError || status.Error == "" {
312+ t.Fatalf("got %+v", status)
313+ }
314+}
315+
316+func TestStopAndStopAll(t *testing.T) {
317+ f := setup(0, "alice", "bob")
318+ _ = f.manager.Start(ctx, "alice")
319+ _ = f.manager.Start(ctx, "bob")
320+ if err := f.manager.Stop(ctx, "alice"); err != nil {
321+ t.Fatal(err)
322+ }
323+ if f.state(t, "alice") != agents.StateStopped || !f.starter.conn("alice").closed() {
324+ t.Fatal("alice should be stopped")
325+ }
326+ if err := f.manager.Stop(ctx, "alice"); err != nil {
327+ t.Fatal("stopping a stopped agent is harmless")
328+ }
329+ f.manager.StopAll(ctx)
330+ if f.state(t, "bob") != agents.StateStopped {
331+ t.Fatal("StopAll should stop bob")
332+ }
333+}
334+
335+func TestStopWhileStartingStopsTheAgentOnceStarted(t *testing.T) {
336+ f := setup(0, "alice")
337+ f.starter.gate = make(chan struct{})
338+ result := make(chan error, 1)
339+ go func() { result <- f.manager.Start(ctx, "alice") }()
340+ waitUntil(t, func() bool { return f.state(t, "alice") == agents.StateStarting })
341+ if err := f.manager.Stop(ctx, "alice"); err != nil {
342+ t.Fatal(err)
343+ }
344+ close(f.starter.gate)
345+ if err := <-result; err == nil {
346+ t.Fatal("the start should report that the agent was stopped")
347+ }
348+ if f.state(t, "alice") != agents.StateStopped {
349+ t.Fatalf("want stopped, got %s", f.state(t, "alice"))
350+ }
351+ waitUntil(t, func() bool { return f.starter.conn("alice").closed() })
352+}
353+
354+func TestUnknownAgent(t *testing.T) {
355+ f := setup(0, "alice")
356+ if _, _, err := f.manager.Acquire(ctx, "nobody"); !errors.Is(err, agents.ErrUnknownAgent) {
357+ t.Fatalf("want ErrUnknownAgent, got %v", err)
358+ }
359+ if _, err := f.manager.Logs("nobody"); !errors.Is(err, agents.ErrUnknownAgent) {
360+ t.Fatalf("want ErrUnknownAgent, got %v", err)
361+ }
362+}
363+
364+func TestStderrIsKeptAndEmitted(t *testing.T) {
365+ f := setup(0, "alice")
366+ _ = f.manager.Start(ctx, "alice")
367+ logs, _ := f.manager.Logs("alice")
368+ if len(logs) != 1 || logs[0] != "alice started in /agents/alice" {
369+ t.Fatalf("logs: %v", logs)
370+ }
371+ emitted := f.recorder.Named(events.AgentLog)
372+ if len(emitted) != 1 || emitted[0].(events.AgentLogPayload).Line != logs[0] {
373+ t.Fatalf("agent:log events: %v", emitted)
374+ }
375+}
376+
377+func TestReconfigure(t *testing.T) {
378+ f := setup(0, "alice", "bob")
379+ _ = f.manager.Start(ctx, "alice")
380+ _ = f.manager.Start(ctx, "bob")
381+
382+ changed := definitions("alice", "garfield")
383+ changed[0].Args = []string{"-acp"}
384+ f.manager.Reconfigure(ctx, changed, 3, time.Minute)
385+
386+ statuses := f.manager.Statuses()
387+ if len(statuses) != 2 || statuses[0].Name != "alice" || statuses[1].Name != "garfield" {
388+ t.Fatalf("got %+v", statuses)
389+ }
390+ if !statuses[0].Outdated || statuses[0].State != agents.StateReady {
391+ t.Fatal("alice keeps running but is outdated")
392+ }
393+ if statuses[1].State != agents.StateStopped {
394+ t.Fatal("a new agent starts stopped")
395+ }
396+ if !f.starter.conn("bob").closed() {
397+ t.Fatal("a removed agent is stopped")
398+ }
399+ _ = f.manager.Restart(ctx, "alice")
400+ if f.manager.Statuses()[0].Outdated {
401+ t.Fatal("a restart applies the new definition")
402+ }
403+}
404+
405+func ExampleManager_Acquire() {
406+ manager := agents.NewManager(definitions("alice"), agents.Options{
407+ Starter: func(context.Context, acp.Spec, func(string)) (agents.Conn, error) { return newFakeConn("alice"), nil },
408+ })
409+ conn, release, err := manager.Acquire(context.Background(), "alice")
410+ if err != nil {
411+ fmt.Println(err)
412+ return
413+ }
414+ fmt.Println(conn.Info().Name, manager.Statuses()[0].State)
415+ release()
416+ fmt.Println(manager.Statuses()[0].State)
417+ // Output:
418+ // alice busy
419+ // ready
420+}
421+
422+func waitUntil(t *testing.T, condition func() bool) {
423+ t.Helper()
424+ deadline := time.Now().Add(2 * time.Second)
425+ for !condition() {
426+ if time.Now().After(deadline) {
427+ t.Fatal("condition not met in time")
428+ }
429+ time.Sleep(5 * time.Millisecond)
430+ }
431+}
new file mode 100644
@@ -0,0 +1,431 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package agents_test
4+
5+import (
6+ "context"
7+ "errors"
8+ "fmt"
9+ "sync"
10+ "testing"
11+ "time"
12+
13+ "git.rickub.com/bots-garden/sidekicks/internal/acp"
14+ "git.rickub.com/bots-garden/sidekicks/internal/agents"
15+ "git.rickub.com/bots-garden/sidekicks/internal/config"
16+ "git.rickub.com/bots-garden/sidekicks/internal/events"
17+)
18+
19+// ---------------------------------------------------------------- fakes
20+
21+type fakeConn struct {
22+ name string
23+ done chan struct{}
24+ once sync.Once
25+ mu sync.Mutex
26+ exitErr error
27+ closedBy string
28+}
29+
30+func newFakeConn(name string) *fakeConn { return &fakeConn{name: name, done: make(chan struct{})} }
31+
32+func (c *fakeConn) Info() acp.Info { return acp.Info{Name: c.name} }
33+func (c *fakeConn) Done() <-chan struct{} { return c.done }
34+func (c *fakeConn) Err() error {
35+ c.mu.Lock()
36+ defer c.mu.Unlock()
37+ return c.exitErr
38+}
39+func (c *fakeConn) Close(context.Context) error {
40+ c.mu.Lock()
41+ c.closedBy = "close"
42+ c.mu.Unlock()
43+ c.once.Do(func() { close(c.done) })
44+ return nil
45+}
46+func (c *fakeConn) crash(err error) {
47+ c.mu.Lock()
48+ c.exitErr = err
49+ c.mu.Unlock()
50+ c.once.Do(func() { close(c.done) })
51+}
52+func (c *fakeConn) closed() bool {
53+ c.mu.Lock()
54+ defer c.mu.Unlock()
55+ return c.closedBy == "close"
56+}
57+
58+// fakeStarter records starts; an agent named in fail cannot start, and gate,
59+// when set, holds every start until it is closed.
60+type fakeStarter struct {
61+ mu sync.Mutex
62+ starts map[string]int
63+ conns map[string]*fakeConn
64+ fail map[string]error
65+ gate chan struct{}
66+}
67+
68+func newFakeStarter() *fakeStarter {
69+ return &fakeStarter{starts: map[string]int{}, conns: map[string]*fakeConn{}, fail: map[string]error{}}
70+}
71+
72+func (s *fakeStarter) start(_ context.Context, spec acp.Spec, onStderr func(string)) (agents.Conn, error) {
73+ if s.gate != nil {
74+ <-s.gate
75+ }
76+ s.mu.Lock()
77+ defer s.mu.Unlock()
78+ s.starts[spec.Name]++
79+ if err := s.fail[spec.Name]; err != nil {
80+ return nil, err
81+ }
82+ onStderr(spec.Name + " started in " + spec.Dir)
83+ conn := newFakeConn(spec.Name)
84+ s.conns[spec.Name] = conn
85+ return conn, nil
86+}
87+
88+func (s *fakeStarter) count(name string) int {
89+ s.mu.Lock()
90+ defer s.mu.Unlock()
91+ return s.starts[name]
92+}
93+
94+func (s *fakeStarter) conn(name string) *fakeConn {
95+ s.mu.Lock()
96+ defer s.mu.Unlock()
97+ return s.conns[name]
98+}
99+
100+type clock struct {
101+ mu sync.Mutex
102+ now time.Time
103+}
104+
105+func (c *clock) Now() time.Time {
106+ c.mu.Lock()
107+ defer c.mu.Unlock()
108+ return c.now
109+}
110+func (c *clock) advance(d time.Duration) {
111+ c.mu.Lock()
112+ defer c.mu.Unlock()
113+ c.now = c.now.Add(d)
114+}
115+
116+func definitions(names ...string) []config.Agent {
117+ var result []config.Agent
118+ for _, name := range names {
119+ result = append(result, config.Agent{Name: name, Title: name, Command: "mm", Workspace: "/agents/" + name})
120+ }
121+ return result
122+}
123+
124+type fixture struct {
125+ manager *agents.Manager
126+ starter *fakeStarter
127+ recorder *events.Recorder
128+ clock *clock
129+}
130+
131+// maxRunning is 2 in every test, like the default limit.
132+const maxRunning = 2
133+
134+func setup(idle time.Duration, names ...string) *fixture {
135+ f := &fixture{starter: newFakeStarter(), recorder: &events.Recorder{}, clock: &clock{now: time.Date(2026, 10, 29, 9, 0, 0, 0, time.UTC)}}
136+ f.manager = agents.NewManager(definitions(names...), agents.Options{
137+ MaxRunning: maxRunning, IdleTimeout: idle, Emitter: f.recorder, Starter: f.starter.start, Now: f.clock.Now,
138+ })
139+ return f
140+}
141+
142+func (f *fixture) state(t *testing.T, name string) agents.State {
143+ t.Helper()
144+ for _, status := range f.manager.Statuses() {
145+ if status.Name == name {
146+ return status.State
147+ }
148+ }
149+ t.Fatalf("no agent %s", name)
150+ return ""
151+}
152+
153+func (f *fixture) statesOf(name string) []string {
154+ var states []string
155+ for _, payload := range f.recorder.Named(events.AgentState) {
156+ if p := payload.(events.AgentStatePayload); p.Agent == name {
157+ states = append(states, p.State)
158+ }
159+ }
160+ return states
161+}
162+
163+var ctx = context.Background()
164+
165+// ---------------------------------------------------------------- tests
166+
167+func TestAgentsStartOnlyWhenNeeded(t *testing.T) {
168+ f := setup(0, "alice", "bob")
169+ if f.starter.count("alice") != 0 || f.state(t, "alice") != agents.StateStopped {
170+ t.Fatal("no agent should start before it is needed")
171+ }
172+ _, release, err := f.manager.Acquire(ctx, "alice")
173+ if err != nil {
174+ t.Fatal(err)
175+ }
176+ if f.state(t, "alice") != agents.StateBusy {
177+ t.Fatalf("an acquired agent is busy, got %s", f.state(t, "alice"))
178+ }
179+ release()
180+ release() // releasing twice is harmless
181+ if f.state(t, "alice") != agents.StateReady {
182+ t.Fatalf("a released agent is ready, got %s", f.state(t, "alice"))
183+ }
184+ if got := fmt.Sprint(f.statesOf("alice")); got != "[starting ready busy ready]" {
185+ t.Fatalf("state events: %s", got)
186+ }
187+}
188+
189+func TestAnAgentIsStartedOnceForSeveralUsers(t *testing.T) {
190+ f := setup(0, "alice")
191+ f.starter.gate = make(chan struct{})
192+ var wg sync.WaitGroup
193+ for i := 0; i < 5; i++ {
194+ wg.Add(1)
195+ go func() {
196+ defer wg.Done()
197+ _, release, err := f.manager.Acquire(ctx, "alice")
198+ if err != nil {
199+ t.Error(err)
200+ return
201+ }
202+ defer release()
203+ }()
204+ }
205+ time.Sleep(50 * time.Millisecond)
206+ close(f.starter.gate)
207+ wg.Wait()
208+ if n := f.starter.count("alice"); n != 1 {
209+ t.Fatalf("alice started %d times", n)
210+ }
211+ if f.state(t, "alice") != agents.StateReady {
212+ t.Fatalf("all users released: want ready, got %s", f.state(t, "alice"))
213+ }
214+}
215+
216+func TestTheLeastRecentlyUsedIdleAgentMakesRoom(t *testing.T) {
217+ f := setup(0, "alice", "bob", "riker")
218+ for _, name := range []string{"alice", "bob"} {
219+ _, release, err := f.manager.Acquire(ctx, name)
220+ if err != nil {
221+ t.Fatal(err)
222+ }
223+ release()
224+ f.clock.advance(time.Minute)
225+ }
226+ if err := f.manager.Start(ctx, "riker"); err != nil {
227+ t.Fatal(err)
228+ }
229+ if f.state(t, "alice") != agents.StateStopped || !f.starter.conn("alice").closed() {
230+ t.Fatal("alice, used first, should have been stopped for riker")
231+ }
232+ if f.state(t, "bob") != agents.StateReady || f.state(t, "riker") != agents.StateReady {
233+ t.Fatal("bob and riker should run")
234+ }
235+}
236+
237+func TestTheLimitIsReachedWhenEveryAgentIsBusy(t *testing.T) {
238+ f := setup(0, "alice", "bob", "riker")
239+ for _, name := range []string{"alice", "bob"} {
240+ if _, _, err := f.manager.Acquire(ctx, name); err != nil {
241+ t.Fatal(err)
242+ }
243+ }
244+ err := f.manager.Start(ctx, "riker")
245+ if !errors.Is(err, agents.ErrLimitReached) {
246+ t.Fatalf("want ErrLimitReached, got %v", err)
247+ }
248+ if f.starter.count("riker") != 0 {
249+ t.Fatal("riker must not start")
250+ }
251+}
252+
253+func TestIdleAgentsAreStopped(t *testing.T) {
254+ f := setup(10*time.Minute, "alice", "bob")
255+ _, releaseAlice, _ := f.manager.Acquire(ctx, "alice")
256+ releaseAlice()
257+ _, releaseBob, _ := f.manager.Acquire(ctx, "bob") // still busy
258+ defer releaseBob()
259+
260+ f.clock.advance(9 * time.Minute)
261+ if stopped := f.manager.StopIdle(ctx); len(stopped) != 0 {
262+ t.Fatalf("too early: %v", stopped)
263+ }
264+ f.clock.advance(time.Minute)
265+ if stopped := fmt.Sprint(f.manager.StopIdle(ctx)); stopped != "[alice]" {
266+ t.Fatalf("only alice is idle, stopped %s", stopped)
267+ }
268+ if f.state(t, "bob") != agents.StateBusy {
269+ t.Fatal("a busy agent is never stopped for idleness")
270+ }
271+}
272+
273+func TestNoIdleStopWhenTheTimeoutIsZero(t *testing.T) {
274+ f := setup(0, "alice")
275+ _ = f.manager.Start(ctx, "alice")
276+ f.clock.advance(24 * time.Hour)
277+ if stopped := f.manager.StopIdle(ctx); len(stopped) != 0 {
278+ t.Fatal("stopIdleAgentsAfter: 0 never stops agents")
279+ }
280+}
281+
282+func TestACrashPutsTheAgentInErrorUntilRestarted(t *testing.T) {
283+ f := setup(0, "alice")
284+ if err := f.manager.Start(ctx, "alice"); err != nil {
285+ t.Fatal(err)
286+ }
287+ f.starter.conn("alice").crash(errors.New("the agent stopped: exit status 1"))
288+ waitUntil(t, func() bool { return f.state(t, "alice") == agents.StateError })
289+ if status := f.manager.Statuses()[0]; status.Error != "the agent stopped: exit status 1" {
290+ t.Fatalf("the error should explain the crash: %+v", status)
291+ }
292+ time.Sleep(20 * time.Millisecond)
293+ if f.starter.count("alice") != 1 {
294+ t.Fatal("a crashed agent is not restarted automatically")
295+ }
296+ if err := f.manager.Restart(ctx, "alice"); err != nil {
297+ t.Fatal(err)
298+ }
299+ if f.state(t, "alice") != agents.StateReady || f.starter.count("alice") != 2 {
300+ t.Fatal("restart should start it again")
301+ }
302+}
303+
304+func TestAFailedStartIsReported(t *testing.T) {
305+ f := setup(0, "ghost")
306+ f.starter.fail["ghost"] = errors.New("start agent ghost: exec: \"mm\": not found")
307+ if err := f.manager.Start(ctx, "ghost"); err == nil {
308+ t.Fatal("want the start error")
309+ }
310+ status := f.manager.Statuses()[0]
311+ if status.State != agents.StateError || status.Error == "" {
312+ t.Fatalf("got %+v", status)
313+ }
314+}
315+
316+func TestStopAndStopAll(t *testing.T) {
317+ f := setup(0, "alice", "bob")
318+ _ = f.manager.Start(ctx, "alice")
319+ _ = f.manager.Start(ctx, "bob")
320+ if err := f.manager.Stop(ctx, "alice"); err != nil {
321+ t.Fatal(err)
322+ }
323+ if f.state(t, "alice") != agents.StateStopped || !f.starter.conn("alice").closed() {
324+ t.Fatal("alice should be stopped")
325+ }
326+ if err := f.manager.Stop(ctx, "alice"); err != nil {
327+ t.Fatal("stopping a stopped agent is harmless")
328+ }
329+ f.manager.StopAll(ctx)
330+ if f.state(t, "bob") != agents.StateStopped {
331+ t.Fatal("StopAll should stop bob")
332+ }
333+}
334+
335+func TestStopWhileStartingStopsTheAgentOnceStarted(t *testing.T) {
336+ f := setup(0, "alice")
337+ f.starter.gate = make(chan struct{})
338+ result := make(chan error, 1)
339+ go func() { result <- f.manager.Start(ctx, "alice") }()
340+ waitUntil(t, func() bool { return f.state(t, "alice") == agents.StateStarting })
341+ if err := f.manager.Stop(ctx, "alice"); err != nil {
342+ t.Fatal(err)
343+ }
344+ close(f.starter.gate)
345+ if err := <-result; err == nil {
346+ t.Fatal("the start should report that the agent was stopped")
347+ }
348+ if f.state(t, "alice") != agents.StateStopped {
349+ t.Fatalf("want stopped, got %s", f.state(t, "alice"))
350+ }
351+ waitUntil(t, func() bool { return f.starter.conn("alice").closed() })
352+}
353+
354+func TestUnknownAgent(t *testing.T) {
355+ f := setup(0, "alice")
356+ if _, _, err := f.manager.Acquire(ctx, "nobody"); !errors.Is(err, agents.ErrUnknownAgent) {
357+ t.Fatalf("want ErrUnknownAgent, got %v", err)
358+ }
359+ if _, err := f.manager.Logs("nobody"); !errors.Is(err, agents.ErrUnknownAgent) {
360+ t.Fatalf("want ErrUnknownAgent, got %v", err)
361+ }
362+}
363+
364+func TestStderrIsKeptAndEmitted(t *testing.T) {
365+ f := setup(0, "alice")
366+ _ = f.manager.Start(ctx, "alice")
367+ logs, _ := f.manager.Logs("alice")
368+ if len(logs) != 1 || logs[0] != "alice started in /agents/alice" {
369+ t.Fatalf("logs: %v", logs)
370+ }
371+ emitted := f.recorder.Named(events.AgentLog)
372+ if len(emitted) != 1 || emitted[0].(events.AgentLogPayload).Line != logs[0] {
373+ t.Fatalf("agent:log events: %v", emitted)
374+ }
375+}
376+
377+func TestReconfigure(t *testing.T) {
378+ f := setup(0, "alice", "bob")
379+ _ = f.manager.Start(ctx, "alice")
380+ _ = f.manager.Start(ctx, "bob")
381+
382+ changed := definitions("alice", "garfield")
383+ changed[0].Args = []string{"-acp"}
384+ f.manager.Reconfigure(ctx, changed, 3, time.Minute)
385+
386+ statuses := f.manager.Statuses()
387+ if len(statuses) != 2 || statuses[0].Name != "alice" || statuses[1].Name != "garfield" {
388+ t.Fatalf("got %+v", statuses)
389+ }
390+ if !statuses[0].Outdated || statuses[0].State != agents.StateReady {
391+ t.Fatal("alice keeps running but is outdated")
392+ }
393+ if statuses[1].State != agents.StateStopped {
394+ t.Fatal("a new agent starts stopped")
395+ }
396+ if !f.starter.conn("bob").closed() {
397+ t.Fatal("a removed agent is stopped")
398+ }
399+ _ = f.manager.Restart(ctx, "alice")
400+ if f.manager.Statuses()[0].Outdated {
401+ t.Fatal("a restart applies the new definition")
402+ }
403+}
404+
405+func ExampleManager_Acquire() {
406+ manager := agents.NewManager(definitions("alice"), agents.Options{
407+ Starter: func(context.Context, acp.Spec, func(string)) (agents.Conn, error) { return newFakeConn("alice"), nil },
408+ })
409+ conn, release, err := manager.Acquire(context.Background(), "alice")
410+ if err != nil {
411+ fmt.Println(err)
412+ return
413+ }
414+ fmt.Println(conn.Info().Name, manager.Statuses()[0].State)
415+ release()
416+ fmt.Println(manager.Statuses()[0].State)
417+ // Output:
418+ // alice busy
419+ // ready
420+}
421+
422+func waitUntil(t *testing.T, condition func() bool) {
423+ t.Helper()
424+ deadline := time.Now().Add(2 * time.Second)
425+ for !condition() {
426+ if time.Now().After(deadline) {
427+ t.Fatal("condition not met in time")
428+ }
429+ time.Sleep(5 * time.Millisecond)
430+ }
431+}
added internal/app/app.go +121 -0
new file mode 100644
@@ -0,0 +1,121 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// Package app is what the interface talks to (ADR 0003): the services bound to
4+// the frontend, and the glue between the configuration and the agents.
5+//
6+// It does not import Wails: main.go binds the services and plugs the Wails
7+// event manager in as the events.Emitter, so this package is tested without a
8+// window.
9+//
10+// core := app.New(location, emitter, nil)
11+// go core.Run(ctx)
12+// defer core.Shutdown(context.Background())
13+// services := []any{&app.AgentService{Core: core}, &app.ConfigService{Core: core}}
14+package app
15+
16+import (
17+ "context"
18+ "errors"
19+ "sync"
20+
21+ "git.rickub.com/bots-garden/sidekicks/internal/agents"
22+ "git.rickub.com/bots-garden/sidekicks/internal/config"
23+ "git.rickub.com/bots-garden/sidekicks/internal/events"
24+)
25+
26+// Core holds the loaded configuration and the agent manager.
27+type Core struct {
28+ location config.Location
29+ emitter events.Emitter
30+ manager *agents.Manager
31+
32+ mu sync.Mutex
33+ cfg *config.Config
34+ problems []config.Problem
35+ created bool
36+}
37+
38+// New loads the configuration at location — creating the default file first
39+// when the default location is used and empty — and prepares the agents. A
40+// configuration with problems still gives a working Core, with no agents and
41+// the problems reported by ConfigService. starter is nil outside tests.
42+func New(location config.Location, emitter events.Emitter, starter agents.Starter) *Core {
43+ if emitter == nil {
44+ emitter = events.Discard
45+ }
46+ core := &Core{location: location, emitter: emitter}
47+ if location.Source == config.SourceDefault {
48+ created, err := config.EnsureDefault(location.Path)
49+ core.created = created
50+ if err != nil {
51+ core.problems = []config.Problem{{Message: err.Error()}}
52+ }
53+ }
54+ cfg, err := config.Load(location.Path)
55+ if err != nil {
56+ core.problems = problemsOf(err)
57+ cfg = emptyConfig(location.Path)
58+ }
59+ core.cfg = cfg
60+ core.manager = agents.NewManager(cfg.Agents, agents.Options{
61+ MaxRunning: cfg.Limits.MaxRunningAgents, IdleTimeout: cfg.Limits.StopIdleAgentsAfter,
62+ Emitter: emitter, Starter: starter,
63+ })
64+ return core
65+}
66+
67+func emptyConfig(path string) *config.Config {
68+ return &config.Config{
69+ Path: path, Version: 1,
70+ UI: config.UI{Theme: config.DefaultTheme, Language: config.DefaultLanguage},
71+ Limits: config.Limits{MaxRunningAgents: config.DefaultMaxRunningAgents, StopIdleAgentsAfter: config.DefaultStopIdleAgentsAfter},
72+ }
73+}
74+
75+func problemsOf(err error) []config.Problem {
76+ var configError *config.Error
77+ if errors.As(err, &configError) {
78+ return configError.Problems
79+ }
80+ return []config.Problem{{Message: err.Error()}}
81+}
82+
83+// Run watches the configuration file and stops idle agents until ctx is done.
84+func (c *Core) Run(ctx context.Context) {
85+ go c.manager.Run(ctx)
86+ _ = config.Watch(ctx, c.location.Path, func(cfg *config.Config, err error) {
87+ c.apply(ctx, cfg, err)
88+ })
89+}
90+
91+// apply takes a reloaded configuration into account. A broken file keeps the
92+// previous configuration in use and reports its problems.
93+func (c *Core) apply(ctx context.Context, cfg *config.Config, err error) {
94+ c.mu.Lock()
95+ if err != nil {
96+ c.problems = problemsOf(err)
97+ c.mu.Unlock()
98+ c.emitter.Emit(events.ConfigChanged, events.ConfigChangedPayload{Path: c.location.Path, Valid: false, Problems: messages(problemsOf(err))})
99+ return
100+ }
101+ c.cfg, c.problems = cfg, nil
102+ c.mu.Unlock()
103+ c.manager.Reconfigure(ctx, cfg.Agents, cfg.Limits.MaxRunningAgents, cfg.Limits.StopIdleAgentsAfter)
104+ c.emitter.Emit(events.ConfigChanged, events.ConfigChangedPayload{Path: c.location.Path, Valid: true, Problems: []string{}})
105+}
106+
107+// Shutdown stops every agent.
108+func (c *Core) Shutdown(ctx context.Context) {
109+ c.manager.StopAll(ctx)
110+}
111+
112+// Manager gives access to the agents, for the services.
113+func (c *Core) Manager() *agents.Manager { return c.manager }
114+
115+func messages(problems []config.Problem) []string {
116+ result := make([]string, 0, len(problems))
117+ for _, problem := range problems {
118+ result = append(result, problem.String())
119+ }
120+ return result
121+}
new file mode 100644
@@ -0,0 +1,121 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// Package app is what the interface talks to (ADR 0003): the services bound to
4+// the frontend, and the glue between the configuration and the agents.
5+//
6+// It does not import Wails: main.go binds the services and plugs the Wails
7+// event manager in as the events.Emitter, so this package is tested without a
8+// window.
9+//
10+// core := app.New(location, emitter, nil)
11+// go core.Run(ctx)
12+// defer core.Shutdown(context.Background())
13+// services := []any{&app.AgentService{Core: core}, &app.ConfigService{Core: core}}
14+package app
15+
16+import (
17+ "context"
18+ "errors"
19+ "sync"
20+
21+ "git.rickub.com/bots-garden/sidekicks/internal/agents"
22+ "git.rickub.com/bots-garden/sidekicks/internal/config"
23+ "git.rickub.com/bots-garden/sidekicks/internal/events"
24+)
25+
26+// Core holds the loaded configuration and the agent manager.
27+type Core struct {
28+ location config.Location
29+ emitter events.Emitter
30+ manager *agents.Manager
31+
32+ mu sync.Mutex
33+ cfg *config.Config
34+ problems []config.Problem
35+ created bool
36+}
37+
38+// New loads the configuration at location — creating the default file first
39+// when the default location is used and empty — and prepares the agents. A
40+// configuration with problems still gives a working Core, with no agents and
41+// the problems reported by ConfigService. starter is nil outside tests.
42+func New(location config.Location, emitter events.Emitter, starter agents.Starter) *Core {
43+ if emitter == nil {
44+ emitter = events.Discard
45+ }
46+ core := &Core{location: location, emitter: emitter}
47+ if location.Source == config.SourceDefault {
48+ created, err := config.EnsureDefault(location.Path)
49+ core.created = created
50+ if err != nil {
51+ core.problems = []config.Problem{{Message: err.Error()}}
52+ }
53+ }
54+ cfg, err := config.Load(location.Path)
55+ if err != nil {
56+ core.problems = problemsOf(err)
57+ cfg = emptyConfig(location.Path)
58+ }
59+ core.cfg = cfg
60+ core.manager = agents.NewManager(cfg.Agents, agents.Options{
61+ MaxRunning: cfg.Limits.MaxRunningAgents, IdleTimeout: cfg.Limits.StopIdleAgentsAfter,
62+ Emitter: emitter, Starter: starter,
63+ })
64+ return core
65+}
66+
67+func emptyConfig(path string) *config.Config {
68+ return &config.Config{
69+ Path: path, Version: 1,
70+ UI: config.UI{Theme: config.DefaultTheme, Language: config.DefaultLanguage},
71+ Limits: config.Limits{MaxRunningAgents: config.DefaultMaxRunningAgents, StopIdleAgentsAfter: config.DefaultStopIdleAgentsAfter},
72+ }
73+}
74+
75+func problemsOf(err error) []config.Problem {
76+ var configError *config.Error
77+ if errors.As(err, &configError) {
78+ return configError.Problems
79+ }
80+ return []config.Problem{{Message: err.Error()}}
81+}
82+
83+// Run watches the configuration file and stops idle agents until ctx is done.
84+func (c *Core) Run(ctx context.Context) {
85+ go c.manager.Run(ctx)
86+ _ = config.Watch(ctx, c.location.Path, func(cfg *config.Config, err error) {
87+ c.apply(ctx, cfg, err)
88+ })
89+}
90+
91+// apply takes a reloaded configuration into account. A broken file keeps the
92+// previous configuration in use and reports its problems.
93+func (c *Core) apply(ctx context.Context, cfg *config.Config, err error) {
94+ c.mu.Lock()
95+ if err != nil {
96+ c.problems = problemsOf(err)
97+ c.mu.Unlock()
98+ c.emitter.Emit(events.ConfigChanged, events.ConfigChangedPayload{Path: c.location.Path, Valid: false, Problems: messages(problemsOf(err))})
99+ return
100+ }
101+ c.cfg, c.problems = cfg, nil
102+ c.mu.Unlock()
103+ c.manager.Reconfigure(ctx, cfg.Agents, cfg.Limits.MaxRunningAgents, cfg.Limits.StopIdleAgentsAfter)
104+ c.emitter.Emit(events.ConfigChanged, events.ConfigChangedPayload{Path: c.location.Path, Valid: true, Problems: []string{}})
105+}
106+
107+// Shutdown stops every agent.
108+func (c *Core) Shutdown(ctx context.Context) {
109+ c.manager.StopAll(ctx)
110+}
111+
112+// Manager gives access to the agents, for the services.
113+func (c *Core) Manager() *agents.Manager { return c.manager }
114+
115+func messages(problems []config.Problem) []string {
116+ result := make([]string, 0, len(problems))
117+ for _, problem := range problems {
118+ result = append(result, problem.String())
119+ }
120+ return result
121+}
added internal/app/app_test.go +141 -0
new file mode 100644
@@ -0,0 +1,141 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package app_test
4+
5+import (
6+ "context"
7+ "os"
8+ "path/filepath"
9+ "strings"
10+ "testing"
11+ "time"
12+
13+ "git.rickub.com/bots-garden/sidekicks/internal/acp"
14+ "git.rickub.com/bots-garden/sidekicks/internal/agents"
15+ "git.rickub.com/bots-garden/sidekicks/internal/app"
16+ "git.rickub.com/bots-garden/sidekicks/internal/config"
17+ "git.rickub.com/bots-garden/sidekicks/internal/events"
18+)
19+
20+type conn struct{ done chan struct{} }
21+
22+func (c *conn) Info() acp.Info { return acp.Info{} }
23+func (c *conn) Done() <-chan struct{} { return c.done }
24+func (c *conn) Err() error { return nil }
25+func (c *conn) Close(context.Context) error { close(c.done); return nil }
26+func starter(context.Context, acp.Spec, func(string)) (agents.Conn, error) {
27+ return &conn{done: make(chan struct{})}, nil
28+}
29+
30+func write(t *testing.T, path, content string) {
31+ t.Helper()
32+ if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
33+ t.Fatal(err)
34+ }
35+}
36+
37+const twoAgents = "version: 1\nui:\n theme: dark\nagents:\n - name: alice\n command: sh\n - name: bob\n command: sh\nchannels:\n - name: général\n agents: [alice]\n"
38+
39+func TestTheDefaultFileIsCreatedAtTheDefaultLocation(t *testing.T) {
40+ path := filepath.Join(t.TempDir(), "sidekicks", "config.yaml")
41+ core := app.New(config.Location{Path: path, Source: config.SourceDefault}, nil, starter)
42+ view := (&app.ConfigService{Core: core}).Current()
43+ if !view.Created || !view.Valid || view.Source != "default" || view.Channels[0].Name != "général" {
44+ t.Fatalf("got %+v", view)
45+ }
46+}
47+
48+func TestAFileGivenExplicitlyIsNeverCreated(t *testing.T) {
49+ path := filepath.Join(t.TempDir(), "missing.yaml")
50+ core := app.New(config.Location{Path: path, Source: config.SourceFlag}, nil, starter)
51+ view := (&app.ConfigService{Core: core}).Current()
52+ if view.Valid || len(view.Problems) == 0 {
53+ t.Fatalf("a missing file is a problem: %+v", view)
54+ }
55+ if _, err := os.Stat(path); err == nil {
56+ t.Fatal("the file must not be created")
57+ }
58+}
59+
60+func TestServicesExposeTheConfigurationAndTheAgents(t *testing.T) {
61+ path := filepath.Join(t.TempDir(), "config.yaml")
62+ write(t, path, twoAgents)
63+ core := app.New(config.Location{Path: path, Source: config.SourceFlag}, nil, starter)
64+ view := (&app.ConfigService{Core: core}).Current()
65+ if !view.Valid || view.Theme != "dark" || view.Language != "fr" || len(view.Channels) != 1 {
66+ t.Fatalf("got %+v", view)
67+ }
68+ service := &app.AgentService{Core: core}
69+ if list := service.List(); len(list) != 2 || list[0].State != agents.StateStopped {
70+ t.Fatalf("got %+v", list)
71+ }
72+ if err := service.Start(context.Background(), "alice"); err != nil {
73+ t.Fatal(err)
74+ }
75+ if service.List()[0].State != agents.StateReady {
76+ t.Fatal("alice should be ready")
77+ }
78+ if err := service.Restart(context.Background(), "alice"); err != nil {
79+ t.Fatal(err)
80+ }
81+ if err := service.Stop(context.Background(), "alice"); err != nil {
82+ t.Fatal(err)
83+ }
84+ if _, err := service.Logs("alice"); err != nil {
85+ t.Fatal(err)
86+ }
87+ core.Shutdown(context.Background())
88+}
89+
90+func TestAnInvalidFileIsReportedWithLines(t *testing.T) {
91+ path := filepath.Join(t.TempDir(), "config.yaml")
92+ write(t, path, "version: 1\nagents:\n - name: Alice\n command: sh\n")
93+ core := app.New(config.Location{Path: path, Source: config.SourceFlag}, nil, starter)
94+ view := (&app.ConfigService{Core: core}).Current()
95+ if view.Valid || !strings.HasPrefix(view.Problems[0], "ligne 3 — agents[0].name") {
96+ t.Fatalf("got %+v", view)
97+ }
98+ if len((&app.AgentService{Core: core}).List()) != 0 {
99+ t.Fatal("an invalid file gives no agents")
100+ }
101+}
102+
103+func TestEditingTheFileReconfiguresAndNotifies(t *testing.T) {
104+ path := filepath.Join(t.TempDir(), "config.yaml")
105+ write(t, path, twoAgents)
106+ recorder := &events.Recorder{}
107+ core := app.New(config.Location{Path: path, Source: config.SourceFlag}, recorder, starter)
108+ ctx, cancel := context.WithCancel(context.Background())
109+ defer cancel()
110+ go core.Run(ctx)
111+ time.Sleep(100 * time.Millisecond)
112+
113+ if err := config.Save(path, []byte(strings.Replace(twoAgents, " - name: bob\n command: sh\n", "", 1))); err != nil {
114+ t.Fatal(err)
115+ }
116+ waitFor(t, func() bool { return len(recorder.Named(events.ConfigChanged)) > 0 })
117+ if list := (&app.AgentService{Core: core}).List(); len(list) != 1 {
118+ t.Fatalf("bob should be gone: %+v", list)
119+ }
120+
121+ write(t, path, "version: 1\nagents:\n - name: Bad\n")
122+ waitFor(t, func() bool { return len(recorder.Named(events.ConfigChanged)) > 1 })
123+ last := recorder.Named(events.ConfigChanged)[1].(events.ConfigChangedPayload)
124+ if last.Valid || len(last.Problems) == 0 {
125+ t.Fatalf("a broken file is reported: %+v", last)
126+ }
127+ if list := (&app.AgentService{Core: core}).List(); len(list) != 1 {
128+ t.Fatal("a broken file keeps the previous configuration")
129+ }
130+}
131+
132+func waitFor(t *testing.T, condition func() bool) {
133+ t.Helper()
134+ deadline := time.Now().Add(3 * time.Second)
135+ for !condition() {
136+ if time.Now().After(deadline) {
137+ t.Fatal("condition not met in time")
138+ }
139+ time.Sleep(10 * time.Millisecond)
140+ }
141+}
new file mode 100644
@@ -0,0 +1,141 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package app_test
4+
5+import (
6+ "context"
7+ "os"
8+ "path/filepath"
9+ "strings"
10+ "testing"
11+ "time"
12+
13+ "git.rickub.com/bots-garden/sidekicks/internal/acp"
14+ "git.rickub.com/bots-garden/sidekicks/internal/agents"
15+ "git.rickub.com/bots-garden/sidekicks/internal/app"
16+ "git.rickub.com/bots-garden/sidekicks/internal/config"
17+ "git.rickub.com/bots-garden/sidekicks/internal/events"
18+)
19+
20+type conn struct{ done chan struct{} }
21+
22+func (c *conn) Info() acp.Info { return acp.Info{} }
23+func (c *conn) Done() <-chan struct{} { return c.done }
24+func (c *conn) Err() error { return nil }
25+func (c *conn) Close(context.Context) error { close(c.done); return nil }
26+func starter(context.Context, acp.Spec, func(string)) (agents.Conn, error) {
27+ return &conn{done: make(chan struct{})}, nil
28+}
29+
30+func write(t *testing.T, path, content string) {
31+ t.Helper()
32+ if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
33+ t.Fatal(err)
34+ }
35+}
36+
37+const twoAgents = "version: 1\nui:\n theme: dark\nagents:\n - name: alice\n command: sh\n - name: bob\n command: sh\nchannels:\n - name: général\n agents: [alice]\n"
38+
39+func TestTheDefaultFileIsCreatedAtTheDefaultLocation(t *testing.T) {
40+ path := filepath.Join(t.TempDir(), "sidekicks", "config.yaml")
41+ core := app.New(config.Location{Path: path, Source: config.SourceDefault}, nil, starter)
42+ view := (&app.ConfigService{Core: core}).Current()
43+ if !view.Created || !view.Valid || view.Source != "default" || view.Channels[0].Name != "général" {
44+ t.Fatalf("got %+v", view)
45+ }
46+}
47+
48+func TestAFileGivenExplicitlyIsNeverCreated(t *testing.T) {
49+ path := filepath.Join(t.TempDir(), "missing.yaml")
50+ core := app.New(config.Location{Path: path, Source: config.SourceFlag}, nil, starter)
51+ view := (&app.ConfigService{Core: core}).Current()
52+ if view.Valid || len(view.Problems) == 0 {
53+ t.Fatalf("a missing file is a problem: %+v", view)
54+ }
55+ if _, err := os.Stat(path); err == nil {
56+ t.Fatal("the file must not be created")
57+ }
58+}
59+
60+func TestServicesExposeTheConfigurationAndTheAgents(t *testing.T) {
61+ path := filepath.Join(t.TempDir(), "config.yaml")
62+ write(t, path, twoAgents)
63+ core := app.New(config.Location{Path: path, Source: config.SourceFlag}, nil, starter)
64+ view := (&app.ConfigService{Core: core}).Current()
65+ if !view.Valid || view.Theme != "dark" || view.Language != "fr" || len(view.Channels) != 1 {
66+ t.Fatalf("got %+v", view)
67+ }
68+ service := &app.AgentService{Core: core}
69+ if list := service.List(); len(list) != 2 || list[0].State != agents.StateStopped {
70+ t.Fatalf("got %+v", list)
71+ }
72+ if err := service.Start(context.Background(), "alice"); err != nil {
73+ t.Fatal(err)
74+ }
75+ if service.List()[0].State != agents.StateReady {
76+ t.Fatal("alice should be ready")
77+ }
78+ if err := service.Restart(context.Background(), "alice"); err != nil {
79+ t.Fatal(err)
80+ }
81+ if err := service.Stop(context.Background(), "alice"); err != nil {
82+ t.Fatal(err)
83+ }
84+ if _, err := service.Logs("alice"); err != nil {
85+ t.Fatal(err)
86+ }
87+ core.Shutdown(context.Background())
88+}
89+
90+func TestAnInvalidFileIsReportedWithLines(t *testing.T) {
91+ path := filepath.Join(t.TempDir(), "config.yaml")
92+ write(t, path, "version: 1\nagents:\n - name: Alice\n command: sh\n")
93+ core := app.New(config.Location{Path: path, Source: config.SourceFlag}, nil, starter)
94+ view := (&app.ConfigService{Core: core}).Current()
95+ if view.Valid || !strings.HasPrefix(view.Problems[0], "ligne 3 — agents[0].name") {
96+ t.Fatalf("got %+v", view)
97+ }
98+ if len((&app.AgentService{Core: core}).List()) != 0 {
99+ t.Fatal("an invalid file gives no agents")
100+ }
101+}
102+
103+func TestEditingTheFileReconfiguresAndNotifies(t *testing.T) {
104+ path := filepath.Join(t.TempDir(), "config.yaml")
105+ write(t, path, twoAgents)
106+ recorder := &events.Recorder{}
107+ core := app.New(config.Location{Path: path, Source: config.SourceFlag}, recorder, starter)
108+ ctx, cancel := context.WithCancel(context.Background())
109+ defer cancel()
110+ go core.Run(ctx)
111+ time.Sleep(100 * time.Millisecond)
112+
113+ if err := config.Save(path, []byte(strings.Replace(twoAgents, " - name: bob\n command: sh\n", "", 1))); err != nil {
114+ t.Fatal(err)
115+ }
116+ waitFor(t, func() bool { return len(recorder.Named(events.ConfigChanged)) > 0 })
117+ if list := (&app.AgentService{Core: core}).List(); len(list) != 1 {
118+ t.Fatalf("bob should be gone: %+v", list)
119+ }
120+
121+ write(t, path, "version: 1\nagents:\n - name: Bad\n")
122+ waitFor(t, func() bool { return len(recorder.Named(events.ConfigChanged)) > 1 })
123+ last := recorder.Named(events.ConfigChanged)[1].(events.ConfigChangedPayload)
124+ if last.Valid || len(last.Problems) == 0 {
125+ t.Fatalf("a broken file is reported: %+v", last)
126+ }
127+ if list := (&app.AgentService{Core: core}).List(); len(list) != 1 {
128+ t.Fatal("a broken file keeps the previous configuration")
129+ }
130+}
131+
132+func waitFor(t *testing.T, condition func() bool) {
133+ t.Helper()
134+ deadline := time.Now().Add(3 * time.Second)
135+ for !condition() {
136+ if time.Now().After(deadline) {
137+ t.Fatal("condition not met in time")
138+ }
139+ time.Sleep(10 * time.Millisecond)
140+ }
141+}
added internal/app/services.go +90 -0
new file mode 100644
@@ -0,0 +1,90 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package app
4+
5+import (
6+ "context"
7+
8+ "git.rickub.com/bots-garden/sidekicks/internal/agents"
9+)
10+
11+// AgentService lets the interface list, start, stop and restart agents and
12+// read their logs. Every change is also announced with agent:state events.
13+type AgentService struct {
14+ Core *Core
15+}
16+
17+// List returns every agent of the configuration with its state.
18+func (s *AgentService) List() []agents.Status {
19+ return s.Core.manager.Statuses()
20+}
21+
22+// Start starts the agent if it is not running.
23+func (s *AgentService) Start(ctx context.Context, name string) error {
24+ return s.Core.manager.Start(ctx, name)
25+}
26+
27+// Stop stops the agent, even if it is busy.
28+func (s *AgentService) Stop(ctx context.Context, name string) error {
29+ return s.Core.manager.Stop(ctx, name)
30+}
31+
32+// Restart stops then starts the agent, e.g. after a crash or a configuration change.
33+func (s *AgentService) Restart(ctx context.Context, name string) error {
34+ return s.Core.manager.Restart(ctx, name)
35+}
36+
37+// Logs returns the last lines the agent wrote on its standard error.
38+func (s *AgentService) Logs(name string) ([]string, error) {
39+ return s.Core.manager.Logs(name)
40+}
41+
42+// ConfigView is the configuration as the interface shows it.
43+type ConfigView struct {
44+ Path string `json:"path"`
45+ // Source is flag, environment, last-opened or default.
46+ Source string `json:"source"`
47+ // Created is true when Sidekicks has just created the default file.
48+ Created bool `json:"created"`
49+ // Valid is false when the file has problems; the last valid configuration
50+ // (or an empty one) is then in use.
51+ Valid bool `json:"valid"`
52+ Problems []string `json:"problems"`
53+ Warnings []string `json:"warnings"`
54+ Theme string `json:"theme"`
55+ Language string `json:"language"`
56+ Channels []ChannelView `json:"channels"`
57+}
58+
59+// ChannelView is a channel declared in the configuration.
60+type ChannelView struct {
61+ Name string `json:"name"`
62+ Description string `json:"description"`
63+ Agents []string `json:"agents"`
64+}
65+
66+// ConfigService tells the interface which configuration is loaded and what is
67+// wrong with it. A config:changed event says when to ask again.
68+type ConfigService struct {
69+ Core *Core
70+}
71+
72+// Current returns the configuration in use.
73+func (s *ConfigService) Current() ConfigView {
74+ c := s.Core
75+ c.mu.Lock()
76+ defer c.mu.Unlock()
77+ view := ConfigView{
78+ Path: c.location.Path, Source: string(c.location.Source), Created: c.created,
79+ Valid: len(c.problems) == 0, Problems: messages(c.problems), Warnings: messages(c.cfg.Warnings),
80+ Theme: c.cfg.UI.Theme, Language: c.cfg.UI.Language, Channels: []ChannelView{},
81+ }
82+ for _, channel := range c.cfg.Channels {
83+ members := channel.Agents
84+ if members == nil {
85+ members = []string{}
86+ }
87+ view.Channels = append(view.Channels, ChannelView{Name: channel.Name, Description: channel.Description, Agents: members})
88+ }
89+ return view
90+}
new file mode 100644
@@ -0,0 +1,90 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package app
4+
5+import (
6+ "context"
7+
8+ "git.rickub.com/bots-garden/sidekicks/internal/agents"
9+)
10+
11+// AgentService lets the interface list, start, stop and restart agents and
12+// read their logs. Every change is also announced with agent:state events.
13+type AgentService struct {
14+ Core *Core
15+}
16+
17+// List returns every agent of the configuration with its state.
18+func (s *AgentService) List() []agents.Status {
19+ return s.Core.manager.Statuses()
20+}
21+
22+// Start starts the agent if it is not running.
23+func (s *AgentService) Start(ctx context.Context, name string) error {
24+ return s.Core.manager.Start(ctx, name)
25+}
26+
27+// Stop stops the agent, even if it is busy.
28+func (s *AgentService) Stop(ctx context.Context, name string) error {
29+ return s.Core.manager.Stop(ctx, name)
30+}
31+
32+// Restart stops then starts the agent, e.g. after a crash or a configuration change.
33+func (s *AgentService) Restart(ctx context.Context, name string) error {
34+ return s.Core.manager.Restart(ctx, name)
35+}
36+
37+// Logs returns the last lines the agent wrote on its standard error.
38+func (s *AgentService) Logs(name string) ([]string, error) {
39+ return s.Core.manager.Logs(name)
40+}
41+
42+// ConfigView is the configuration as the interface shows it.
43+type ConfigView struct {
44+ Path string `json:"path"`
45+ // Source is flag, environment, last-opened or default.
46+ Source string `json:"source"`
47+ // Created is true when Sidekicks has just created the default file.
48+ Created bool `json:"created"`
49+ // Valid is false when the file has problems; the last valid configuration
50+ // (or an empty one) is then in use.
51+ Valid bool `json:"valid"`
52+ Problems []string `json:"problems"`
53+ Warnings []string `json:"warnings"`
54+ Theme string `json:"theme"`
55+ Language string `json:"language"`
56+ Channels []ChannelView `json:"channels"`
57+}
58+
59+// ChannelView is a channel declared in the configuration.
60+type ChannelView struct {
61+ Name string `json:"name"`
62+ Description string `json:"description"`
63+ Agents []string `json:"agents"`
64+}
65+
66+// ConfigService tells the interface which configuration is loaded and what is
67+// wrong with it. A config:changed event says when to ask again.
68+type ConfigService struct {
69+ Core *Core
70+}
71+
72+// Current returns the configuration in use.
73+func (s *ConfigService) Current() ConfigView {
74+ c := s.Core
75+ c.mu.Lock()
76+ defer c.mu.Unlock()
77+ view := ConfigView{
78+ Path: c.location.Path, Source: string(c.location.Source), Created: c.created,
79+ Valid: len(c.problems) == 0, Problems: messages(c.problems), Warnings: messages(c.cfg.Warnings),
80+ Theme: c.cfg.UI.Theme, Language: c.cfg.UI.Language, Channels: []ChannelView{},
81+ }
82+ for _, channel := range c.cfg.Channels {
83+ members := channel.Agents
84+ if members == nil {
85+ members = []string{}
86+ }
87+ view.Channels = append(view.Channels, ChannelView{Name: channel.Name, Description: channel.Description, Agents: members})
88+ }
89+ return view
90+}
added internal/config/config.go +303 -0
new file mode 100644
@@ -0,0 +1,303 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// Package config loads, validates, creates, saves and watches the Sidekicks
4+// configuration file (docs/en/reference/configuration.md).
5+//
6+// Loading a file returns either a Config ready to use — defaults applied,
7+// paths resolved — or an *Error listing every problem with its line, in the
8+// language of the interface:
9+//
10+// cfg, err := config.Load(path)
11+// var problems *config.Error
12+// if errors.As(err, &problems) {
13+// for _, p := range problems.Problems {
14+// fmt.Println(p) // "ligne 12 — agents[0].name : « Alice Smith » n'est pas un nom valide…"
15+// }
16+// }
17+package config
18+
19+import (
20+ "errors"
21+ "fmt"
22+ "os"
23+ "os/exec"
24+ "path/filepath"
25+ "strings"
26+ "time"
27+
28+ "go.yaml.in/yaml/v3"
29+)
30+
31+// Default values of the optional keys.
32+const (
33+ DefaultTheme = "system"
34+ DefaultLanguage = "fr"
35+ DefaultMaxRunningAgents = 2
36+ DefaultStopIdleAgentsAfter = 10 * time.Minute
37+ RuntimeLocal = "local"
38+)
39+
40+// Config is a loaded configuration: defaults applied and paths absolute.
41+type Config struct {
42+ // Path is the absolute path of the file it was loaded from.
43+ Path string
44+ Version int
45+ UI UI
46+ Limits Limits
47+ Agents []Agent
48+ Channels []Channel
49+ // Warnings are problems that do not prevent using the configuration,
50+ // such as an agent command that cannot be found yet.
51+ Warnings []Problem
52+}
53+
54+// UI holds the interface preferences.
55+type UI struct {
56+ Theme string // system, light or dark
57+ Language string // fr or en
58+}
59+
60+// Limits bound the resources agents use.
61+type Limits struct {
62+ MaxRunningAgents int
63+ // StopIdleAgentsAfter is 0 when idle agents are never stopped.
64+ StopIdleAgentsAfter time.Duration
65+}
66+
67+// Agent describes one agent Sidekicks can start.
68+type Agent struct {
69+ Name string
70+ Title string
71+ Icon string
72+ Description string
73+ // Command is either a bare program name looked up in PATH, or an absolute path.
74+ Command string
75+ Args []string
76+ Env map[string]string
77+ // Workspace is the absolute path of the agent's folder.
78+ Workspace string
79+ Runtime Runtime
80+}
81+
82+// Runtime tells where an agent runs.
83+type Runtime struct {
84+ Type string // local, container or sbx
85+ Container string
86+ Sandbox string
87+ WorkspacePath string
88+}
89+
90+// Channel is a channel declared in the file.
91+type Channel struct {
92+ Name string
93+ Description string
94+ Agents []string
95+}
96+
97+// Agent returns the agent called name.
98+func (c *Config) Agent(name string) (Agent, bool) {
99+ for _, agent := range c.Agents {
100+ if agent.Name == name {
101+ return agent, true
102+ }
103+ }
104+ return Agent{}, false
105+}
106+
107+// Load reads and parses the file at path. See Parse.
108+func Load(path string) (*Config, error) {
109+ absolute, err := filepath.Abs(path)
110+ if err != nil {
111+ return nil, err
112+ }
113+ content, err := os.ReadFile(absolute)
114+ if err != nil {
115+ return nil, err
116+ }
117+ return Parse(content, absolute)
118+}
119+
120+// Parse validates content and turns it into a Config. path is the absolute
121+// path of the file, used to resolve relative paths. Problems are returned as
122+// an *Error; an agent command that cannot be found is only a warning.
123+func Parse(content []byte, path string) (*Config, error) {
124+ var root yaml.Node
125+ if err := yaml.Unmarshal(content, &root); err != nil {
126+ return nil, newError(path, DefaultLanguage, syntaxProblem(err, DefaultLanguage))
127+ }
128+ language := declaredLanguage(&root)
129+ if problems := schemaProblems(content, &root, language); len(problems) > 0 {
130+ return nil, newError(path, language, problems...)
131+ }
132+
133+ var raw rawConfig
134+ if err := root.Decode(&raw); err != nil {
135+ return nil, newError(path, language, syntaxProblem(err, language))
136+ }
137+ if problems := crossEntryProblems(&raw, &root, language); len(problems) > 0 {
138+ return nil, newError(path, language, problems...)
139+ }
140+ cfg := raw.toConfig(filepath.Dir(path))
141+ cfg.Path = path
142+ cfg.Warnings = withLanguage(commandWarnings(cfg, &root, language), language)
143+ return cfg, nil
144+}
145+
146+// rawConfig mirrors the YAML file; optional values are left empty and filled by toConfig.
147+type rawConfig struct {
148+ Version int `yaml:"version"`
149+ UI struct {
150+ Theme string `yaml:"theme"`
151+ Language string `yaml:"language"`
152+ } `yaml:"ui"`
153+ Limits struct {
154+ MaxRunningAgents int `yaml:"maxRunningAgents"`
155+ StopIdleAgentsAfter string `yaml:"stopIdleAgentsAfter"`
156+ } `yaml:"limits"`
157+ Agents []struct {
158+ Name string `yaml:"name"`
159+ Title string `yaml:"title"`
160+ Icon string `yaml:"icon"`
161+ Description string `yaml:"description"`
162+ Command string `yaml:"command"`
163+ Args []string `yaml:"args"`
164+ Env map[string]string `yaml:"env"`
165+ Workspace string `yaml:"workspace"`
166+ Runtime struct {
167+ Type string `yaml:"type"`
168+ Container string `yaml:"container"`
169+ Sandbox string `yaml:"sandbox"`
170+ WorkspacePath string `yaml:"workspacePath"`
171+ } `yaml:"runtime"`
172+ } `yaml:"agents"`
173+ Channels []struct {
174+ Name string `yaml:"name"`
175+ Description string `yaml:"description"`
176+ Agents []string `yaml:"agents"`
177+ } `yaml:"channels"`
178+}
179+
180+func (raw *rawConfig) toConfig(baseDir string) *Config {
181+ cfg := &Config{
182+ Version: raw.Version,
183+ UI: UI{Theme: orDefault(raw.UI.Theme, DefaultTheme), Language: orDefault(raw.UI.Language, DefaultLanguage)},
184+ Limits: Limits{
185+ MaxRunningAgents: DefaultMaxRunningAgents,
186+ StopIdleAgentsAfter: DefaultStopIdleAgentsAfter,
187+ },
188+ }
189+ if raw.Limits.MaxRunningAgents > 0 {
190+ cfg.Limits.MaxRunningAgents = raw.Limits.MaxRunningAgents
191+ }
192+ if raw.Limits.StopIdleAgentsAfter != "" {
193+ // The schema has already checked the format.
194+ cfg.Limits.StopIdleAgentsAfter, _ = time.ParseDuration(raw.Limits.StopIdleAgentsAfter)
195+ }
196+ for _, agent := range raw.Agents {
197+ cfg.Agents = append(cfg.Agents, Agent{
198+ Name: agent.Name,
199+ Title: orDefault(agent.Title, agent.Name),
200+ Icon: agent.Icon,
201+ Description: agent.Description,
202+ Command: resolveCommand(agent.Command, baseDir),
203+ Args: agent.Args,
204+ Env: agent.Env,
205+ Workspace: resolvePath(orDefault(agent.Workspace, "."), baseDir),
206+ Runtime: Runtime{
207+ Type: orDefault(agent.Runtime.Type, RuntimeLocal),
208+ Container: agent.Runtime.Container,
209+ Sandbox: agent.Runtime.Sandbox,
210+ WorkspacePath: agent.Runtime.WorkspacePath,
211+ },
212+ })
213+ }
214+ for _, channel := range raw.Channels {
215+ cfg.Channels = append(cfg.Channels, Channel(channel))
216+ }
217+ return cfg
218+}
219+
220+func orDefault(value, fallback string) string {
221+ if value == "" {
222+ return fallback
223+ }
224+ return value
225+}
226+
227+// resolvePath makes path absolute: "~" is the home folder, relative paths are
228+// relative to baseDir (the folder of the configuration file).
229+func resolvePath(path, baseDir string) string {
230+ if path == "~" || strings.HasPrefix(path, "~/") {
231+ if home, err := os.UserHomeDir(); err == nil {
232+ path = filepath.Join(home, strings.TrimPrefix(path, "~"))
233+ }
234+ }
235+ if filepath.IsAbs(path) {
236+ return filepath.Clean(path)
237+ }
238+ return filepath.Join(baseDir, path)
239+}
240+
241+// resolveCommand keeps a bare name for a PATH lookup and resolves anything
242+// that contains a separator like a path.
243+func resolveCommand(command, baseDir string) string {
244+ if !strings.ContainsRune(command, '/') && !strings.HasPrefix(command, "~") {
245+ return command
246+ }
247+ return resolvePath(command, baseDir)
248+}
249+
250+// commandWarnings reports the agents whose command cannot be found. They stay
251+// usable: the program may be installed later.
252+func commandWarnings(cfg *Config, root *yaml.Node, language string) []Problem {
253+ var warnings []Problem
254+ for index, agent := range cfg.Agents {
255+ if commandExists(agent.Command) {
256+ continue
257+ }
258+ path := []string{"agents", fmt.Sprint(index), "command"}
259+ line, column := position(root, path)
260+ warnings = append(warnings, Problem{
261+ Line: line, Column: column, Path: displayPath(path), Warning: true,
262+ Message: message(language, msgCommandNotFound, agent.Command, agent.Name),
263+ })
264+ }
265+ return warnings
266+}
267+
268+func commandExists(command string) bool {
269+ if filepath.IsAbs(command) {
270+ info, err := os.Stat(command)
271+ return err == nil && !info.IsDir()
272+ }
273+ _, err := exec.LookPath(command)
274+ return err == nil
275+}
276+
277+// writeFileAtomically writes content next to path then renames it over path,
278+// so an editor or a watcher never sees a half-written file.
279+func writeFileAtomically(path string, content []byte) error {
280+ directory := filepath.Dir(path)
281+ if err := os.MkdirAll(directory, 0o755); err != nil {
282+ return err
283+ }
284+ temporary, err := os.CreateTemp(directory, ".sidekicks-*.yaml")
285+ if err != nil {
286+ return err
287+ }
288+ if _, err := temporary.Write(content); err != nil {
289+ _ = temporary.Close()
290+ _ = os.Remove(temporary.Name())
291+ return err
292+ }
293+ if err := temporary.Close(); err != nil {
294+ _ = os.Remove(temporary.Name())
295+ return err
296+ }
297+ if info, err := os.Stat(path); err == nil {
298+ _ = os.Chmod(temporary.Name(), info.Mode().Perm())
299+ } else if errors.Is(err, os.ErrNotExist) {
300+ _ = os.Chmod(temporary.Name(), 0o644)
301+ }
302+ return os.Rename(temporary.Name(), path)
303+}
new file mode 100644
@@ -0,0 +1,303 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// Package config loads, validates, creates, saves and watches the Sidekicks
4+// configuration file (docs/en/reference/configuration.md).
5+//
6+// Loading a file returns either a Config ready to use — defaults applied,
7+// paths resolved — or an *Error listing every problem with its line, in the
8+// language of the interface:
9+//
10+// cfg, err := config.Load(path)
11+// var problems *config.Error
12+// if errors.As(err, &problems) {
13+// for _, p := range problems.Problems {
14+// fmt.Println(p) // "ligne 12 — agents[0].name : « Alice Smith » n'est pas un nom valide…"
15+// }
16+// }
17+package config
18+
19+import (
20+ "errors"
21+ "fmt"
22+ "os"
23+ "os/exec"
24+ "path/filepath"
25+ "strings"
26+ "time"
27+
28+ "go.yaml.in/yaml/v3"
29+)
30+
31+// Default values of the optional keys.
32+const (
33+ DefaultTheme = "system"
34+ DefaultLanguage = "fr"
35+ DefaultMaxRunningAgents = 2
36+ DefaultStopIdleAgentsAfter = 10 * time.Minute
37+ RuntimeLocal = "local"
38+)
39+
40+// Config is a loaded configuration: defaults applied and paths absolute.
41+type Config struct {
42+ // Path is the absolute path of the file it was loaded from.
43+ Path string
44+ Version int
45+ UI UI
46+ Limits Limits
47+ Agents []Agent
48+ Channels []Channel
49+ // Warnings are problems that do not prevent using the configuration,
50+ // such as an agent command that cannot be found yet.
51+ Warnings []Problem
52+}
53+
54+// UI holds the interface preferences.
55+type UI struct {
56+ Theme string // system, light or dark
57+ Language string // fr or en
58+}
59+
60+// Limits bound the resources agents use.
61+type Limits struct {
62+ MaxRunningAgents int
63+ // StopIdleAgentsAfter is 0 when idle agents are never stopped.
64+ StopIdleAgentsAfter time.Duration
65+}
66+
67+// Agent describes one agent Sidekicks can start.
68+type Agent struct {
69+ Name string
70+ Title string
71+ Icon string
72+ Description string
73+ // Command is either a bare program name looked up in PATH, or an absolute path.
74+ Command string
75+ Args []string
76+ Env map[string]string
77+ // Workspace is the absolute path of the agent's folder.
78+ Workspace string
79+ Runtime Runtime
80+}
81+
82+// Runtime tells where an agent runs.
83+type Runtime struct {
84+ Type string // local, container or sbx
85+ Container string
86+ Sandbox string
87+ WorkspacePath string
88+}
89+
90+// Channel is a channel declared in the file.
91+type Channel struct {
92+ Name string
93+ Description string
94+ Agents []string
95+}
96+
97+// Agent returns the agent called name.
98+func (c *Config) Agent(name string) (Agent, bool) {
99+ for _, agent := range c.Agents {
100+ if agent.Name == name {
101+ return agent, true
102+ }
103+ }
104+ return Agent{}, false
105+}
106+
107+// Load reads and parses the file at path. See Parse.
108+func Load(path string) (*Config, error) {
109+ absolute, err := filepath.Abs(path)
110+ if err != nil {
111+ return nil, err
112+ }
113+ content, err := os.ReadFile(absolute)
114+ if err != nil {
115+ return nil, err
116+ }
117+ return Parse(content, absolute)
118+}
119+
120+// Parse validates content and turns it into a Config. path is the absolute
121+// path of the file, used to resolve relative paths. Problems are returned as
122+// an *Error; an agent command that cannot be found is only a warning.
123+func Parse(content []byte, path string) (*Config, error) {
124+ var root yaml.Node
125+ if err := yaml.Unmarshal(content, &root); err != nil {
126+ return nil, newError(path, DefaultLanguage, syntaxProblem(err, DefaultLanguage))
127+ }
128+ language := declaredLanguage(&root)
129+ if problems := schemaProblems(content, &root, language); len(problems) > 0 {
130+ return nil, newError(path, language, problems...)
131+ }
132+
133+ var raw rawConfig
134+ if err := root.Decode(&raw); err != nil {
135+ return nil, newError(path, language, syntaxProblem(err, language))
136+ }
137+ if problems := crossEntryProblems(&raw, &root, language); len(problems) > 0 {
138+ return nil, newError(path, language, problems...)
139+ }
140+ cfg := raw.toConfig(filepath.Dir(path))
141+ cfg.Path = path
142+ cfg.Warnings = withLanguage(commandWarnings(cfg, &root, language), language)
143+ return cfg, nil
144+}
145+
146+// rawConfig mirrors the YAML file; optional values are left empty and filled by toConfig.
147+type rawConfig struct {
148+ Version int `yaml:"version"`
149+ UI struct {
150+ Theme string `yaml:"theme"`
151+ Language string `yaml:"language"`
152+ } `yaml:"ui"`
153+ Limits struct {
154+ MaxRunningAgents int `yaml:"maxRunningAgents"`
155+ StopIdleAgentsAfter string `yaml:"stopIdleAgentsAfter"`
156+ } `yaml:"limits"`
157+ Agents []struct {
158+ Name string `yaml:"name"`
159+ Title string `yaml:"title"`
160+ Icon string `yaml:"icon"`
161+ Description string `yaml:"description"`
162+ Command string `yaml:"command"`
163+ Args []string `yaml:"args"`
164+ Env map[string]string `yaml:"env"`
165+ Workspace string `yaml:"workspace"`
166+ Runtime struct {
167+ Type string `yaml:"type"`
168+ Container string `yaml:"container"`
169+ Sandbox string `yaml:"sandbox"`
170+ WorkspacePath string `yaml:"workspacePath"`
171+ } `yaml:"runtime"`
172+ } `yaml:"agents"`
173+ Channels []struct {
174+ Name string `yaml:"name"`
175+ Description string `yaml:"description"`
176+ Agents []string `yaml:"agents"`
177+ } `yaml:"channels"`
178+}
179+
180+func (raw *rawConfig) toConfig(baseDir string) *Config {
181+ cfg := &Config{
182+ Version: raw.Version,
183+ UI: UI{Theme: orDefault(raw.UI.Theme, DefaultTheme), Language: orDefault(raw.UI.Language, DefaultLanguage)},
184+ Limits: Limits{
185+ MaxRunningAgents: DefaultMaxRunningAgents,
186+ StopIdleAgentsAfter: DefaultStopIdleAgentsAfter,
187+ },
188+ }
189+ if raw.Limits.MaxRunningAgents > 0 {
190+ cfg.Limits.MaxRunningAgents = raw.Limits.MaxRunningAgents
191+ }
192+ if raw.Limits.StopIdleAgentsAfter != "" {
193+ // The schema has already checked the format.
194+ cfg.Limits.StopIdleAgentsAfter, _ = time.ParseDuration(raw.Limits.StopIdleAgentsAfter)
195+ }
196+ for _, agent := range raw.Agents {
197+ cfg.Agents = append(cfg.Agents, Agent{
198+ Name: agent.Name,
199+ Title: orDefault(agent.Title, agent.Name),
200+ Icon: agent.Icon,
201+ Description: agent.Description,
202+ Command: resolveCommand(agent.Command, baseDir),
203+ Args: agent.Args,
204+ Env: agent.Env,
205+ Workspace: resolvePath(orDefault(agent.Workspace, "."), baseDir),
206+ Runtime: Runtime{
207+ Type: orDefault(agent.Runtime.Type, RuntimeLocal),
208+ Container: agent.Runtime.Container,
209+ Sandbox: agent.Runtime.Sandbox,
210+ WorkspacePath: agent.Runtime.WorkspacePath,
211+ },
212+ })
213+ }
214+ for _, channel := range raw.Channels {
215+ cfg.Channels = append(cfg.Channels, Channel(channel))
216+ }
217+ return cfg
218+}
219+
220+func orDefault(value, fallback string) string {
221+ if value == "" {
222+ return fallback
223+ }
224+ return value
225+}
226+
227+// resolvePath makes path absolute: "~" is the home folder, relative paths are
228+// relative to baseDir (the folder of the configuration file).
229+func resolvePath(path, baseDir string) string {
230+ if path == "~" || strings.HasPrefix(path, "~/") {
231+ if home, err := os.UserHomeDir(); err == nil {
232+ path = filepath.Join(home, strings.TrimPrefix(path, "~"))
233+ }
234+ }
235+ if filepath.IsAbs(path) {
236+ return filepath.Clean(path)
237+ }
238+ return filepath.Join(baseDir, path)
239+}
240+
241+// resolveCommand keeps a bare name for a PATH lookup and resolves anything
242+// that contains a separator like a path.
243+func resolveCommand(command, baseDir string) string {
244+ if !strings.ContainsRune(command, '/') && !strings.HasPrefix(command, "~") {
245+ return command
246+ }
247+ return resolvePath(command, baseDir)
248+}
249+
250+// commandWarnings reports the agents whose command cannot be found. They stay
251+// usable: the program may be installed later.
252+func commandWarnings(cfg *Config, root *yaml.Node, language string) []Problem {
253+ var warnings []Problem
254+ for index, agent := range cfg.Agents {
255+ if commandExists(agent.Command) {
256+ continue
257+ }
258+ path := []string{"agents", fmt.Sprint(index), "command"}
259+ line, column := position(root, path)
260+ warnings = append(warnings, Problem{
261+ Line: line, Column: column, Path: displayPath(path), Warning: true,
262+ Message: message(language, msgCommandNotFound, agent.Command, agent.Name),
263+ })
264+ }
265+ return warnings
266+}
267+
268+func commandExists(command string) bool {
269+ if filepath.IsAbs(command) {
270+ info, err := os.Stat(command)
271+ return err == nil && !info.IsDir()
272+ }
273+ _, err := exec.LookPath(command)
274+ return err == nil
275+}
276+
277+// writeFileAtomically writes content next to path then renames it over path,
278+// so an editor or a watcher never sees a half-written file.
279+func writeFileAtomically(path string, content []byte) error {
280+ directory := filepath.Dir(path)
281+ if err := os.MkdirAll(directory, 0o755); err != nil {
282+ return err
283+ }
284+ temporary, err := os.CreateTemp(directory, ".sidekicks-*.yaml")
285+ if err != nil {
286+ return err
287+ }
288+ if _, err := temporary.Write(content); err != nil {
289+ _ = temporary.Close()
290+ _ = os.Remove(temporary.Name())
291+ return err
292+ }
293+ if err := temporary.Close(); err != nil {
294+ _ = os.Remove(temporary.Name())
295+ return err
296+ }
297+ if info, err := os.Stat(path); err == nil {
298+ _ = os.Chmod(temporary.Name(), info.Mode().Perm())
299+ } else if errors.Is(err, os.ErrNotExist) {
300+ _ = os.Chmod(temporary.Name(), 0o644)
301+ }
302+ return os.Rename(temporary.Name(), path)
303+}
added internal/config/config_test.go +322 -0
new file mode 100644
@@ -0,0 +1,322 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package config_test
4+
5+import (
6+ "context"
7+ "errors"
8+ "fmt"
9+ "os"
10+ "path/filepath"
11+ "strings"
12+ "testing"
13+ "time"
14+
15+ "git.rickub.com/bots-garden/sidekicks/internal/config"
16+)
17+
18+const minimal = "version: 1\nagents:\n - name: alice\n command: sh\n"
19+
20+func parse(t *testing.T, content string) (*config.Config, error) {
21+ t.Helper()
22+ return config.Parse([]byte(content), "/home/me/team/config.yaml")
23+}
24+
25+func problems(t *testing.T, content string) []config.Problem {
26+ t.Helper()
27+ _, err := parse(t, content)
28+ var configError *config.Error
29+ if !errors.As(err, &configError) {
30+ t.Fatalf("want a *config.Error, got %v", err)
31+ }
32+ return configError.Problems
33+}
34+
35+func byPath(t *testing.T, found []config.Problem, path string) config.Problem {
36+ t.Helper()
37+ for _, problem := range found {
38+ if problem.Path == path {
39+ return problem
40+ }
41+ }
42+ t.Fatalf("no problem for %s in %+v", path, found)
43+ return config.Problem{}
44+}
45+
46+func TestMinimalFileGetsTheDefaults(t *testing.T) {
47+ cfg, err := parse(t, minimal)
48+ if err != nil {
49+ t.Fatal(err)
50+ }
51+ if cfg.UI.Theme != "system" || cfg.UI.Language != "fr" {
52+ t.Errorf("ui defaults: %+v", cfg.UI)
53+ }
54+ if cfg.Limits.MaxRunningAgents != 2 || cfg.Limits.StopIdleAgentsAfter != 10*time.Minute {
55+ t.Errorf("limits defaults: %+v", cfg.Limits)
56+ }
57+ alice := cfg.Agents[0]
58+ if alice.Title != "alice" || alice.Runtime.Type != "local" || alice.Workspace != "/home/me/team" {
59+ t.Errorf("agent defaults: %+v", alice)
60+ }
61+}
62+
63+func TestPathsAreResolvedAgainstTheConfigurationFolder(t *testing.T) {
64+ home, _ := os.UserHomeDir()
65+ cfg, err := parse(t, `version: 1
66+agents:
67+ - name: relative
68+ command: ../scripts/agents.sh
69+ workspace: ../agents/alice
70+ - name: absolute
71+ command: /usr/bin/env
72+ workspace: /srv/agent
73+ - name: home
74+ command: mm
75+ workspace: ~/agents/bob
76+`)
77+ if err != nil {
78+ t.Fatal(err)
79+ }
80+ cases := []struct{ got, want string }{
81+ {cfg.Agents[0].Command, "/home/me/scripts/agents.sh"},
82+ {cfg.Agents[0].Workspace, "/home/me/agents/alice"},
83+ {cfg.Agents[1].Workspace, "/srv/agent"},
84+ {cfg.Agents[2].Command, "mm"},
85+ {cfg.Agents[2].Workspace, filepath.Join(home, "agents/bob")},
86+ }
87+ for _, c := range cases {
88+ if c.got != c.want {
89+ t.Errorf("got %q, want %q", c.got, c.want)
90+ }
91+ }
92+}
93+
94+func TestLimitsAndUIAreRead(t *testing.T) {
95+ cfg, err := parse(t, "version: 1\nui:\n theme: dark\n language: en\nlimits:\n maxRunningAgents: 3\n stopIdleAgentsAfter: 0\nagents: []\n")
96+ if err != nil {
97+ t.Fatal(err)
98+ }
99+ if cfg.UI.Theme != "dark" || cfg.UI.Language != "en" || cfg.Limits.MaxRunningAgents != 3 || cfg.Limits.StopIdleAgentsAfter != 0 {
100+ t.Fatalf("got %+v %+v", cfg.UI, cfg.Limits)
101+ }
102+}
103+
104+func TestProblemsAreLocatedAndExplained(t *testing.T) {
105+ cases := []struct {
106+ name, content string
107+ line int
108+ path, message string
109+ }{
110+ {"unknown key with suggestion", "version: 1\nagents:\n - name: alice\n comand: mm\n", 4, "agents[0].comand", "vouliez-vous dire « command »"},
111+ {"missing key", "version: 1\nagents:\n - name: alice\n", 3, "agents[0]", "il manque la clé « command »"},
112+ {"invalid name", "version: 1\nagents:\n - name: Alice Smith\n command: mm\n", 3, "agents[0].name", "« Alice Smith » n'est pas un nom valide"},
113+ {"bad duration", "version: 1\nlimits:\n stopIdleAgentsAfter: dix minutes\nagents: []\n", 3, "limits.stopIdleAgentsAfter", "n'est pas une durée valide"},
114+ {"number other than 0 as duration", "version: 1\nlimits:\n stopIdleAgentsAfter: 5\nagents: []\n", 3, "limits.stopIdleAgentsAfter", "« 5 » n'est pas une durée valide"},
115+ {"unknown theme", "version: 1\nui:\n theme: blue\nagents: []\n", 3, "ui.theme", "choisissez parmi : system, light, dark"},
116+ {"wrong version", "version: 2\nagents: []\n", 1, "version", "seule la version 1 existe"},
117+ {"container without its name", "version: 1\nagents:\n - name: a\n command: mm\n runtime:\n type: container\n", 6, "agents[0].runtime", "il manque la clé « container »"},
118+ {"key not allowed with local", "version: 1\nagents:\n - name: a\n command: mm\n runtime:\n type: local\n sandbox: x\n", 7, "agents[0].runtime.sandbox", "n'a pas de sens avec ce type de runtime"},
119+ {"duplicate agent", "version: 1\nagents:\n - name: a\n command: mm\n - name: a\n command: mm\n", 5, "agents[1].name", "déjà un agent nommé « a »"},
120+ {"duplicate channel", "version: 1\nagents: []\nchannels:\n - name: x\n - name: x\n", 5, "channels[1].name", "déjà un channel nommé « x »"},
121+ {"unknown member", "version: 1\nagents: []\nchannels:\n - name: x\n agents: [bob]\n", 5, "channels[0].agents[0]", "aucun agent ne s'appelle « bob »"},
122+ {"duplicate member", "version: 1\nagents: []\nchannels:\n - name: x\n agents: [bob, bob]\n", 5, "channels[0].agents", "« bob » apparaît deux fois"},
123+ {"yaml syntax", "version: 1\nagents: [\n", 2, "", "pas du YAML valide"},
124+ }
125+ for _, c := range cases {
126+ t.Run(c.name, func(t *testing.T) {
127+ found := problems(t, c.content)
128+ for _, problem := range found {
129+ if problem.Line == c.line && problem.Path == c.path && strings.Contains(problem.Message, c.message) {
130+ return
131+ }
132+ }
133+ t.Fatalf("no problem at line %d, %q containing %q in %+v", c.line, c.path, c.message, found)
134+ })
135+ }
136+}
137+
138+func TestProblemsFollowTheDeclaredLanguage(t *testing.T) {
139+ found := problems(t, "version: 1\nui:\n language: en\nagents:\n - name: alice\n comand: mm\n")
140+ got := byPath(t, found, "agents[0].comand").String()
141+ want := `line 6 — agents[0].comand: unknown key "comand"; did you mean "command"?`
142+ if got != want {
143+ t.Fatalf("got %s\nwant %s", got, want)
144+ }
145+}
146+
147+func TestFrenchProblemFormat(t *testing.T) {
148+ found := problems(t, "version: 1\nagents:\n - name: alice\n comand: mm\n")
149+ got := byPath(t, found, "agents[0].comand").String()
150+ want := "ligne 4 — agents[0].comand : clé inconnue « comand » ; vouliez-vous dire « command » ?"
151+ if got != want {
152+ t.Fatalf("got %s\nwant %s", got, want)
153+ }
154+}
155+
156+func TestMissingCommandIsOnlyAWarning(t *testing.T) {
157+ cfg, err := parse(t, "version: 1\nagents:\n - name: ghost\n command: no-such-program-xyz\n")
158+ if err != nil {
159+ t.Fatal(err)
160+ }
161+ if len(cfg.Warnings) != 1 || !cfg.Warnings[0].Warning || cfg.Warnings[0].Line != 4 {
162+ t.Fatalf("want one warning at line 4, got %+v", cfg.Warnings)
163+ }
164+}
165+
166+func TestTheProjectExampleLoads(t *testing.T) {
167+ cfg, err := config.Load("../../examples/test-agents.yaml")
168+ if err != nil {
169+ t.Fatal(err)
170+ }
171+ if len(cfg.Agents) != 4 || len(cfg.Channels) != 2 {
172+ t.Fatalf("got %d agents, %d channels", len(cfg.Agents), len(cfg.Channels))
173+ }
174+ if len(cfg.Warnings) != 0 {
175+ t.Fatalf("scripts/agents.sh should be found: %v", cfg.Warnings)
176+ }
177+}
178+
179+func TestLocatePrecedence(t *testing.T) {
180+ env := func(value string) func(string) string {
181+ return func(key string) string {
182+ if key == config.EnvironmentVariable {
183+ return value
184+ }
185+ return ""
186+ }
187+ }
188+ defaultPath, _ := config.DefaultPath()
189+ cases := []struct {
190+ flag, environment, last string
191+ want config.Location
192+ }{
193+ {"/a.yaml", "/b.yaml", "/c.yaml", config.Location{Path: "/a.yaml", Source: config.SourceFlag}},
194+ {"", "/b.yaml", "/c.yaml", config.Location{Path: "/b.yaml", Source: config.SourceEnvironment}},
195+ {"", "", "/c.yaml", config.Location{Path: "/c.yaml", Source: config.SourceLastOpened}},
196+ {"", "", "", config.Location{Path: defaultPath, Source: config.SourceDefault}},
197+ }
198+ for _, c := range cases {
199+ got, err := config.Locate(c.flag, env(c.environment), c.last)
200+ if err != nil || got != c.want {
201+ t.Errorf("Locate(%q, %q, %q) = %+v, %v; want %+v", c.flag, c.environment, c.last, got, err, c.want)
202+ }
203+ }
204+}
205+
206+func TestDefaultPathEndsWithSidekicksConfig(t *testing.T) {
207+ path, err := config.DefaultPath()
208+ if err != nil {
209+ t.Fatal(err)
210+ }
211+ if !strings.HasSuffix(path, filepath.Join("sidekicks", "config.yaml")) {
212+ t.Fatal(path)
213+ }
214+}
215+
216+func TestEnsureDefaultCreatesAValidCommentedFileOnce(t *testing.T) {
217+ path := filepath.Join(t.TempDir(), "sidekicks", "config.yaml")
218+ created, err := config.EnsureDefault(path)
219+ if err != nil || !created {
220+ t.Fatalf("created=%v err=%v", created, err)
221+ }
222+ cfg, err := config.Load(path)
223+ if err != nil {
224+ t.Fatal(err)
225+ }
226+ if len(cfg.Agents) != 0 || cfg.Channels[0].Name != "général" {
227+ t.Fatalf("unexpected default: %+v", cfg)
228+ }
229+ if err := os.WriteFile(path, []byte(minimal), 0o644); err != nil {
230+ t.Fatal(err)
231+ }
232+ created, err = config.EnsureDefault(path)
233+ if err != nil || created {
234+ t.Fatalf("an existing file must be left alone: created=%v err=%v", created, err)
235+ }
236+ if content, _ := os.ReadFile(path); string(content) != minimal {
237+ t.Fatal("existing file was modified")
238+ }
239+}
240+
241+func TestSaveRefusesAnInvalidFileAndKeepsTheOldOne(t *testing.T) {
242+ path := filepath.Join(t.TempDir(), "config.yaml")
243+ if err := config.Save(path, []byte(minimal)); err != nil {
244+ t.Fatal(err)
245+ }
246+ if err := config.Save(path, []byte("version: 1\nagents:\n - name: Bad Name\n command: mm\n")); err == nil {
247+ t.Fatal("an invalid file must not be saved")
248+ }
249+ if content, _ := os.ReadFile(path); string(content) != minimal {
250+ t.Fatal("the previous file was overwritten")
251+ }
252+}
253+
254+func TestAppendAgentKeepsComments(t *testing.T) {
255+ original := "# my team\nversion: 1\n\nagents: [] # nobody yet\n# a channel for everything\nchannels:\n - name: général\n"
256+ updated, err := config.AppendAgent([]byte(original), config.AgentEntry{Name: "bob", Title: "Bob", Command: "mm", Args: []string{"-acp"}})
257+ if err != nil {
258+ t.Fatal(err)
259+ }
260+ for _, comment := range []string{"# my team", "# a channel for everything"} {
261+ if !strings.Contains(string(updated), comment) {
262+ t.Errorf("comment %q lost:\n%s", comment, updated)
263+ }
264+ }
265+ cfg, err := config.Parse(updated, "/tmp/config.yaml")
266+ if err != nil {
267+ t.Fatal(err)
268+ }
269+ if bob, ok := cfg.Agent("bob"); !ok || bob.Title != "Bob" || bob.Args[0] != "-acp" {
270+ t.Fatalf("bob not added correctly:\n%s", updated)
271+ }
272+}
273+
274+func TestAppendAgentRejectsADuplicate(t *testing.T) {
275+ if _, err := config.AppendAgent([]byte(minimal), config.AgentEntry{Name: "alice", Command: "mm"}); err == nil {
276+ t.Fatal("a second alice must be refused")
277+ }
278+}
279+
280+func TestWatchReloadsAfterAnAtomicSave(t *testing.T) {
281+ path := filepath.Join(t.TempDir(), "config.yaml")
282+ if err := config.Save(path, []byte(minimal)); err != nil {
283+ t.Fatal(err)
284+ }
285+ ctx, cancel := context.WithCancel(context.Background())
286+ defer cancel()
287+ reloaded := make(chan *config.Config, 4)
288+ go func() {
289+ _ = config.Watch(ctx, path, func(cfg *config.Config, err error) {
290+ if err == nil {
291+ reloaded <- cfg
292+ }
293+ })
294+ }()
295+ time.Sleep(100 * time.Millisecond) // let the watcher start
296+ updated := strings.Replace(minimal, "agents:", "ui:\n theme: dark\nagents:", 1)
297+ if err := config.Save(path, []byte(updated)); err != nil {
298+ t.Fatal(err)
299+ }
300+ select {
301+ case cfg := <-reloaded:
302+ if cfg.UI.Theme != "dark" {
303+ t.Fatalf("reloaded the old content: %+v", cfg.UI)
304+ }
305+ case <-time.After(3 * time.Second):
306+ t.Fatal("no reload after saving")
307+ }
308+}
309+
310+func ExampleLoad() {
311+ directory, _ := os.MkdirTemp("", "sidekicks")
312+ defer func() { _ = os.RemoveAll(directory) }()
313+ path := filepath.Join(directory, "config.yaml")
314+ _ = os.WriteFile(path, []byte("version: 1\nagents:\n - name: Alice\n command: mm\n"), 0o644)
315+
316+ _, err := config.Load(path)
317+ var problems *config.Error
318+ if errors.As(err, &problems) {
319+ fmt.Println(problems.Problems[0])
320+ }
321+ // Output: ligne 3 — agents[0].name : « Alice » n'est pas un nom valide : minuscules, chiffres, « - » ou « _ », sans espace, en commençant par une lettre (ex. « alice »)
322+}
new file mode 100644
@@ -0,0 +1,322 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package config_test
4+
5+import (
6+ "context"
7+ "errors"
8+ "fmt"
9+ "os"
10+ "path/filepath"
11+ "strings"
12+ "testing"
13+ "time"
14+
15+ "git.rickub.com/bots-garden/sidekicks/internal/config"
16+)
17+
18+const minimal = "version: 1\nagents:\n - name: alice\n command: sh\n"
19+
20+func parse(t *testing.T, content string) (*config.Config, error) {
21+ t.Helper()
22+ return config.Parse([]byte(content), "/home/me/team/config.yaml")
23+}
24+
25+func problems(t *testing.T, content string) []config.Problem {
26+ t.Helper()
27+ _, err := parse(t, content)
28+ var configError *config.Error
29+ if !errors.As(err, &configError) {
30+ t.Fatalf("want a *config.Error, got %v", err)
31+ }
32+ return configError.Problems
33+}
34+
35+func byPath(t *testing.T, found []config.Problem, path string) config.Problem {
36+ t.Helper()
37+ for _, problem := range found {
38+ if problem.Path == path {
39+ return problem
40+ }
41+ }
42+ t.Fatalf("no problem for %s in %+v", path, found)
43+ return config.Problem{}
44+}
45+
46+func TestMinimalFileGetsTheDefaults(t *testing.T) {
47+ cfg, err := parse(t, minimal)
48+ if err != nil {
49+ t.Fatal(err)
50+ }
51+ if cfg.UI.Theme != "system" || cfg.UI.Language != "fr" {
52+ t.Errorf("ui defaults: %+v", cfg.UI)
53+ }
54+ if cfg.Limits.MaxRunningAgents != 2 || cfg.Limits.StopIdleAgentsAfter != 10*time.Minute {
55+ t.Errorf("limits defaults: %+v", cfg.Limits)
56+ }
57+ alice := cfg.Agents[0]
58+ if alice.Title != "alice" || alice.Runtime.Type != "local" || alice.Workspace != "/home/me/team" {
59+ t.Errorf("agent defaults: %+v", alice)
60+ }
61+}
62+
63+func TestPathsAreResolvedAgainstTheConfigurationFolder(t *testing.T) {
64+ home, _ := os.UserHomeDir()
65+ cfg, err := parse(t, `version: 1
66+agents:
67+ - name: relative
68+ command: ../scripts/agents.sh
69+ workspace: ../agents/alice
70+ - name: absolute
71+ command: /usr/bin/env
72+ workspace: /srv/agent
73+ - name: home
74+ command: mm
75+ workspace: ~/agents/bob
76+`)
77+ if err != nil {
78+ t.Fatal(err)
79+ }
80+ cases := []struct{ got, want string }{
81+ {cfg.Agents[0].Command, "/home/me/scripts/agents.sh"},
82+ {cfg.Agents[0].Workspace, "/home/me/agents/alice"},
83+ {cfg.Agents[1].Workspace, "/srv/agent"},
84+ {cfg.Agents[2].Command, "mm"},
85+ {cfg.Agents[2].Workspace, filepath.Join(home, "agents/bob")},
86+ }
87+ for _, c := range cases {
88+ if c.got != c.want {
89+ t.Errorf("got %q, want %q", c.got, c.want)
90+ }
91+ }
92+}
93+
94+func TestLimitsAndUIAreRead(t *testing.T) {
95+ cfg, err := parse(t, "version: 1\nui:\n theme: dark\n language: en\nlimits:\n maxRunningAgents: 3\n stopIdleAgentsAfter: 0\nagents: []\n")
96+ if err != nil {
97+ t.Fatal(err)
98+ }
99+ if cfg.UI.Theme != "dark" || cfg.UI.Language != "en" || cfg.Limits.MaxRunningAgents != 3 || cfg.Limits.StopIdleAgentsAfter != 0 {
100+ t.Fatalf("got %+v %+v", cfg.UI, cfg.Limits)
101+ }
102+}
103+
104+func TestProblemsAreLocatedAndExplained(t *testing.T) {
105+ cases := []struct {
106+ name, content string
107+ line int
108+ path, message string
109+ }{
110+ {"unknown key with suggestion", "version: 1\nagents:\n - name: alice\n comand: mm\n", 4, "agents[0].comand", "vouliez-vous dire « command »"},
111+ {"missing key", "version: 1\nagents:\n - name: alice\n", 3, "agents[0]", "il manque la clé « command »"},
112+ {"invalid name", "version: 1\nagents:\n - name: Alice Smith\n command: mm\n", 3, "agents[0].name", "« Alice Smith » n'est pas un nom valide"},
113+ {"bad duration", "version: 1\nlimits:\n stopIdleAgentsAfter: dix minutes\nagents: []\n", 3, "limits.stopIdleAgentsAfter", "n'est pas une durée valide"},
114+ {"number other than 0 as duration", "version: 1\nlimits:\n stopIdleAgentsAfter: 5\nagents: []\n", 3, "limits.stopIdleAgentsAfter", "« 5 » n'est pas une durée valide"},
115+ {"unknown theme", "version: 1\nui:\n theme: blue\nagents: []\n", 3, "ui.theme", "choisissez parmi : system, light, dark"},
116+ {"wrong version", "version: 2\nagents: []\n", 1, "version", "seule la version 1 existe"},
117+ {"container without its name", "version: 1\nagents:\n - name: a\n command: mm\n runtime:\n type: container\n", 6, "agents[0].runtime", "il manque la clé « container »"},
118+ {"key not allowed with local", "version: 1\nagents:\n - name: a\n command: mm\n runtime:\n type: local\n sandbox: x\n", 7, "agents[0].runtime.sandbox", "n'a pas de sens avec ce type de runtime"},
119+ {"duplicate agent", "version: 1\nagents:\n - name: a\n command: mm\n - name: a\n command: mm\n", 5, "agents[1].name", "déjà un agent nommé « a »"},
120+ {"duplicate channel", "version: 1\nagents: []\nchannels:\n - name: x\n - name: x\n", 5, "channels[1].name", "déjà un channel nommé « x »"},
121+ {"unknown member", "version: 1\nagents: []\nchannels:\n - name: x\n agents: [bob]\n", 5, "channels[0].agents[0]", "aucun agent ne s'appelle « bob »"},
122+ {"duplicate member", "version: 1\nagents: []\nchannels:\n - name: x\n agents: [bob, bob]\n", 5, "channels[0].agents", "« bob » apparaît deux fois"},
123+ {"yaml syntax", "version: 1\nagents: [\n", 2, "", "pas du YAML valide"},
124+ }
125+ for _, c := range cases {
126+ t.Run(c.name, func(t *testing.T) {
127+ found := problems(t, c.content)
128+ for _, problem := range found {
129+ if problem.Line == c.line && problem.Path == c.path && strings.Contains(problem.Message, c.message) {
130+ return
131+ }
132+ }
133+ t.Fatalf("no problem at line %d, %q containing %q in %+v", c.line, c.path, c.message, found)
134+ })
135+ }
136+}
137+
138+func TestProblemsFollowTheDeclaredLanguage(t *testing.T) {
139+ found := problems(t, "version: 1\nui:\n language: en\nagents:\n - name: alice\n comand: mm\n")
140+ got := byPath(t, found, "agents[0].comand").String()
141+ want := `line 6 — agents[0].comand: unknown key "comand"; did you mean "command"?`
142+ if got != want {
143+ t.Fatalf("got %s\nwant %s", got, want)
144+ }
145+}
146+
147+func TestFrenchProblemFormat(t *testing.T) {
148+ found := problems(t, "version: 1\nagents:\n - name: alice\n comand: mm\n")
149+ got := byPath(t, found, "agents[0].comand").String()
150+ want := "ligne 4 — agents[0].comand : clé inconnue « comand » ; vouliez-vous dire « command » ?"
151+ if got != want {
152+ t.Fatalf("got %s\nwant %s", got, want)
153+ }
154+}
155+
156+func TestMissingCommandIsOnlyAWarning(t *testing.T) {
157+ cfg, err := parse(t, "version: 1\nagents:\n - name: ghost\n command: no-such-program-xyz\n")
158+ if err != nil {
159+ t.Fatal(err)
160+ }
161+ if len(cfg.Warnings) != 1 || !cfg.Warnings[0].Warning || cfg.Warnings[0].Line != 4 {
162+ t.Fatalf("want one warning at line 4, got %+v", cfg.Warnings)
163+ }
164+}
165+
166+func TestTheProjectExampleLoads(t *testing.T) {
167+ cfg, err := config.Load("../../examples/test-agents.yaml")
168+ if err != nil {
169+ t.Fatal(err)
170+ }
171+ if len(cfg.Agents) != 4 || len(cfg.Channels) != 2 {
172+ t.Fatalf("got %d agents, %d channels", len(cfg.Agents), len(cfg.Channels))
173+ }
174+ if len(cfg.Warnings) != 0 {
175+ t.Fatalf("scripts/agents.sh should be found: %v", cfg.Warnings)
176+ }
177+}
178+
179+func TestLocatePrecedence(t *testing.T) {
180+ env := func(value string) func(string) string {
181+ return func(key string) string {
182+ if key == config.EnvironmentVariable {
183+ return value
184+ }
185+ return ""
186+ }
187+ }
188+ defaultPath, _ := config.DefaultPath()
189+ cases := []struct {
190+ flag, environment, last string
191+ want config.Location
192+ }{
193+ {"/a.yaml", "/b.yaml", "/c.yaml", config.Location{Path: "/a.yaml", Source: config.SourceFlag}},
194+ {"", "/b.yaml", "/c.yaml", config.Location{Path: "/b.yaml", Source: config.SourceEnvironment}},
195+ {"", "", "/c.yaml", config.Location{Path: "/c.yaml", Source: config.SourceLastOpened}},
196+ {"", "", "", config.Location{Path: defaultPath, Source: config.SourceDefault}},
197+ }
198+ for _, c := range cases {
199+ got, err := config.Locate(c.flag, env(c.environment), c.last)
200+ if err != nil || got != c.want {
201+ t.Errorf("Locate(%q, %q, %q) = %+v, %v; want %+v", c.flag, c.environment, c.last, got, err, c.want)
202+ }
203+ }
204+}
205+
206+func TestDefaultPathEndsWithSidekicksConfig(t *testing.T) {
207+ path, err := config.DefaultPath()
208+ if err != nil {
209+ t.Fatal(err)
210+ }
211+ if !strings.HasSuffix(path, filepath.Join("sidekicks", "config.yaml")) {
212+ t.Fatal(path)
213+ }
214+}
215+
216+func TestEnsureDefaultCreatesAValidCommentedFileOnce(t *testing.T) {
217+ path := filepath.Join(t.TempDir(), "sidekicks", "config.yaml")
218+ created, err := config.EnsureDefault(path)
219+ if err != nil || !created {
220+ t.Fatalf("created=%v err=%v", created, err)
221+ }
222+ cfg, err := config.Load(path)
223+ if err != nil {
224+ t.Fatal(err)
225+ }
226+ if len(cfg.Agents) != 0 || cfg.Channels[0].Name != "général" {
227+ t.Fatalf("unexpected default: %+v", cfg)
228+ }
229+ if err := os.WriteFile(path, []byte(minimal), 0o644); err != nil {
230+ t.Fatal(err)
231+ }
232+ created, err = config.EnsureDefault(path)
233+ if err != nil || created {
234+ t.Fatalf("an existing file must be left alone: created=%v err=%v", created, err)
235+ }
236+ if content, _ := os.ReadFile(path); string(content) != minimal {
237+ t.Fatal("existing file was modified")
238+ }
239+}
240+
241+func TestSaveRefusesAnInvalidFileAndKeepsTheOldOne(t *testing.T) {
242+ path := filepath.Join(t.TempDir(), "config.yaml")
243+ if err := config.Save(path, []byte(minimal)); err != nil {
244+ t.Fatal(err)
245+ }
246+ if err := config.Save(path, []byte("version: 1\nagents:\n - name: Bad Name\n command: mm\n")); err == nil {
247+ t.Fatal("an invalid file must not be saved")
248+ }
249+ if content, _ := os.ReadFile(path); string(content) != minimal {
250+ t.Fatal("the previous file was overwritten")
251+ }
252+}
253+
254+func TestAppendAgentKeepsComments(t *testing.T) {
255+ original := "# my team\nversion: 1\n\nagents: [] # nobody yet\n# a channel for everything\nchannels:\n - name: général\n"
256+ updated, err := config.AppendAgent([]byte(original), config.AgentEntry{Name: "bob", Title: "Bob", Command: "mm", Args: []string{"-acp"}})
257+ if err != nil {
258+ t.Fatal(err)
259+ }
260+ for _, comment := range []string{"# my team", "# a channel for everything"} {
261+ if !strings.Contains(string(updated), comment) {
262+ t.Errorf("comment %q lost:\n%s", comment, updated)
263+ }
264+ }
265+ cfg, err := config.Parse(updated, "/tmp/config.yaml")
266+ if err != nil {
267+ t.Fatal(err)
268+ }
269+ if bob, ok := cfg.Agent("bob"); !ok || bob.Title != "Bob" || bob.Args[0] != "-acp" {
270+ t.Fatalf("bob not added correctly:\n%s", updated)
271+ }
272+}
273+
274+func TestAppendAgentRejectsADuplicate(t *testing.T) {
275+ if _, err := config.AppendAgent([]byte(minimal), config.AgentEntry{Name: "alice", Command: "mm"}); err == nil {
276+ t.Fatal("a second alice must be refused")
277+ }
278+}
279+
280+func TestWatchReloadsAfterAnAtomicSave(t *testing.T) {
281+ path := filepath.Join(t.TempDir(), "config.yaml")
282+ if err := config.Save(path, []byte(minimal)); err != nil {
283+ t.Fatal(err)
284+ }
285+ ctx, cancel := context.WithCancel(context.Background())
286+ defer cancel()
287+ reloaded := make(chan *config.Config, 4)
288+ go func() {
289+ _ = config.Watch(ctx, path, func(cfg *config.Config, err error) {
290+ if err == nil {
291+ reloaded <- cfg
292+ }
293+ })
294+ }()
295+ time.Sleep(100 * time.Millisecond) // let the watcher start
296+ updated := strings.Replace(minimal, "agents:", "ui:\n theme: dark\nagents:", 1)
297+ if err := config.Save(path, []byte(updated)); err != nil {
298+ t.Fatal(err)
299+ }
300+ select {
301+ case cfg := <-reloaded:
302+ if cfg.UI.Theme != "dark" {
303+ t.Fatalf("reloaded the old content: %+v", cfg.UI)
304+ }
305+ case <-time.After(3 * time.Second):
306+ t.Fatal("no reload after saving")
307+ }
308+}
309+
310+func ExampleLoad() {
311+ directory, _ := os.MkdirTemp("", "sidekicks")
312+ defer func() { _ = os.RemoveAll(directory) }()
313+ path := filepath.Join(directory, "config.yaml")
314+ _ = os.WriteFile(path, []byte("version: 1\nagents:\n - name: Alice\n command: mm\n"), 0o644)
315+
316+ _, err := config.Load(path)
317+ var problems *config.Error
318+ if errors.As(err, &problems) {
319+ fmt.Println(problems.Problems[0])
320+ }
321+ // Output: ligne 3 — agents[0].name : « Alice » n'est pas un nom valide : minuscules, chiffres, « - » ou « _ », sans espace, en commençant par une lettre (ex. « alice »)
322+}
added internal/config/default.go +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package config
4+
5+import (
6+ "errors"
7+ "os"
8+)
9+
10+// DefaultContent is written when no configuration file exists yet. Every key
11+// is explained, because the people reading it are not developers.
12+const DefaultContent = `# Configuration de Sidekicks
13+#
14+# Ce fichier décrit les agents avec lesquels vous pouvez discuter.
15+# Il est modifiable dans Sidekicks (panneau Settings) ou avec n'importe quel éditeur.
16+# Documentation : docs/fr/reference/configuration.md
17+
18+# Version du format de ce fichier. Ne la changez pas.
19+version: 1
20+
21+ui:
22+ # Thème : system (comme l'ordinateur), light (clair) ou dark (sombre).
23+ theme: system
24+ # Langue de l'interface : fr ou en.
25+ language: fr
26+
27+limits:
28+ # Nombre d'agents actifs en même temps. Chaque agent local occupe de la mémoire.
29+ maxRunningAgents: 2
30+ # Un agent inutilisé depuis ce délai est arrêté pour libérer la mémoire (ex. 30s, 10m, 1h).
31+ stopIdleAgentsAfter: 10m
32+
33+# Les agents : on les appelle dans une conversation avec @nom.
34+agents: []
35+# Exemple, à copier entre les crochets ci-dessus (en enlevant les « # ») :
36+# - name: alice # le nom utilisé avec @alice
37+# title: Alice (assistante) # le nom affiché
38+# icon: "👩"
39+# description: Répond aux questions simples
40+# command: mm # le programme de l'agent
41+# args: ["-acp", "-config", "agent.yaml"]
42+# workspace: ~/agents/alice # le dossier de l'agent
43+
44+# Les channels créés au démarrage s'ils n'existent pas encore.
45+channels:
46+ - name: général
47+ description: Pour commencer
48+`
49+
50+// EnsureDefault creates the file at path with DefaultContent when it does
51+// not exist, and reports whether it did. An existing file is never touched.
52+//
53+// created, err := config.EnsureDefault(location.Path)
54+func EnsureDefault(path string) (bool, error) {
55+ _, err := os.Stat(path)
56+ if err == nil {
57+ return false, nil
58+ }
59+ if !errors.Is(err, os.ErrNotExist) {
60+ return false, err
61+ }
62+ return true, writeFileAtomically(path, []byte(DefaultContent))
63+}
new file mode 100644
@@ -0,0 +1,63 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package config
4+
5+import (
6+ "errors"
7+ "os"
8+)
9+
10+// DefaultContent is written when no configuration file exists yet. Every key
11+// is explained, because the people reading it are not developers.
12+const DefaultContent = `# Configuration de Sidekicks
13+#
14+# Ce fichier décrit les agents avec lesquels vous pouvez discuter.
15+# Il est modifiable dans Sidekicks (panneau Settings) ou avec n'importe quel éditeur.
16+# Documentation : docs/fr/reference/configuration.md
17+
18+# Version du format de ce fichier. Ne la changez pas.
19+version: 1
20+
21+ui:
22+ # Thème : system (comme l'ordinateur), light (clair) ou dark (sombre).
23+ theme: system
24+ # Langue de l'interface : fr ou en.
25+ language: fr
26+
27+limits:
28+ # Nombre d'agents actifs en même temps. Chaque agent local occupe de la mémoire.
29+ maxRunningAgents: 2
30+ # Un agent inutilisé depuis ce délai est arrêté pour libérer la mémoire (ex. 30s, 10m, 1h).
31+ stopIdleAgentsAfter: 10m
32+
33+# Les agents : on les appelle dans une conversation avec @nom.
34+agents: []
35+# Exemple, à copier entre les crochets ci-dessus (en enlevant les « # ») :
36+# - name: alice # le nom utilisé avec @alice
37+# title: Alice (assistante) # le nom affiché
38+# icon: "👩"
39+# description: Répond aux questions simples
40+# command: mm # le programme de l'agent
41+# args: ["-acp", "-config", "agent.yaml"]
42+# workspace: ~/agents/alice # le dossier de l'agent
43+
44+# Les channels créés au démarrage s'ils n'existent pas encore.
45+channels:
46+ - name: général
47+ description: Pour commencer
48+`
49+
50+// EnsureDefault creates the file at path with DefaultContent when it does
51+// not exist, and reports whether it did. An existing file is never touched.
52+//
53+// created, err := config.EnsureDefault(location.Path)
54+func EnsureDefault(path string) (bool, error) {
55+ _, err := os.Stat(path)
56+ if err == nil {
57+ return false, nil
58+ }
59+ if !errors.Is(err, os.ErrNotExist) {
60+ return false, err
61+ }
62+ return true, writeFileAtomically(path, []byte(DefaultContent))
63+}
added internal/config/locate.go +65 -0
new file mode 100644
@@ -0,0 +1,65 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package config
4+
5+import (
6+ "os"
7+ "path/filepath"
8+)
9+
10+// EnvironmentVariable overrides the default location of the configuration file.
11+const EnvironmentVariable = "SIDEKICKS_CONFIG"
12+
13+// Source tells where the configuration path came from.
14+type Source string
15+
16+// The sources, in order of precedence.
17+const (
18+ SourceFlag Source = "flag"
19+ SourceEnvironment Source = "environment"
20+ SourceLastOpened Source = "last-opened"
21+ SourceDefault Source = "default"
22+)
23+
24+// Location is the configuration file to use and why.
25+type Location struct {
26+ Path string
27+ Source Source
28+}
29+
30+// Locate picks the configuration file: the --config flag, then the
31+// SIDEKICKS_CONFIG variable, then the file last opened from the application,
32+// then the default location. getenv is os.Getenv outside tests.
33+//
34+// location, err := config.Locate(*configFlag, os.Getenv, "")
35+func Locate(flagValue string, getenv func(string) string, lastOpened string) (Location, error) {
36+ home, _ := os.UserHomeDir()
37+ switch {
38+ case flagValue != "":
39+ return absoluteLocation(flagValue, home, SourceFlag)
40+ case getenv(EnvironmentVariable) != "":
41+ return absoluteLocation(getenv(EnvironmentVariable), home, SourceEnvironment)
42+ case lastOpened != "":
43+ return absoluteLocation(lastOpened, home, SourceLastOpened)
44+ }
45+ path, err := DefaultPath()
46+ return Location{Path: path, Source: SourceDefault}, err
47+}
48+
49+// DefaultPath is <user config dir>/sidekicks/config.yaml, e.g.
50+// ~/Library/Application Support/sidekicks/config.yaml on macOS.
51+func DefaultPath() (string, error) {
52+ directory, err := os.UserConfigDir()
53+ if err != nil {
54+ return "", err
55+ }
56+ return filepath.Join(directory, "sidekicks", "config.yaml"), nil
57+}
58+
59+func absoluteLocation(path, home string, source Source) (Location, error) {
60+ if path == "~" || len(path) > 1 && path[:2] == "~/" {
61+ path = filepath.Join(home, path[1:])
62+ }
63+ absolute, err := filepath.Abs(path)
64+ return Location{Path: absolute, Source: source}, err
65+}
new file mode 100644
@@ -0,0 +1,65 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package config
4+
5+import (
6+ "os"
7+ "path/filepath"
8+)
9+
10+// EnvironmentVariable overrides the default location of the configuration file.
11+const EnvironmentVariable = "SIDEKICKS_CONFIG"
12+
13+// Source tells where the configuration path came from.
14+type Source string
15+
16+// The sources, in order of precedence.
17+const (
18+ SourceFlag Source = "flag"
19+ SourceEnvironment Source = "environment"
20+ SourceLastOpened Source = "last-opened"
21+ SourceDefault Source = "default"
22+)
23+
24+// Location is the configuration file to use and why.
25+type Location struct {
26+ Path string
27+ Source Source
28+}
29+
30+// Locate picks the configuration file: the --config flag, then the
31+// SIDEKICKS_CONFIG variable, then the file last opened from the application,
32+// then the default location. getenv is os.Getenv outside tests.
33+//
34+// location, err := config.Locate(*configFlag, os.Getenv, "")
35+func Locate(flagValue string, getenv func(string) string, lastOpened string) (Location, error) {
36+ home, _ := os.UserHomeDir()
37+ switch {
38+ case flagValue != "":
39+ return absoluteLocation(flagValue, home, SourceFlag)
40+ case getenv(EnvironmentVariable) != "":
41+ return absoluteLocation(getenv(EnvironmentVariable), home, SourceEnvironment)
42+ case lastOpened != "":
43+ return absoluteLocation(lastOpened, home, SourceLastOpened)
44+ }
45+ path, err := DefaultPath()
46+ return Location{Path: path, Source: SourceDefault}, err
47+}
48+
49+// DefaultPath is <user config dir>/sidekicks/config.yaml, e.g.
50+// ~/Library/Application Support/sidekicks/config.yaml on macOS.
51+func DefaultPath() (string, error) {
52+ directory, err := os.UserConfigDir()
53+ if err != nil {
54+ return "", err
55+ }
56+ return filepath.Join(directory, "sidekicks", "config.yaml"), nil
57+}
58+
59+func absoluteLocation(path, home string, source Source) (Location, error) {
60+ if path == "~" || len(path) > 1 && path[:2] == "~/" {
61+ path = filepath.Join(home, path[1:])
62+ }
63+ absolute, err := filepath.Abs(path)
64+ return Location{Path: absolute, Source: source}, err
65+}
added internal/config/problems.go +450 -0
new file mode 100644
@@ -0,0 +1,450 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package config
4+
5+import (
6+ "errors"
7+ "fmt"
8+ "regexp"
9+ "sort"
10+ "strconv"
11+ "strings"
12+
13+ "git.rickub.com/bots-garden/sidekicks/schema"
14+ "github.com/santhosh-tekuri/jsonschema/v6"
15+ "github.com/santhosh-tekuri/jsonschema/v6/kind"
16+ "go.yaml.in/yaml/v3"
17+ textlanguage "golang.org/x/text/language"
18+ textmessage "golang.org/x/text/message"
19+)
20+
21+// Problem is one thing wrong with a configuration file, located and explained
22+// for a person.
23+type Problem struct {
24+ // Line and Column are 1-based; 0 when the problem has no precise place.
25+ Line, Column int
26+ // Path names the key, e.g. "agents[0].name"; empty for the whole file.
27+ Path string
28+ // Message explains the problem and how to fix it, in the interface language.
29+ Message string
30+ // Warning is true when the configuration can still be used.
31+ Warning bool
32+ // Language is the language of Message (fr or en).
33+ Language string
34+}
35+
36+// String formats the problem as "ligne 12 — agents[0].name : message"
37+// (or "line 12 — agents[0].name: message" in English).
38+func (p Problem) String() string {
39+ language := languageOrDefault(p.Language)
40+ separator := map[string]string{"fr": " : ", "en": ": "}[language]
41+ text := p.Message
42+ if p.Path != "" {
43+ text = p.Path + separator + text
44+ }
45+ if p.Line > 0 {
46+ text = fmt.Sprintf(map[string]string{"fr": "ligne %d — ", "en": "line %d — "}[language], p.Line) + text
47+ }
48+ return text
49+}
50+
51+// Error lists the problems that prevent a configuration file from being used.
52+type Error struct {
53+ Path string
54+ Problems []Problem
55+}
56+
57+func newError(path, language string, problems ...Problem) *Error {
58+ return &Error{Path: path, Problems: withLanguage(problems, language)}
59+}
60+
61+func withLanguage(problems []Problem, language string) []Problem {
62+ for i := range problems {
63+ problems[i].Language = language
64+ }
65+ return problems
66+}
67+
68+func (e *Error) Error() string {
69+ lines := make([]string, 0, len(e.Problems))
70+ for _, problem := range e.Problems {
71+ lines = append(lines, problem.String())
72+ }
73+ return fmt.Sprintf("%s: %s", e.Path, strings.Join(lines, "; "))
74+}
75+
76+// Messages, in French and English. Each takes the fmt arguments listed in its comment.
77+const (
78+ msgSyntax = "syntax" // detail
79+ msgUnknownKey = "unknownKey" // key, suggestion ("" when none)
80+ msgMissingKey = "missingKey" // key
81+ msgInvalidName = "invalidName" // value
82+ msgNotOneOf = "notOneOf" // value, choices
83+ msgVersion = "version" // value
84+ msgTooSmall = "tooSmall" // value, minimum
85+ msgDuration = "duration" // value
86+ msgWrongType = "wrongType" // expected type
87+ msgDuplicateItem = "duplicateItem" // value
88+ msgNotAllowedHere = "notAllowedHere" // key
89+ msgDuplicateAgent = "duplicateAgent" // name
90+ msgDuplicateChan = "duplicateChan" // name
91+ msgUnknownMember = "unknownMember" // agent
92+ msgCommandNotFound = "commandNotFound" // command, agent
93+ msgAbsolutePath = "absolutePath" // value
94+ msgTooLong = "tooLong" // maximum
95+ msgEmpty = "empty" // —
96+ msgOther = "other" // detail
97+)
98+
99+var messages = map[string]map[string]string{
100+ "fr": {
101+ msgSyntax: "le fichier n'est pas du YAML valide : %s",
102+ msgUnknownKey: "clé inconnue « %s »%s",
103+ msgMissingKey: "il manque la clé « %s »",
104+ msgInvalidName: "« %s » n'est pas un nom valide : minuscules, chiffres, « - » ou « _ », sans espace, en commençant par une lettre (ex. « alice »)",
105+ msgNotOneOf: "« %v » n'est pas une valeur possible ; choisissez parmi : %s",
106+ msgVersion: "version %v inconnue : seule la version 1 existe",
107+ msgTooSmall: "%v est trop petit ; le minimum est %v",
108+ msgDuration: "« %s » n'est pas une durée valide (ex. 30s, 10m, 1h30m, ou 0)",
109+ msgWrongType: "valeur du mauvais type : il faut %s",
110+ msgDuplicateItem: "« %v » apparaît deux fois",
111+ msgNotAllowedHere: "la clé « %s » n'a pas de sens avec ce type de runtime",
112+ msgDuplicateAgent: "il y a déjà un agent nommé « %s » ; les noms doivent être uniques",
113+ msgDuplicateChan: "il y a déjà un channel nommé « %s » ; les noms doivent être uniques",
114+ msgUnknownMember: "aucun agent ne s'appelle « %s »",
115+ msgCommandNotFound: "la commande « %s » de l'agent « %s » est introuvable ; installez-la ou corrigez le chemin",
116+ msgAbsolutePath: "« %s » doit être un chemin absolu (commençant par /)",
117+ msgTooLong: "texte trop long : %d caractères au maximum",
118+ msgEmpty: "la valeur ne peut pas être vide",
119+ msgOther: "%s",
120+ },
121+ "en": {
122+ msgSyntax: "the file is not valid YAML: %s",
123+ msgUnknownKey: "unknown key \"%s\"%s",
124+ msgMissingKey: "the key \"%s\" is missing",
125+ msgInvalidName: "\"%s\" is not a valid name: lowercase letters, digits, \"-\" or \"_\", no space, starting with a letter (e.g. \"alice\")",
126+ msgNotOneOf: "\"%v\" is not a possible value; choose among: %s",
127+ msgVersion: "unknown version %v: only version 1 exists",
128+ msgTooSmall: "%v is too small; the minimum is %v",
129+ msgDuration: "\"%s\" is not a valid duration (e.g. 30s, 10m, 1h30m, or 0)",
130+ msgWrongType: "value of the wrong type: %s expected",
131+ msgDuplicateItem: "\"%v\" appears twice",
132+ msgNotAllowedHere: "the key \"%s\" makes no sense with this runtime type",
133+ msgDuplicateAgent: "there is already an agent called \"%s\"; names must be unique",
134+ msgDuplicateChan: "there is already a channel called \"%s\"; names must be unique",
135+ msgUnknownMember: "no agent is called \"%s\"",
136+ msgCommandNotFound: "the command \"%s\" of agent \"%s\" cannot be found; install it or fix the path",
137+ msgAbsolutePath: "\"%s\" must be an absolute path (starting with /)",
138+ msgTooLong: "text too long: %d characters at most",
139+ msgEmpty: "the value cannot be empty",
140+ msgOther: "%s",
141+ },
142+}
143+
144+var suggestionFormat = map[string]string{"fr": " ; vouliez-vous dire « %s » ?", "en": "; did you mean \"%s\"?"}
145+
146+func message(language, key string, args ...any) string {
147+ catalog, ok := messages[language]
148+ if !ok {
149+ catalog = messages[DefaultLanguage]
150+ }
151+ return fmt.Sprintf(catalog[key], args...)
152+}
153+
154+// declaredLanguage reads ui.language from the document before it is
155+// validated, so that its own problems are reported in that language.
156+func declaredLanguage(root *yaml.Node) string {
157+ node := lookup(root, []string{"ui", "language"})
158+ if node != nil && messages[node.Value] != nil {
159+ return node.Value
160+ }
161+ return DefaultLanguage
162+}
163+
164+// englishPrinter formats the validator's own messages, for the rare kinds
165+// that have no message of ours.
166+var englishPrinter = textmessage.NewPrinter(textlanguage.English)
167+
168+var yamlLine = regexp.MustCompile(`line (\d+)`)
169+
170+func syntaxProblem(err error, language string) Problem {
171+ detail := strings.TrimPrefix(err.Error(), "yaml: ")
172+ problem := Problem{Message: message(language, msgSyntax, detail)}
173+ if match := yamlLine.FindStringSubmatch(detail); match != nil {
174+ problem.Line, _ = strconv.Atoi(match[1])
175+ }
176+ return problem
177+}
178+
179+// schemaProblems validates content against the JSON Schema and turns every
180+// violation into a located Problem.
181+func schemaProblems(content []byte, root *yaml.Node, language string) []Problem {
182+ err := schema.ValidateConfig(content)
183+ if err == nil {
184+ return nil
185+ }
186+ var validationError *jsonschema.ValidationError
187+ if !errors.As(err, &validationError) {
188+ return []Problem{syntaxProblem(err, language)}
189+ }
190+ var problems []Problem
191+ for _, leaf := range leaves(validationError) {
192+ problems = append(problems, schemaProblem(leaf, root, language))
193+ }
194+ sort.SliceStable(problems, func(i, j int) bool { return problems[i].Line < problems[j].Line })
195+ return dedupe(problems)
196+}
197+
198+func leaves(err *jsonschema.ValidationError) []*jsonschema.ValidationError {
199+ if len(err.Causes) == 0 {
200+ return []*jsonschema.ValidationError{err}
201+ }
202+ var result []*jsonschema.ValidationError
203+ for _, cause := range err.Causes {
204+ result = append(result, leaves(cause)...)
205+ }
206+ return result
207+}
208+
209+func dedupe(problems []Problem) []Problem {
210+ seen := map[string]bool{}
211+ var result []Problem
212+ for _, problem := range problems {
213+ key := problem.String()
214+ if !seen[key] {
215+ seen[key] = true
216+ result = append(result, problem)
217+ }
218+ }
219+ return result
220+}
221+
222+func schemaProblem(err *jsonschema.ValidationError, root *yaml.Node, language string) Problem {
223+ path := err.InstanceLocation
224+ problem := Problem{Path: displayPath(path)}
225+ switch k := err.ErrorKind.(type) {
226+ case *kind.AdditionalProperties:
227+ key := k.Properties[0]
228+ path = append(append([]string(nil), path...), key)
229+ problem.Path = displayPath(path)
230+ problem.Message = message(language, msgUnknownKey, key, suggestion(language, key, allowedKeys(path)))
231+ problem.Line, problem.Column = keyPosition(root, path)
232+ return problem
233+ case *kind.Required:
234+ problem.Message = message(language, msgMissingKey, k.Missing[0])
235+ case *kind.Pattern:
236+ problem.Message = patternMessage(language, path, k.Got)
237+ case *kind.Enum:
238+ choices := make([]string, len(k.Want))
239+ for i, want := range k.Want {
240+ choices[i] = fmt.Sprint(want)
241+ }
242+ problem.Message = message(language, msgNotOneOf, k.Got, strings.Join(choices, ", "))
243+ case *kind.Const:
244+ problem.Message = message(language, msgVersion, k.Got)
245+ case *kind.Minimum:
246+ problem.Message = message(language, msgTooSmall, k.Got.RatString(), k.Want.RatString())
247+ case *kind.Maximum:
248+ // Only stopIdleAgentsAfter has a maximum: a number there can only be 0.
249+ problem.Message = message(language, msgDuration, k.Got.RatString())
250+ case *kind.Type:
251+ problem.Message = message(language, msgWrongType, strings.Join(k.Want, " / "))
252+ case *kind.UniqueItems:
253+ node := lookup(root, append(append([]string(nil), path...), strconv.Itoa(k.Duplicates[1])))
254+ value := ""
255+ if node != nil {
256+ value = node.Value
257+ }
258+ problem.Message = message(language, msgDuplicateItem, value)
259+ case *kind.FalseSchema:
260+ problem.Message = message(language, msgNotAllowedHere, path[len(path)-1])
261+ problem.Line, problem.Column = keyPosition(root, path)
262+ return problem
263+ case *kind.MinLength:
264+ problem.Message = message(language, msgEmpty)
265+ case *kind.MaxLength:
266+ problem.Message = message(language, msgTooLong, k.Want)
267+ default:
268+ problem.Message = message(language, msgOther, err.ErrorKind.LocalizedString(englishPrinter))
269+ }
270+ problem.Line, problem.Column = position(root, path)
271+ return problem
272+}
273+
274+func patternMessage(language string, path []string, value string) string {
275+ switch last := path[len(path)-1]; last {
276+ case "stopIdleAgentsAfter":
277+ return message(language, msgDuration, value)
278+ case "workspacePath":
279+ return message(language, msgAbsolutePath, value)
280+ }
281+ return message(language, msgInvalidName, value)
282+}
283+
284+// allowedKeys lists the keys the schema accepts at the parent of path, to
285+// suggest the closest one for a misspelled key.
286+func allowedKeys(path []string) []string {
287+ parent := path[:len(path)-1]
288+ switch {
289+ case len(parent) == 0:
290+ return []string{"version", "ui", "limits", "agents", "channels"}
291+ case parent[0] == "ui":
292+ return []string{"theme", "language"}
293+ case parent[0] == "limits":
294+ return []string{"maxRunningAgents", "stopIdleAgentsAfter"}
295+ case parent[0] == "agents" && len(parent) == 2:
296+ return []string{"name", "title", "icon", "description", "command", "args", "env", "workspace", "runtime"}
297+ case parent[0] == "agents" && len(parent) == 3 && parent[2] == "runtime":
298+ return []string{"type", "container", "sandbox", "workspacePath"}
299+ case parent[0] == "channels":
300+ return []string{"name", "description", "agents"}
301+ }
302+ return nil
303+}
304+
305+func suggestion(language, key string, allowed []string) string {
306+ best, bestDistance := "", 3
307+ for _, candidate := range allowed {
308+ if distance := levenshtein(strings.ToLower(key), strings.ToLower(candidate)); distance < bestDistance {
309+ best, bestDistance = candidate, distance
310+ }
311+ }
312+ if best == "" {
313+ return ""
314+ }
315+ return fmt.Sprintf(suggestionFormat[languageOrDefault(language)], best)
316+}
317+
318+func languageOrDefault(language string) string {
319+ if _, ok := suggestionFormat[language]; ok {
320+ return language
321+ }
322+ return DefaultLanguage
323+}
324+
325+func levenshtein(a, b string) int {
326+ previous := make([]int, len(b)+1)
327+ for j := range previous {
328+ previous[j] = j
329+ }
330+ for i := 1; i <= len(a); i++ {
331+ current := make([]int, len(b)+1)
332+ current[0] = i
333+ for j := 1; j <= len(b); j++ {
334+ cost := 1
335+ if a[i-1] == b[j-1] {
336+ cost = 0
337+ }
338+ current[j] = min(previous[j]+1, current[j-1]+1, previous[j-1]+cost)
339+ }
340+ previous = current
341+ }
342+ return previous[len(b)]
343+}
344+
345+// crossEntryProblems checks the rules JSON Schema cannot express: unique
346+// agent and channel names, channel members that exist.
347+func crossEntryProblems(raw *rawConfig, root *yaml.Node, language string) []Problem {
348+ var problems []Problem
349+ agentNames := map[string]bool{}
350+ for index, agent := range raw.Agents {
351+ if agentNames[agent.Name] {
352+ problems = append(problems, located(root, []string{"agents", strconv.Itoa(index), "name"}, message(language, msgDuplicateAgent, agent.Name)))
353+ }
354+ agentNames[agent.Name] = true
355+ }
356+ channelNames := map[string]bool{}
357+ for index, channel := range raw.Channels {
358+ if channelNames[channel.Name] {
359+ problems = append(problems, located(root, []string{"channels", strconv.Itoa(index), "name"}, message(language, msgDuplicateChan, channel.Name)))
360+ }
361+ channelNames[channel.Name] = true
362+ for member, agent := range channel.Agents {
363+ if !agentNames[agent] {
364+ problems = append(problems, located(root, []string{"channels", strconv.Itoa(index), "agents", strconv.Itoa(member)}, message(language, msgUnknownMember, agent)))
365+ }
366+ }
367+ }
368+ return problems
369+}
370+
371+func located(root *yaml.Node, path []string, text string) Problem {
372+ line, column := position(root, path)
373+ return Problem{Line: line, Column: column, Path: displayPath(path), Message: text}
374+}
375+
376+// displayPath turns ["agents", "0", "name"] into "agents[0].name".
377+func displayPath(path []string) string {
378+ var builder strings.Builder
379+ for _, part := range path {
380+ if _, err := strconv.Atoi(part); err == nil {
381+ builder.WriteString("[" + part + "]")
382+ continue
383+ }
384+ if builder.Len() > 0 {
385+ builder.WriteString(".")
386+ }
387+ builder.WriteString(part)
388+ }
389+ return builder.String()
390+}
391+
392+// position returns the line and column of the value at path, or of its
393+// closest existing parent when it does not exist (a missing key).
394+func position(root *yaml.Node, path []string) (int, int) {
395+ for length := len(path); length >= 0; length-- {
396+ if node := lookup(root, path[:length]); node != nil {
397+ return node.Line, node.Column
398+ }
399+ }
400+ return 0, 0
401+}
402+
403+// keyPosition returns the position of the key itself (for an unknown key).
404+func keyPosition(root *yaml.Node, path []string) (int, int) {
405+ parent := lookup(root, path[:len(path)-1])
406+ if parent != nil && parent.Kind == yaml.MappingNode {
407+ for i := 0; i+1 < len(parent.Content); i += 2 {
408+ if parent.Content[i].Value == path[len(path)-1] {
409+ return parent.Content[i].Line, parent.Content[i].Column
410+ }
411+ }
412+ }
413+ return position(root, path)
414+}
415+
416+// lookup walks the YAML tree along path (mapping keys and sequence indexes).
417+func lookup(root *yaml.Node, path []string) *yaml.Node {
418+ node := root
419+ if node.Kind == yaml.DocumentNode {
420+ if len(node.Content) == 0 {
421+ return nil
422+ }
423+ node = node.Content[0]
424+ }
425+ for _, part := range path {
426+ switch node.Kind {
427+ case yaml.MappingNode:
428+ next := (*yaml.Node)(nil)
429+ for i := 0; i+1 < len(node.Content); i += 2 {
430+ if node.Content[i].Value == part {
431+ next = node.Content[i+1]
432+ break
433+ }
434+ }
435+ if next == nil {
436+ return nil
437+ }
438+ node = next
439+ case yaml.SequenceNode:
440+ index, err := strconv.Atoi(part)
441+ if err != nil || index < 0 || index >= len(node.Content) {
442+ return nil
443+ }
444+ node = node.Content[index]
445+ default:
446+ return nil
447+ }
448+ }
449+ return node
450+}
new file mode 100644
@@ -0,0 +1,450 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package config
4+
5+import (
6+ "errors"
7+ "fmt"
8+ "regexp"
9+ "sort"
10+ "strconv"
11+ "strings"
12+
13+ "git.rickub.com/bots-garden/sidekicks/schema"
14+ "github.com/santhosh-tekuri/jsonschema/v6"
15+ "github.com/santhosh-tekuri/jsonschema/v6/kind"
16+ "go.yaml.in/yaml/v3"
17+ textlanguage "golang.org/x/text/language"
18+ textmessage "golang.org/x/text/message"
19+)
20+
21+// Problem is one thing wrong with a configuration file, located and explained
22+// for a person.
23+type Problem struct {
24+ // Line and Column are 1-based; 0 when the problem has no precise place.
25+ Line, Column int
26+ // Path names the key, e.g. "agents[0].name"; empty for the whole file.
27+ Path string
28+ // Message explains the problem and how to fix it, in the interface language.
29+ Message string
30+ // Warning is true when the configuration can still be used.
31+ Warning bool
32+ // Language is the language of Message (fr or en).
33+ Language string
34+}
35+
36+// String formats the problem as "ligne 12 — agents[0].name : message"
37+// (or "line 12 — agents[0].name: message" in English).
38+func (p Problem) String() string {
39+ language := languageOrDefault(p.Language)
40+ separator := map[string]string{"fr": " : ", "en": ": "}[language]
41+ text := p.Message
42+ if p.Path != "" {
43+ text = p.Path + separator + text
44+ }
45+ if p.Line > 0 {
46+ text = fmt.Sprintf(map[string]string{"fr": "ligne %d — ", "en": "line %d — "}[language], p.Line) + text
47+ }
48+ return text
49+}
50+
51+// Error lists the problems that prevent a configuration file from being used.
52+type Error struct {
53+ Path string
54+ Problems []Problem
55+}
56+
57+func newError(path, language string, problems ...Problem) *Error {
58+ return &Error{Path: path, Problems: withLanguage(problems, language)}
59+}
60+
61+func withLanguage(problems []Problem, language string) []Problem {
62+ for i := range problems {
63+ problems[i].Language = language
64+ }
65+ return problems
66+}
67+
68+func (e *Error) Error() string {
69+ lines := make([]string, 0, len(e.Problems))
70+ for _, problem := range e.Problems {
71+ lines = append(lines, problem.String())
72+ }
73+ return fmt.Sprintf("%s: %s", e.Path, strings.Join(lines, "; "))
74+}
75+
76+// Messages, in French and English. Each takes the fmt arguments listed in its comment.
77+const (
78+ msgSyntax = "syntax" // detail
79+ msgUnknownKey = "unknownKey" // key, suggestion ("" when none)
80+ msgMissingKey = "missingKey" // key
81+ msgInvalidName = "invalidName" // value
82+ msgNotOneOf = "notOneOf" // value, choices
83+ msgVersion = "version" // value
84+ msgTooSmall = "tooSmall" // value, minimum
85+ msgDuration = "duration" // value
86+ msgWrongType = "wrongType" // expected type
87+ msgDuplicateItem = "duplicateItem" // value
88+ msgNotAllowedHere = "notAllowedHere" // key
89+ msgDuplicateAgent = "duplicateAgent" // name
90+ msgDuplicateChan = "duplicateChan" // name
91+ msgUnknownMember = "unknownMember" // agent
92+ msgCommandNotFound = "commandNotFound" // command, agent
93+ msgAbsolutePath = "absolutePath" // value
94+ msgTooLong = "tooLong" // maximum
95+ msgEmpty = "empty" // —
96+ msgOther = "other" // detail
97+)
98+
99+var messages = map[string]map[string]string{
100+ "fr": {
101+ msgSyntax: "le fichier n'est pas du YAML valide : %s",
102+ msgUnknownKey: "clé inconnue « %s »%s",
103+ msgMissingKey: "il manque la clé « %s »",
104+ msgInvalidName: "« %s » n'est pas un nom valide : minuscules, chiffres, « - » ou « _ », sans espace, en commençant par une lettre (ex. « alice »)",
105+ msgNotOneOf: "« %v » n'est pas une valeur possible ; choisissez parmi : %s",
106+ msgVersion: "version %v inconnue : seule la version 1 existe",
107+ msgTooSmall: "%v est trop petit ; le minimum est %v",
108+ msgDuration: "« %s » n'est pas une durée valide (ex. 30s, 10m, 1h30m, ou 0)",
109+ msgWrongType: "valeur du mauvais type : il faut %s",
110+ msgDuplicateItem: "« %v » apparaît deux fois",
111+ msgNotAllowedHere: "la clé « %s » n'a pas de sens avec ce type de runtime",
112+ msgDuplicateAgent: "il y a déjà un agent nommé « %s » ; les noms doivent être uniques",
113+ msgDuplicateChan: "il y a déjà un channel nommé « %s » ; les noms doivent être uniques",
114+ msgUnknownMember: "aucun agent ne s'appelle « %s »",
115+ msgCommandNotFound: "la commande « %s » de l'agent « %s » est introuvable ; installez-la ou corrigez le chemin",
116+ msgAbsolutePath: "« %s » doit être un chemin absolu (commençant par /)",
117+ msgTooLong: "texte trop long : %d caractères au maximum",
118+ msgEmpty: "la valeur ne peut pas être vide",
119+ msgOther: "%s",
120+ },
121+ "en": {
122+ msgSyntax: "the file is not valid YAML: %s",
123+ msgUnknownKey: "unknown key \"%s\"%s",
124+ msgMissingKey: "the key \"%s\" is missing",
125+ msgInvalidName: "\"%s\" is not a valid name: lowercase letters, digits, \"-\" or \"_\", no space, starting with a letter (e.g. \"alice\")",
126+ msgNotOneOf: "\"%v\" is not a possible value; choose among: %s",
127+ msgVersion: "unknown version %v: only version 1 exists",
128+ msgTooSmall: "%v is too small; the minimum is %v",
129+ msgDuration: "\"%s\" is not a valid duration (e.g. 30s, 10m, 1h30m, or 0)",
130+ msgWrongType: "value of the wrong type: %s expected",
131+ msgDuplicateItem: "\"%v\" appears twice",
132+ msgNotAllowedHere: "the key \"%s\" makes no sense with this runtime type",
133+ msgDuplicateAgent: "there is already an agent called \"%s\"; names must be unique",
134+ msgDuplicateChan: "there is already a channel called \"%s\"; names must be unique",
135+ msgUnknownMember: "no agent is called \"%s\"",
136+ msgCommandNotFound: "the command \"%s\" of agent \"%s\" cannot be found; install it or fix the path",
137+ msgAbsolutePath: "\"%s\" must be an absolute path (starting with /)",
138+ msgTooLong: "text too long: %d characters at most",
139+ msgEmpty: "the value cannot be empty",
140+ msgOther: "%s",
141+ },
142+}
143+
144+var suggestionFormat = map[string]string{"fr": " ; vouliez-vous dire « %s » ?", "en": "; did you mean \"%s\"?"}
145+
146+func message(language, key string, args ...any) string {
147+ catalog, ok := messages[language]
148+ if !ok {
149+ catalog = messages[DefaultLanguage]
150+ }
151+ return fmt.Sprintf(catalog[key], args...)
152+}
153+
154+// declaredLanguage reads ui.language from the document before it is
155+// validated, so that its own problems are reported in that language.
156+func declaredLanguage(root *yaml.Node) string {
157+ node := lookup(root, []string{"ui", "language"})
158+ if node != nil && messages[node.Value] != nil {
159+ return node.Value
160+ }
161+ return DefaultLanguage
162+}
163+
164+// englishPrinter formats the validator's own messages, for the rare kinds
165+// that have no message of ours.
166+var englishPrinter = textmessage.NewPrinter(textlanguage.English)
167+
168+var yamlLine = regexp.MustCompile(`line (\d+)`)
169+
170+func syntaxProblem(err error, language string) Problem {
171+ detail := strings.TrimPrefix(err.Error(), "yaml: ")
172+ problem := Problem{Message: message(language, msgSyntax, detail)}
173+ if match := yamlLine.FindStringSubmatch(detail); match != nil {
174+ problem.Line, _ = strconv.Atoi(match[1])
175+ }
176+ return problem
177+}
178+
179+// schemaProblems validates content against the JSON Schema and turns every
180+// violation into a located Problem.
181+func schemaProblems(content []byte, root *yaml.Node, language string) []Problem {
182+ err := schema.ValidateConfig(content)
183+ if err == nil {
184+ return nil
185+ }
186+ var validationError *jsonschema.ValidationError
187+ if !errors.As(err, &validationError) {
188+ return []Problem{syntaxProblem(err, language)}
189+ }
190+ var problems []Problem
191+ for _, leaf := range leaves(validationError) {
192+ problems = append(problems, schemaProblem(leaf, root, language))
193+ }
194+ sort.SliceStable(problems, func(i, j int) bool { return problems[i].Line < problems[j].Line })
195+ return dedupe(problems)
196+}
197+
198+func leaves(err *jsonschema.ValidationError) []*jsonschema.ValidationError {
199+ if len(err.Causes) == 0 {
200+ return []*jsonschema.ValidationError{err}
201+ }
202+ var result []*jsonschema.ValidationError
203+ for _, cause := range err.Causes {
204+ result = append(result, leaves(cause)...)
205+ }
206+ return result
207+}
208+
209+func dedupe(problems []Problem) []Problem {
210+ seen := map[string]bool{}
211+ var result []Problem
212+ for _, problem := range problems {
213+ key := problem.String()
214+ if !seen[key] {
215+ seen[key] = true
216+ result = append(result, problem)
217+ }
218+ }
219+ return result
220+}
221+
222+func schemaProblem(err *jsonschema.ValidationError, root *yaml.Node, language string) Problem {
223+ path := err.InstanceLocation
224+ problem := Problem{Path: displayPath(path)}
225+ switch k := err.ErrorKind.(type) {
226+ case *kind.AdditionalProperties:
227+ key := k.Properties[0]
228+ path = append(append([]string(nil), path...), key)
229+ problem.Path = displayPath(path)
230+ problem.Message = message(language, msgUnknownKey, key, suggestion(language, key, allowedKeys(path)))
231+ problem.Line, problem.Column = keyPosition(root, path)
232+ return problem
233+ case *kind.Required:
234+ problem.Message = message(language, msgMissingKey, k.Missing[0])
235+ case *kind.Pattern:
236+ problem.Message = patternMessage(language, path, k.Got)
237+ case *kind.Enum:
238+ choices := make([]string, len(k.Want))
239+ for i, want := range k.Want {
240+ choices[i] = fmt.Sprint(want)
241+ }
242+ problem.Message = message(language, msgNotOneOf, k.Got, strings.Join(choices, ", "))
243+ case *kind.Const:
244+ problem.Message = message(language, msgVersion, k.Got)
245+ case *kind.Minimum:
246+ problem.Message = message(language, msgTooSmall, k.Got.RatString(), k.Want.RatString())
247+ case *kind.Maximum:
248+ // Only stopIdleAgentsAfter has a maximum: a number there can only be 0.
249+ problem.Message = message(language, msgDuration, k.Got.RatString())
250+ case *kind.Type:
251+ problem.Message = message(language, msgWrongType, strings.Join(k.Want, " / "))
252+ case *kind.UniqueItems:
253+ node := lookup(root, append(append([]string(nil), path...), strconv.Itoa(k.Duplicates[1])))
254+ value := ""
255+ if node != nil {
256+ value = node.Value
257+ }
258+ problem.Message = message(language, msgDuplicateItem, value)
259+ case *kind.FalseSchema:
260+ problem.Message = message(language, msgNotAllowedHere, path[len(path)-1])
261+ problem.Line, problem.Column = keyPosition(root, path)
262+ return problem
263+ case *kind.MinLength:
264+ problem.Message = message(language, msgEmpty)
265+ case *kind.MaxLength:
266+ problem.Message = message(language, msgTooLong, k.Want)
267+ default:
268+ problem.Message = message(language, msgOther, err.ErrorKind.LocalizedString(englishPrinter))
269+ }
270+ problem.Line, problem.Column = position(root, path)
271+ return problem
272+}
273+
274+func patternMessage(language string, path []string, value string) string {
275+ switch last := path[len(path)-1]; last {
276+ case "stopIdleAgentsAfter":
277+ return message(language, msgDuration, value)
278+ case "workspacePath":
279+ return message(language, msgAbsolutePath, value)
280+ }
281+ return message(language, msgInvalidName, value)
282+}
283+
284+// allowedKeys lists the keys the schema accepts at the parent of path, to
285+// suggest the closest one for a misspelled key.
286+func allowedKeys(path []string) []string {
287+ parent := path[:len(path)-1]
288+ switch {
289+ case len(parent) == 0:
290+ return []string{"version", "ui", "limits", "agents", "channels"}
291+ case parent[0] == "ui":
292+ return []string{"theme", "language"}
293+ case parent[0] == "limits":
294+ return []string{"maxRunningAgents", "stopIdleAgentsAfter"}
295+ case parent[0] == "agents" && len(parent) == 2:
296+ return []string{"name", "title", "icon", "description", "command", "args", "env", "workspace", "runtime"}
297+ case parent[0] == "agents" && len(parent) == 3 && parent[2] == "runtime":
298+ return []string{"type", "container", "sandbox", "workspacePath"}
299+ case parent[0] == "channels":
300+ return []string{"name", "description", "agents"}
301+ }
302+ return nil
303+}
304+
305+func suggestion(language, key string, allowed []string) string {
306+ best, bestDistance := "", 3
307+ for _, candidate := range allowed {
308+ if distance := levenshtein(strings.ToLower(key), strings.ToLower(candidate)); distance < bestDistance {
309+ best, bestDistance = candidate, distance
310+ }
311+ }
312+ if best == "" {
313+ return ""
314+ }
315+ return fmt.Sprintf(suggestionFormat[languageOrDefault(language)], best)
316+}
317+
318+func languageOrDefault(language string) string {
319+ if _, ok := suggestionFormat[language]; ok {
320+ return language
321+ }
322+ return DefaultLanguage
323+}
324+
325+func levenshtein(a, b string) int {
326+ previous := make([]int, len(b)+1)
327+ for j := range previous {
328+ previous[j] = j
329+ }
330+ for i := 1; i <= len(a); i++ {
331+ current := make([]int, len(b)+1)
332+ current[0] = i
333+ for j := 1; j <= len(b); j++ {
334+ cost := 1
335+ if a[i-1] == b[j-1] {
336+ cost = 0
337+ }
338+ current[j] = min(previous[j]+1, current[j-1]+1, previous[j-1]+cost)
339+ }
340+ previous = current
341+ }
342+ return previous[len(b)]
343+}
344+
345+// crossEntryProblems checks the rules JSON Schema cannot express: unique
346+// agent and channel names, channel members that exist.
347+func crossEntryProblems(raw *rawConfig, root *yaml.Node, language string) []Problem {
348+ var problems []Problem
349+ agentNames := map[string]bool{}
350+ for index, agent := range raw.Agents {
351+ if agentNames[agent.Name] {
352+ problems = append(problems, located(root, []string{"agents", strconv.Itoa(index), "name"}, message(language, msgDuplicateAgent, agent.Name)))
353+ }
354+ agentNames[agent.Name] = true
355+ }
356+ channelNames := map[string]bool{}
357+ for index, channel := range raw.Channels {
358+ if channelNames[channel.Name] {
359+ problems = append(problems, located(root, []string{"channels", strconv.Itoa(index), "name"}, message(language, msgDuplicateChan, channel.Name)))
360+ }
361+ channelNames[channel.Name] = true
362+ for member, agent := range channel.Agents {
363+ if !agentNames[agent] {
364+ problems = append(problems, located(root, []string{"channels", strconv.Itoa(index), "agents", strconv.Itoa(member)}, message(language, msgUnknownMember, agent)))
365+ }
366+ }
367+ }
368+ return problems
369+}
370+
371+func located(root *yaml.Node, path []string, text string) Problem {
372+ line, column := position(root, path)
373+ return Problem{Line: line, Column: column, Path: displayPath(path), Message: text}
374+}
375+
376+// displayPath turns ["agents", "0", "name"] into "agents[0].name".
377+func displayPath(path []string) string {
378+ var builder strings.Builder
379+ for _, part := range path {
380+ if _, err := strconv.Atoi(part); err == nil {
381+ builder.WriteString("[" + part + "]")
382+ continue
383+ }
384+ if builder.Len() > 0 {
385+ builder.WriteString(".")
386+ }
387+ builder.WriteString(part)
388+ }
389+ return builder.String()
390+}
391+
392+// position returns the line and column of the value at path, or of its
393+// closest existing parent when it does not exist (a missing key).
394+func position(root *yaml.Node, path []string) (int, int) {
395+ for length := len(path); length >= 0; length-- {
396+ if node := lookup(root, path[:length]); node != nil {
397+ return node.Line, node.Column
398+ }
399+ }
400+ return 0, 0
401+}
402+
403+// keyPosition returns the position of the key itself (for an unknown key).
404+func keyPosition(root *yaml.Node, path []string) (int, int) {
405+ parent := lookup(root, path[:len(path)-1])
406+ if parent != nil && parent.Kind == yaml.MappingNode {
407+ for i := 0; i+1 < len(parent.Content); i += 2 {
408+ if parent.Content[i].Value == path[len(path)-1] {
409+ return parent.Content[i].Line, parent.Content[i].Column
410+ }
411+ }
412+ }
413+ return position(root, path)
414+}
415+
416+// lookup walks the YAML tree along path (mapping keys and sequence indexes).
417+func lookup(root *yaml.Node, path []string) *yaml.Node {
418+ node := root
419+ if node.Kind == yaml.DocumentNode {
420+ if len(node.Content) == 0 {
421+ return nil
422+ }
423+ node = node.Content[0]
424+ }
425+ for _, part := range path {
426+ switch node.Kind {
427+ case yaml.MappingNode:
428+ next := (*yaml.Node)(nil)
429+ for i := 0; i+1 < len(node.Content); i += 2 {
430+ if node.Content[i].Value == part {
431+ next = node.Content[i+1]
432+ break
433+ }
434+ }
435+ if next == nil {
436+ return nil
437+ }
438+ node = next
439+ case yaml.SequenceNode:
440+ index, err := strconv.Atoi(part)
441+ if err != nil || index < 0 || index >= len(node.Content) {
442+ return nil
443+ }
444+ node = node.Content[index]
445+ default:
446+ return nil
447+ }
448+ }
449+ return node
450+}
added internal/config/save.go +86 -0
new file mode 100644
@@ -0,0 +1,86 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package config
4+
5+import (
6+ "bytes"
7+ "errors"
8+ "fmt"
9+ "path/filepath"
10+
11+ "go.yaml.in/yaml/v3"
12+)
13+
14+// Save validates content and, only if it is valid, writes it to path
15+// atomically. This is what the Settings editor uses: an invalid file is never
16+// written, and the user keeps their text and its comments exactly as typed.
17+//
18+// if err := config.Save(path, editorContent); err != nil { … show the problems … }
19+func Save(path string, content []byte) error {
20+ absolute, err := filepath.Abs(path)
21+ if err != nil {
22+ return err
23+ }
24+ if _, err := Parse(content, absolute); err != nil {
25+ return err
26+ }
27+ return writeFileAtomically(absolute, content)
28+}
29+
30+// AgentEntry is an agent to add to a file with AppendAgent. Empty optional
31+// fields are left out of the file.
32+type AgentEntry struct {
33+ Name string `yaml:"name"`
34+ Title string `yaml:"title,omitempty"`
35+ Icon string `yaml:"icon,omitempty"`
36+ Description string `yaml:"description,omitempty"`
37+ Command string `yaml:"command"`
38+ Args []string `yaml:"args,omitempty,flow"`
39+ Env map[string]string `yaml:"env,omitempty"`
40+ Workspace string `yaml:"workspace,omitempty"`
41+}
42+
43+// AppendAgent returns content with entry added at the end of the agents list.
44+// The rest of the document — comments included — is kept. The result is
45+// validated, so a duplicate name is reported like any other problem.
46+//
47+// updated, err := config.AppendAgent(content, config.AgentEntry{Name: "bob", Command: "mm"})
48+func AppendAgent(content []byte, entry AgentEntry) ([]byte, error) {
49+ var document yaml.Node
50+ if err := yaml.Unmarshal(content, &document); err != nil {
51+ return nil, err
52+ }
53+ if document.Kind != yaml.DocumentNode || len(document.Content) == 0 || document.Content[0].Kind != yaml.MappingNode {
54+ return nil, errors.New("the configuration is not a YAML mapping")
55+ }
56+ agents := lookup(&document, []string{"agents"})
57+ if agents == nil {
58+ mapping := document.Content[0]
59+ agents = &yaml.Node{Kind: yaml.SequenceNode}
60+ mapping.Content = append(mapping.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "agents"}, agents)
61+ }
62+ if agents.Kind != yaml.SequenceNode {
63+ return nil, errors.New("agents is not a list")
64+ }
65+ var entryNode yaml.Node
66+ if err := entryNode.Encode(entry); err != nil {
67+ return nil, err
68+ }
69+ // "agents: []" is a flow sequence; a block list reads better once it has entries.
70+ agents.Style = 0
71+ agents.Content = append(agents.Content, &entryNode)
72+
73+ var buffer bytes.Buffer
74+ encoder := yaml.NewEncoder(&buffer)
75+ encoder.SetIndent(2)
76+ if err := encoder.Encode(&document); err != nil {
77+ return nil, err
78+ }
79+ if err := encoder.Close(); err != nil {
80+ return nil, err
81+ }
82+ if _, err := Parse(buffer.Bytes(), "/"); err != nil {
83+ return nil, fmt.Errorf("the agent cannot be added: %w", err)
84+ }
85+ return buffer.Bytes(), nil
86+}
new file mode 100644
@@ -0,0 +1,86 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package config
4+
5+import (
6+ "bytes"
7+ "errors"
8+ "fmt"
9+ "path/filepath"
10+
11+ "go.yaml.in/yaml/v3"
12+)
13+
14+// Save validates content and, only if it is valid, writes it to path
15+// atomically. This is what the Settings editor uses: an invalid file is never
16+// written, and the user keeps their text and its comments exactly as typed.
17+//
18+// if err := config.Save(path, editorContent); err != nil { … show the problems … }
19+func Save(path string, content []byte) error {
20+ absolute, err := filepath.Abs(path)
21+ if err != nil {
22+ return err
23+ }
24+ if _, err := Parse(content, absolute); err != nil {
25+ return err
26+ }
27+ return writeFileAtomically(absolute, content)
28+}
29+
30+// AgentEntry is an agent to add to a file with AppendAgent. Empty optional
31+// fields are left out of the file.
32+type AgentEntry struct {
33+ Name string `yaml:"name"`
34+ Title string `yaml:"title,omitempty"`
35+ Icon string `yaml:"icon,omitempty"`
36+ Description string `yaml:"description,omitempty"`
37+ Command string `yaml:"command"`
38+ Args []string `yaml:"args,omitempty,flow"`
39+ Env map[string]string `yaml:"env,omitempty"`
40+ Workspace string `yaml:"workspace,omitempty"`
41+}
42+
43+// AppendAgent returns content with entry added at the end of the agents list.
44+// The rest of the document — comments included — is kept. The result is
45+// validated, so a duplicate name is reported like any other problem.
46+//
47+// updated, err := config.AppendAgent(content, config.AgentEntry{Name: "bob", Command: "mm"})
48+func AppendAgent(content []byte, entry AgentEntry) ([]byte, error) {
49+ var document yaml.Node
50+ if err := yaml.Unmarshal(content, &document); err != nil {
51+ return nil, err
52+ }
53+ if document.Kind != yaml.DocumentNode || len(document.Content) == 0 || document.Content[0].Kind != yaml.MappingNode {
54+ return nil, errors.New("the configuration is not a YAML mapping")
55+ }
56+ agents := lookup(&document, []string{"agents"})
57+ if agents == nil {
58+ mapping := document.Content[0]
59+ agents = &yaml.Node{Kind: yaml.SequenceNode}
60+ mapping.Content = append(mapping.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "agents"}, agents)
61+ }
62+ if agents.Kind != yaml.SequenceNode {
63+ return nil, errors.New("agents is not a list")
64+ }
65+ var entryNode yaml.Node
66+ if err := entryNode.Encode(entry); err != nil {
67+ return nil, err
68+ }
69+ // "agents: []" is a flow sequence; a block list reads better once it has entries.
70+ agents.Style = 0
71+ agents.Content = append(agents.Content, &entryNode)
72+
73+ var buffer bytes.Buffer
74+ encoder := yaml.NewEncoder(&buffer)
75+ encoder.SetIndent(2)
76+ if err := encoder.Encode(&document); err != nil {
77+ return nil, err
78+ }
79+ if err := encoder.Close(); err != nil {
80+ return nil, err
81+ }
82+ if _, err := Parse(buffer.Bytes(), "/"); err != nil {
83+ return nil, fmt.Errorf("the agent cannot be added: %w", err)
84+ }
85+ return buffer.Bytes(), nil
86+}
added internal/config/testconfig_test.go +66 -0
new file mode 100644
@@ -0,0 +1,66 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package config_test
4+
5+import (
6+ "os"
7+ "os/exec"
8+ "path/filepath"
9+ "strings"
10+ "testing"
11+
12+ "git.rickub.com/bots-garden/sidekicks/internal/config"
13+)
14+
15+// scripts/install-test-config.sh writes the test agents' configuration at the
16+// default location; what it writes must load, with absolute paths and the
17+// environment needed outside a terminal.
18+func TestInstallTestConfigWritesAValidConfiguration(t *testing.T) {
19+ root, err := filepath.Abs("../..")
20+ if err != nil {
21+ t.Fatal(err)
22+ }
23+ target := filepath.Join(t.TempDir(), "sidekicks", "config.yaml")
24+ if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
25+ t.Fatal(err)
26+ }
27+ if err := os.WriteFile(target, []byte("version: 1\nagents: []\n"), 0o644); err != nil {
28+ t.Fatal(err)
29+ }
30+
31+ script := exec.Command(filepath.Join(root, "scripts", "install-test-config.sh"), target)
32+ script.Env = append(os.Environ(), "MM_BIN=/opt/test/mm")
33+ if output, err := script.CombinedOutput(); err != nil {
34+ t.Fatalf("%v\n%s", err, output)
35+ }
36+
37+ cfg, err := config.Load(target)
38+ if err != nil {
39+ t.Fatal(err)
40+ }
41+ if len(cfg.Agents) != 4 || len(cfg.Channels) != 2 {
42+ t.Fatalf("got %d agents and %d channels", len(cfg.Agents), len(cfg.Channels))
43+ }
44+ for _, agent := range cfg.Agents {
45+ if agent.Command != filepath.Join(root, "scripts", "agents.sh") {
46+ t.Errorf("%s: command %q", agent.Name, agent.Command)
47+ }
48+ if agent.Workspace != filepath.Join(root, "agents", agent.Name) {
49+ t.Errorf("%s: workspace %q", agent.Name, agent.Workspace)
50+ }
51+ if agent.Env["MM_BIN"] != "/opt/test/mm" || agent.Env["PATH"] != os.Getenv("PATH") {
52+ t.Errorf("%s: env %v", agent.Name, agent.Env)
53+ }
54+ }
55+ if len(cfg.Warnings) != 0 {
56+ t.Errorf("unexpected warnings: %v", cfg.Warnings)
57+ }
58+
59+ backups, _ := filepath.Glob(target + ".*.bak")
60+ if len(backups) != 1 {
61+ t.Fatalf("the previous file should be kept as a backup, found %v", backups)
62+ }
63+ if content, _ := os.ReadFile(backups[0]); !strings.Contains(string(content), "agents: []") {
64+ t.Fatal("the backup does not hold the previous file")
65+ }
66+}
new file mode 100644
@@ -0,0 +1,66 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package config_test
4+
5+import (
6+ "os"
7+ "os/exec"
8+ "path/filepath"
9+ "strings"
10+ "testing"
11+
12+ "git.rickub.com/bots-garden/sidekicks/internal/config"
13+)
14+
15+// scripts/install-test-config.sh writes the test agents' configuration at the
16+// default location; what it writes must load, with absolute paths and the
17+// environment needed outside a terminal.
18+func TestInstallTestConfigWritesAValidConfiguration(t *testing.T) {
19+ root, err := filepath.Abs("../..")
20+ if err != nil {
21+ t.Fatal(err)
22+ }
23+ target := filepath.Join(t.TempDir(), "sidekicks", "config.yaml")
24+ if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
25+ t.Fatal(err)
26+ }
27+ if err := os.WriteFile(target, []byte("version: 1\nagents: []\n"), 0o644); err != nil {
28+ t.Fatal(err)
29+ }
30+
31+ script := exec.Command(filepath.Join(root, "scripts", "install-test-config.sh"), target)
32+ script.Env = append(os.Environ(), "MM_BIN=/opt/test/mm")
33+ if output, err := script.CombinedOutput(); err != nil {
34+ t.Fatalf("%v\n%s", err, output)
35+ }
36+
37+ cfg, err := config.Load(target)
38+ if err != nil {
39+ t.Fatal(err)
40+ }
41+ if len(cfg.Agents) != 4 || len(cfg.Channels) != 2 {
42+ t.Fatalf("got %d agents and %d channels", len(cfg.Agents), len(cfg.Channels))
43+ }
44+ for _, agent := range cfg.Agents {
45+ if agent.Command != filepath.Join(root, "scripts", "agents.sh") {
46+ t.Errorf("%s: command %q", agent.Name, agent.Command)
47+ }
48+ if agent.Workspace != filepath.Join(root, "agents", agent.Name) {
49+ t.Errorf("%s: workspace %q", agent.Name, agent.Workspace)
50+ }
51+ if agent.Env["MM_BIN"] != "/opt/test/mm" || agent.Env["PATH"] != os.Getenv("PATH") {
52+ t.Errorf("%s: env %v", agent.Name, agent.Env)
53+ }
54+ }
55+ if len(cfg.Warnings) != 0 {
56+ t.Errorf("unexpected warnings: %v", cfg.Warnings)
57+ }
58+
59+ backups, _ := filepath.Glob(target + ".*.bak")
60+ if len(backups) != 1 {
61+ t.Fatalf("the previous file should be kept as a backup, found %v", backups)
62+ }
63+ if content, _ := os.ReadFile(backups[0]); !strings.Contains(string(content), "agents: []") {
64+ t.Fatal("the backup does not hold the previous file")
65+ }
66+}
added internal/config/watch.go +65 -0
new file mode 100644
@@ -0,0 +1,65 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package config
4+
5+import (
6+ "context"
7+ "path/filepath"
8+ "time"
9+
10+ "github.com/fsnotify/fsnotify"
11+)
12+
13+// WatchDelay groups the bursts of file events an editor produces when it
14+// saves (write, rename, chmod…) into one reload.
15+const WatchDelay = 200 * time.Millisecond
16+
17+// Watch calls onChange with the reloaded configuration (or the problems that
18+// prevent loading it) each time the file at path changes, until ctx is done.
19+//
20+// The folder is watched rather than the file: editors often save by writing a
21+// new file and renaming it, which a watch on the file itself would miss.
22+//
23+// go config.Watch(ctx, path, func(cfg *config.Config, err error) { … })
24+func Watch(ctx context.Context, path string, onChange func(*Config, error)) error {
25+ absolute, err := filepath.Abs(path)
26+ if err != nil {
27+ return err
28+ }
29+ watcher, err := fsnotify.NewWatcher()
30+ if err != nil {
31+ return err
32+ }
33+ defer func() { _ = watcher.Close() }()
34+ if err := watcher.Add(filepath.Dir(absolute)); err != nil {
35+ return err
36+ }
37+
38+ var timer *time.Timer
39+ reload := func() { onChange(Load(absolute)) }
40+ for {
41+ select {
42+ case <-ctx.Done():
43+ if timer != nil {
44+ timer.Stop()
45+ }
46+ return nil
47+ case event, ok := <-watcher.Events:
48+ if !ok {
49+ return nil
50+ }
51+ if filepath.Clean(event.Name) != absolute || event.Op == fsnotify.Chmod {
52+ continue
53+ }
54+ if timer != nil {
55+ timer.Stop()
56+ }
57+ timer = time.AfterFunc(WatchDelay, reload)
58+ case err, ok := <-watcher.Errors:
59+ if !ok {
60+ return nil
61+ }
62+ onChange(nil, err)
63+ }
64+ }
65+}
new file mode 100644
@@ -0,0 +1,65 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package config
4+
5+import (
6+ "context"
7+ "path/filepath"
8+ "time"
9+
10+ "github.com/fsnotify/fsnotify"
11+)
12+
13+// WatchDelay groups the bursts of file events an editor produces when it
14+// saves (write, rename, chmod…) into one reload.
15+const WatchDelay = 200 * time.Millisecond
16+
17+// Watch calls onChange with the reloaded configuration (or the problems that
18+// prevent loading it) each time the file at path changes, until ctx is done.
19+//
20+// The folder is watched rather than the file: editors often save by writing a
21+// new file and renaming it, which a watch on the file itself would miss.
22+//
23+// go config.Watch(ctx, path, func(cfg *config.Config, err error) { … })
24+func Watch(ctx context.Context, path string, onChange func(*Config, error)) error {
25+ absolute, err := filepath.Abs(path)
26+ if err != nil {
27+ return err
28+ }
29+ watcher, err := fsnotify.NewWatcher()
30+ if err != nil {
31+ return err
32+ }
33+ defer func() { _ = watcher.Close() }()
34+ if err := watcher.Add(filepath.Dir(absolute)); err != nil {
35+ return err
36+ }
37+
38+ var timer *time.Timer
39+ reload := func() { onChange(Load(absolute)) }
40+ for {
41+ select {
42+ case <-ctx.Done():
43+ if timer != nil {
44+ timer.Stop()
45+ }
46+ return nil
47+ case event, ok := <-watcher.Events:
48+ if !ok {
49+ return nil
50+ }
51+ if filepath.Clean(event.Name) != absolute || event.Op == fsnotify.Chmod {
52+ continue
53+ }
54+ if timer != nil {
55+ timer.Stop()
56+ }
57+ timer = time.AfterFunc(WatchDelay, reload)
58+ case err, ok := <-watcher.Errors:
59+ if !ok {
60+ return nil
61+ }
62+ onChange(nil, err)
63+ }
64+ }
65+}
added internal/events/events.go +110 -0
new file mode 100644
@@ -0,0 +1,110 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// Package events defines the events Sidekicks sends from Go to the interface
4+// (ADR 0003, "Events from Go to the UI"): their names, their payloads, and the
5+// Emitter the domain packages send them through.
6+//
7+// The domain packages depend on the Emitter interface only, never on Wails:
8+// main.go plugs the Wails event manager in, and tests plug a Recorder in.
9+//
10+// emitter.Emit(events.AgentState, events.AgentStatePayload{Agent: "alice", State: "ready"})
11+package events
12+
13+import "sync"
14+
15+// Event names. Each one is registered with Wails in main.go together with its
16+// payload type, which generates the matching TypeScript constant and type.
17+const (
18+ // ConfigChanged: the configuration file was (re)loaded. Payload: ConfigChangedPayload.
19+ ConfigChanged = "config:changed"
20+ // AgentState: an agent changed state. Payload: AgentStatePayload.
21+ AgentState = "agent:state"
22+ // AgentLog: an agent wrote a line on its standard error. Payload: AgentLogPayload.
23+ AgentLog = "agent:log"
24+)
25+
26+// ConfigChangedPayload tells the interface which configuration is loaded and
27+// what is wrong with it, if anything.
28+type ConfigChangedPayload struct {
29+ // Path is the absolute path of the configuration file.
30+ Path string `json:"path"`
31+ // Valid is false when the file could not be loaded; the previous
32+ // configuration then stays in use.
33+ Valid bool `json:"valid"`
34+ // Problems are human-readable messages, one per problem, in the interface language.
35+ Problems []string `json:"problems"`
36+}
37+
38+// AgentStatePayload is one state transition of an agent.
39+type AgentStatePayload struct {
40+ Agent string `json:"agent"`
41+ // State is one of stopped, starting, ready, busy, error.
42+ State string `json:"state"`
43+ // Error explains the error state; empty otherwise.
44+ Error string `json:"error,omitempty"`
45+}
46+
47+// AgentLogPayload is one line written by an agent on its standard error.
48+type AgentLogPayload struct {
49+ Agent string `json:"agent"`
50+ Line string `json:"line"`
51+}
52+
53+// Emitter sends an event to the interface. Implementations must be safe for
54+// concurrent use: agents report from their own goroutines.
55+type Emitter interface {
56+ Emit(name string, payload any)
57+}
58+
59+// EmitterFunc adapts a function to the Emitter interface.
60+//
61+// emitter := events.EmitterFunc(func(name string, payload any) { log.Println(name, payload) })
62+type EmitterFunc func(name string, payload any)
63+
64+// Emit calls f(name, payload).
65+func (f EmitterFunc) Emit(name string, payload any) { f(name, payload) }
66+
67+// Discard is an Emitter that drops every event.
68+var Discard Emitter = EmitterFunc(func(string, any) {})
69+
70+// Recorded is one event captured by a Recorder.
71+type Recorded struct {
72+ Name string
73+ Payload any
74+}
75+
76+// Recorder is an Emitter that keeps every event, for tests.
77+//
78+// recorder := &events.Recorder{}
79+// manager := agents.NewManager(…, recorder)
80+// …
81+// recorder.Named(events.AgentState)
82+type Recorder struct {
83+ mu sync.Mutex
84+ events []Recorded
85+}
86+
87+// Emit records the event.
88+func (r *Recorder) Emit(name string, payload any) {
89+ r.mu.Lock()
90+ defer r.mu.Unlock()
91+ r.events = append(r.events, Recorded{Name: name, Payload: payload})
92+}
93+
94+// All returns a copy of the recorded events, oldest first.
95+func (r *Recorder) All() []Recorded {
96+ r.mu.Lock()
97+ defer r.mu.Unlock()
98+ return append([]Recorded(nil), r.events...)
99+}
100+
101+// Named returns the payloads of the events called name, oldest first.
102+func (r *Recorder) Named(name string) []any {
103+ var payloads []any
104+ for _, event := range r.All() {
105+ if event.Name == name {
106+ payloads = append(payloads, event.Payload)
107+ }
108+ }
109+ return payloads
110+}
new file mode 100644
@@ -0,0 +1,110 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// Package events defines the events Sidekicks sends from Go to the interface
4+// (ADR 0003, "Events from Go to the UI"): their names, their payloads, and the
5+// Emitter the domain packages send them through.
6+//
7+// The domain packages depend on the Emitter interface only, never on Wails:
8+// main.go plugs the Wails event manager in, and tests plug a Recorder in.
9+//
10+// emitter.Emit(events.AgentState, events.AgentStatePayload{Agent: "alice", State: "ready"})
11+package events
12+
13+import "sync"
14+
15+// Event names. Each one is registered with Wails in main.go together with its
16+// payload type, which generates the matching TypeScript constant and type.
17+const (
18+ // ConfigChanged: the configuration file was (re)loaded. Payload: ConfigChangedPayload.
19+ ConfigChanged = "config:changed"
20+ // AgentState: an agent changed state. Payload: AgentStatePayload.
21+ AgentState = "agent:state"
22+ // AgentLog: an agent wrote a line on its standard error. Payload: AgentLogPayload.
23+ AgentLog = "agent:log"
24+)
25+
26+// ConfigChangedPayload tells the interface which configuration is loaded and
27+// what is wrong with it, if anything.
28+type ConfigChangedPayload struct {
29+ // Path is the absolute path of the configuration file.
30+ Path string `json:"path"`
31+ // Valid is false when the file could not be loaded; the previous
32+ // configuration then stays in use.
33+ Valid bool `json:"valid"`
34+ // Problems are human-readable messages, one per problem, in the interface language.
35+ Problems []string `json:"problems"`
36+}
37+
38+// AgentStatePayload is one state transition of an agent.
39+type AgentStatePayload struct {
40+ Agent string `json:"agent"`
41+ // State is one of stopped, starting, ready, busy, error.
42+ State string `json:"state"`
43+ // Error explains the error state; empty otherwise.
44+ Error string `json:"error,omitempty"`
45+}
46+
47+// AgentLogPayload is one line written by an agent on its standard error.
48+type AgentLogPayload struct {
49+ Agent string `json:"agent"`
50+ Line string `json:"line"`
51+}
52+
53+// Emitter sends an event to the interface. Implementations must be safe for
54+// concurrent use: agents report from their own goroutines.
55+type Emitter interface {
56+ Emit(name string, payload any)
57+}
58+
59+// EmitterFunc adapts a function to the Emitter interface.
60+//
61+// emitter := events.EmitterFunc(func(name string, payload any) { log.Println(name, payload) })
62+type EmitterFunc func(name string, payload any)
63+
64+// Emit calls f(name, payload).
65+func (f EmitterFunc) Emit(name string, payload any) { f(name, payload) }
66+
67+// Discard is an Emitter that drops every event.
68+var Discard Emitter = EmitterFunc(func(string, any) {})
69+
70+// Recorded is one event captured by a Recorder.
71+type Recorded struct {
72+ Name string
73+ Payload any
74+}
75+
76+// Recorder is an Emitter that keeps every event, for tests.
77+//
78+// recorder := &events.Recorder{}
79+// manager := agents.NewManager(…, recorder)
80+// …
81+// recorder.Named(events.AgentState)
82+type Recorder struct {
83+ mu sync.Mutex
84+ events []Recorded
85+}
86+
87+// Emit records the event.
88+func (r *Recorder) Emit(name string, payload any) {
89+ r.mu.Lock()
90+ defer r.mu.Unlock()
91+ r.events = append(r.events, Recorded{Name: name, Payload: payload})
92+}
93+
94+// All returns a copy of the recorded events, oldest first.
95+func (r *Recorder) All() []Recorded {
96+ r.mu.Lock()
97+ defer r.mu.Unlock()
98+ return append([]Recorded(nil), r.events...)
99+}
100+
101+// Named returns the payloads of the events called name, oldest first.
102+func (r *Recorder) Named(name string) []any {
103+ var payloads []any
104+ for _, event := range r.All() {
105+ if event.Name == name {
106+ payloads = append(payloads, event.Payload)
107+ }
108+ }
109+ return payloads
110+}
added internal/events/events_test.go +57 -0
new file mode 100644
@@ -0,0 +1,57 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package events_test
4+
5+import (
6+ "fmt"
7+ "sync"
8+ "testing"
9+
10+ "git.rickub.com/bots-garden/sidekicks/internal/events"
11+)
12+
13+func TestRecorderKeepsEventsInOrder(t *testing.T) {
14+ recorder := &events.Recorder{}
15+ recorder.Emit(events.AgentState, events.AgentStatePayload{Agent: "alice", State: "starting"})
16+ recorder.Emit(events.AgentLog, events.AgentLogPayload{Agent: "alice", Line: "hello"})
17+ recorder.Emit(events.AgentState, events.AgentStatePayload{Agent: "alice", State: "ready"})
18+
19+ states := recorder.Named(events.AgentState)
20+ if len(states) != 2 {
21+ t.Fatalf("want 2 state events, got %d", len(states))
22+ }
23+ if states[1].(events.AgentStatePayload).State != "ready" {
24+ t.Fatalf("events out of order: %v", states)
25+ }
26+ if len(recorder.All()) != 3 {
27+ t.Fatalf("want 3 events, got %d", len(recorder.All()))
28+ }
29+}
30+
31+func TestRecorderIsSafeForConcurrentUse(t *testing.T) {
32+ recorder := &events.Recorder{}
33+ var wg sync.WaitGroup
34+ for i := 0; i < 50; i++ {
35+ wg.Add(1)
36+ go func() {
37+ defer wg.Done()
38+ recorder.Emit(events.AgentLog, events.AgentLogPayload{Line: "x"})
39+ }()
40+ }
41+ wg.Wait()
42+ if got := len(recorder.All()); got != 50 {
43+ t.Fatalf("want 50 events, got %d", got)
44+ }
45+}
46+
47+func TestDiscardAcceptsEvents(_ *testing.T) {
48+ events.Discard.Emit(events.ConfigChanged, nil)
49+}
50+
51+func ExampleEmitterFunc() {
52+ emitter := events.EmitterFunc(func(name string, payload any) {
53+ fmt.Println(name, payload.(events.AgentStatePayload).State)
54+ })
55+ emitter.Emit(events.AgentState, events.AgentStatePayload{Agent: "alice", State: "ready"})
56+ // Output: agent:state ready
57+}
new file mode 100644
@@ -0,0 +1,57 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+package events_test
4+
5+import (
6+ "fmt"
7+ "sync"
8+ "testing"
9+
10+ "git.rickub.com/bots-garden/sidekicks/internal/events"
11+)
12+
13+func TestRecorderKeepsEventsInOrder(t *testing.T) {
14+ recorder := &events.Recorder{}
15+ recorder.Emit(events.AgentState, events.AgentStatePayload{Agent: "alice", State: "starting"})
16+ recorder.Emit(events.AgentLog, events.AgentLogPayload{Agent: "alice", Line: "hello"})
17+ recorder.Emit(events.AgentState, events.AgentStatePayload{Agent: "alice", State: "ready"})
18+
19+ states := recorder.Named(events.AgentState)
20+ if len(states) != 2 {
21+ t.Fatalf("want 2 state events, got %d", len(states))
22+ }
23+ if states[1].(events.AgentStatePayload).State != "ready" {
24+ t.Fatalf("events out of order: %v", states)
25+ }
26+ if len(recorder.All()) != 3 {
27+ t.Fatalf("want 3 events, got %d", len(recorder.All()))
28+ }
29+}
30+
31+func TestRecorderIsSafeForConcurrentUse(t *testing.T) {
32+ recorder := &events.Recorder{}
33+ var wg sync.WaitGroup
34+ for i := 0; i < 50; i++ {
35+ wg.Add(1)
36+ go func() {
37+ defer wg.Done()
38+ recorder.Emit(events.AgentLog, events.AgentLogPayload{Line: "x"})
39+ }()
40+ }
41+ wg.Wait()
42+ if got := len(recorder.All()); got != 50 {
43+ t.Fatalf("want 50 events, got %d", got)
44+ }
45+}
46+
47+func TestDiscardAcceptsEvents(_ *testing.T) {
48+ events.Discard.Emit(events.ConfigChanged, nil)
49+}
50+
51+func ExampleEmitterFunc() {
52+ emitter := events.EmitterFunc(func(name string, payload any) {
53+ fmt.Println(name, payload.(events.AgentStatePayload).State)
54+ })
55+ emitter.Emit(events.AgentState, events.AgentStatePayload{Agent: "alice", State: "ready"})
56+ // Output: agent:state ready
57+}
added main.go +87 -0
new file mode 100644
@@ -0,0 +1,87 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// Command sidekicks is the Sidekicks desktop application: it reads the
4+// configuration, binds the services of internal/app to the Svelte interface
5+// and opens the window. The main package sits at the repository root because
6+// the build Taskfiles generated by Wails build "." (ADR 0003, amendment).
7+//
8+// sidekicks [--config path/to/config.yaml]
9+package main
10+
11+import (
12+ "context"
13+ "embed"
14+ "flag"
15+ "fmt"
16+ "log"
17+ "os"
18+
19+ "git.rickub.com/bots-garden/sidekicks/internal/app"
20+ "git.rickub.com/bots-garden/sidekicks/internal/config"
21+ "git.rickub.com/bots-garden/sidekicks/internal/events"
22+ "github.com/wailsapp/wails/v3/pkg/application"
23+)
24+
25+//go:embed all:frontend/dist
26+var assets embed.FS
27+
28+// titleBarInset must match --titlebar-inset in frontend/src/style.css: the
29+// height of the area covered by the macOS traffic lights.
30+const titleBarInset = 38
31+
32+func init() {
33+ // Registering the payload types generates typed TypeScript events.
34+ application.RegisterEvent[events.ConfigChangedPayload](events.ConfigChanged)
35+ application.RegisterEvent[events.AgentStatePayload](events.AgentState)
36+ application.RegisterEvent[events.AgentLogPayload](events.AgentLog)
37+}
38+
39+func main() {
40+ configPath := flag.String("config", "", "configuration file (default: $"+config.EnvironmentVariable+", then the default location)")
41+ flag.Parse()
42+
43+ location, err := config.Locate(*configPath, os.Getenv, "")
44+ if err != nil {
45+ fmt.Fprintln(os.Stderr, "sidekicks:", err)
46+ os.Exit(1)
47+ }
48+
49+ // The emitter is called once the application exists; core only emits
50+ // after Run has started.
51+ var wailsApp *application.App
52+ emitter := events.EmitterFunc(func(name string, payload any) { wailsApp.Event.Emit(name, payload) })
53+ core := app.New(location, emitter, nil)
54+ ctx, cancel := context.WithCancel(context.Background())
55+
56+ wailsApp = application.New(application.Options{
57+ Name: "Sidekicks",
58+ Description: "Converse with AI agents in channels",
59+ Services: []application.Service{
60+ application.NewService(&app.AgentService{Core: core}),
61+ application.NewService(&app.ConfigService{Core: core}),
62+ },
63+ Assets: application.AssetOptions{Handler: application.AssetFileServerFS(assets)},
64+ Mac: application.MacOptions{ApplicationShouldTerminateAfterLastWindowClosed: true},
65+ OnShutdown: func() {
66+ cancel()
67+ core.Shutdown(context.Background())
68+ },
69+ })
70+ wailsApp.Window.NewWithOptions(application.WebviewWindowOptions{
71+ Title: "Sidekicks",
72+ Width: 1280,
73+ Height: 800,
74+ Mac: application.MacWindow{
75+ InvisibleTitleBarHeight: titleBarInset,
76+ Backdrop: application.MacBackdropTranslucent,
77+ TitleBar: application.MacTitleBarHiddenInset,
78+ },
79+ BackgroundColour: application.NewRGB(24, 25, 29),
80+ URL: "/",
81+ })
82+
83+ go core.Run(ctx)
84+ if err := wailsApp.Run(); err != nil {
85+ log.Fatal(err)
86+ }
87+}
new file mode 100644
@@ -0,0 +1,87 @@
1+// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
2+
3+// Command sidekicks is the Sidekicks desktop application: it reads the
4+// configuration, binds the services of internal/app to the Svelte interface
5+// and opens the window. The main package sits at the repository root because
6+// the build Taskfiles generated by Wails build "." (ADR 0003, amendment).
7+//
8+// sidekicks [--config path/to/config.yaml]
9+package main
10+
11+import (
12+ "context"
13+ "embed"
14+ "flag"
15+ "fmt"
16+ "log"
17+ "os"
18+
19+ "git.rickub.com/bots-garden/sidekicks/internal/app"
20+ "git.rickub.com/bots-garden/sidekicks/internal/config"
21+ "git.rickub.com/bots-garden/sidekicks/internal/events"
22+ "github.com/wailsapp/wails/v3/pkg/application"
23+)
24+
25+//go:embed all:frontend/dist
26+var assets embed.FS
27+
28+// titleBarInset must match --titlebar-inset in frontend/src/style.css: the
29+// height of the area covered by the macOS traffic lights.
30+const titleBarInset = 38
31+
32+func init() {
33+ // Registering the payload types generates typed TypeScript events.
34+ application.RegisterEvent[events.ConfigChangedPayload](events.ConfigChanged)
35+ application.RegisterEvent[events.AgentStatePayload](events.AgentState)
36+ application.RegisterEvent[events.AgentLogPayload](events.AgentLog)
37+}
38+
39+func main() {
40+ configPath := flag.String("config", "", "configuration file (default: $"+config.EnvironmentVariable+", then the default location)")
41+ flag.Parse()
42+
43+ location, err := config.Locate(*configPath, os.Getenv, "")
44+ if err != nil {
45+ fmt.Fprintln(os.Stderr, "sidekicks:", err)
46+ os.Exit(1)
47+ }
48+
49+ // The emitter is called once the application exists; core only emits
50+ // after Run has started.
51+ var wailsApp *application.App
52+ emitter := events.EmitterFunc(func(name string, payload any) { wailsApp.Event.Emit(name, payload) })
53+ core := app.New(location, emitter, nil)
54+ ctx, cancel := context.WithCancel(context.Background())
55+
56+ wailsApp = application.New(application.Options{
57+ Name: "Sidekicks",
58+ Description: "Converse with AI agents in channels",
59+ Services: []application.Service{
60+ application.NewService(&app.AgentService{Core: core}),
61+ application.NewService(&app.ConfigService{Core: core}),
62+ },
63+ Assets: application.AssetOptions{Handler: application.AssetFileServerFS(assets)},
64+ Mac: application.MacOptions{ApplicationShouldTerminateAfterLastWindowClosed: true},
65+ OnShutdown: func() {
66+ cancel()
67+ core.Shutdown(context.Background())
68+ },
69+ })
70+ wailsApp.Window.NewWithOptions(application.WebviewWindowOptions{
71+ Title: "Sidekicks",
72+ Width: 1280,
73+ Height: 800,
74+ Mac: application.MacWindow{
75+ InvisibleTitleBarHeight: titleBarInset,
76+ Backdrop: application.MacBackdropTranslucent,
77+ TitleBar: application.MacTitleBarHiddenInset,
78+ },
79+ BackgroundColour: application.NewRGB(24, 25, 29),
80+ URL: "/",
81+ })
82+
83+ go core.Run(ctx)
84+ if err := wailsApp.Run(); err != nil {
85+ log.Fatal(err)
86+ }
87+}
modified schema/config.schema.json +8 -3
@@ -75,10 +75,15 @@
7575 "default": 2
7676 },
7777 "stopIdleAgentsAfter": {
78- "description": "Stop an agent that has not been used for this long (Go duration: 30s, 10m, 1h). 0 never stops it.",
79- "type": "string",
78+ "description": "Stop an agent that has not been used for this long (Go duration: 30s, 10m, 1h). 0 (number or text) never stops it.",
79+ "type": [
80+ "string",
81+ "integer"
82+ ],
8083 "pattern": "^(0|([0-9]+(\\.[0-9]+)?(ms|s|m|h))+)$",
81- "default": "10m"
84+ "default": "10m",
85+ "minimum": 0,
86+ "maximum": 0
8287 }
8388 }
8489 },
@@ -75,10 +75,15 @@
75 "default": 275 "default": 2
76 },76 },
77 "stopIdleAgentsAfter": {77 "stopIdleAgentsAfter": {
78- "description": "Stop an agent that has not been used for this long (Go duration: 30s, 10m, 1h). 0 never stops it.",78+ "description": "Stop an agent that has not been used for this long (Go duration: 30s, 10m, 1h). 0 (number or text) never stops it.",
79- "type": "string",79+ "type": [
80+ "string",
81+ "integer"
82+ ],
80 "pattern": "^(0|([0-9]+(\\.[0-9]+)?(ms|s|m|h))+)$",83 "pattern": "^(0|([0-9]+(\\.[0-9]+)?(ms|s|m|h))+)$",
81- "default": "10m"84+ "default": "10m",
85+ "minimum": 0,
86+ "maximum": 0
82 }87 }
83 }88 }
84 },89 },
added schema/testdata/invalid/idle-number-not-zero.yaml +4 -0
new file mode 100644
@@ -0,0 +1,4 @@
1+version: 1
2+limits:
3+ stopIdleAgentsAfter: 5
4+agents: []
new file mode 100644
@@ -0,0 +1,4 @@
1+version: 1
2+limits:
3+ stopIdleAgentsAfter: 5
4+agents: []
added schema/testdata/valid/idle-zero-number.yaml +4 -0
new file mode 100644
@@ -0,0 +1,4 @@
1+version: 1
2+limits:
3+ stopIdleAgentsAfter: 0
4+agents: []
new file mode 100644
@@ -0,0 +1,4 @@
1+version: 1
2+limits:
3+ stopIdleAgentsAfter: 0
4+agents: []
added scripts/check-license-headers.sh +61 -0
new file mode 100755
@@ -0,0 +1,61 @@
1+#!/usr/bin/env bash
2+# Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
3+#
4+# check-license-headers.sh — every hand-written source file must carry the
5+# one-line licence header in its first three lines (convention of issue #8).
6+#
7+# Usage: scripts/check-license-headers.sh [repository root]
8+# Exits 1 and lists the files without the header.
9+#
10+# Checked: the files git knows (tracked, or new and not ignored) with a source
11+# extension. Not checked, on purpose:
12+# - generated files: frontend/bindings, Wails' build/ and Taskfile.yml, *.d.ts, lockfiles
13+# - template configuration: vite.config.ts, svelte.config.js
14+# - third-party content: agents/ (the test agents)
15+# - test data: */testdata/*
16+# - the owner's personal scripts at the root: git.sh, new.*.sh, tag-step.sh
17+# - kit files: .claude/
18+# - the issue tracker: .tickets/
19+
20+set -eu
21+
22+root="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
23+marker="Sidekicks Source-Available License"
24+
25+is_source() {
26+ case "$1" in
27+ *.go | *.ts | *.js | *.svelte | *.css | *.html | *.sh | *.py | *.yml | *.yaml | Makefile | */Makefile) return 0 ;;
28+ esac
29+ return 1
30+}
31+
32+is_excluded() {
33+ case "$1" in
34+ frontend/bindings/* | */frontend/bindings/*) return 0 ;;
35+ build/* | */build/* | Taskfile.yml | */Taskfile.yml) return 0 ;;
36+ *.d.ts | pnpm-lock.yaml | */pnpm-lock.yaml) return 0 ;;
37+ */vite.config.ts | vite.config.ts | */svelte.config.js | svelte.config.js) return 0 ;;
38+ agents/*) return 0 ;;
39+ */testdata/*) return 0 ;;
40+ git.sh | new.*.sh | tag-step.sh) return 0 ;;
41+ .claude/* | .tickets/*) return 0 ;;
42+ esac
43+ return 1
44+}
45+
46+missing=0
47+while IFS= read -r file; do
48+ is_source "$file" || continue
49+ is_excluded "$file" && continue
50+ [ -f "$root/$file" ] || continue
51+ if ! head -n 3 "$root/$file" | grep -qF "$marker"; then
52+ echo "missing licence header: $file"
53+ missing=$((missing + 1))
54+ fi
55+done < <(git -C "$root" ls-files --cached --others --exclude-standard)
56+
57+if [ "$missing" -gt 0 ]; then
58+ echo "$missing file(s) without the licence header (see .tickets/issues/0008-*.yaml, comment 1)" >&2
59+ exit 1
60+fi
61+echo "licence headers: ok"
new file mode 100755
@@ -0,0 +1,61 @@
1+#!/usr/bin/env bash
2+# Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
3+#
4+# check-license-headers.sh — every hand-written source file must carry the
5+# one-line licence header in its first three lines (convention of issue #8).
6+#
7+# Usage: scripts/check-license-headers.sh [repository root]
8+# Exits 1 and lists the files without the header.
9+#
10+# Checked: the files git knows (tracked, or new and not ignored) with a source
11+# extension. Not checked, on purpose:
12+# - generated files: frontend/bindings, Wails' build/ and Taskfile.yml, *.d.ts, lockfiles
13+# - template configuration: vite.config.ts, svelte.config.js
14+# - third-party content: agents/ (the test agents)
15+# - test data: */testdata/*
16+# - the owner's personal scripts at the root: git.sh, new.*.sh, tag-step.sh
17+# - kit files: .claude/
18+# - the issue tracker: .tickets/
19+
20+set -eu
21+
22+root="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
23+marker="Sidekicks Source-Available License"
24+
25+is_source() {
26+ case "$1" in
27+ *.go | *.ts | *.js | *.svelte | *.css | *.html | *.sh | *.py | *.yml | *.yaml | Makefile | */Makefile) return 0 ;;
28+ esac
29+ return 1
30+}
31+
32+is_excluded() {
33+ case "$1" in
34+ frontend/bindings/* | */frontend/bindings/*) return 0 ;;
35+ build/* | */build/* | Taskfile.yml | */Taskfile.yml) return 0 ;;
36+ *.d.ts | pnpm-lock.yaml | */pnpm-lock.yaml) return 0 ;;
37+ */vite.config.ts | vite.config.ts | */svelte.config.js | svelte.config.js) return 0 ;;
38+ agents/*) return 0 ;;
39+ */testdata/*) return 0 ;;
40+ git.sh | new.*.sh | tag-step.sh) return 0 ;;
41+ .claude/* | .tickets/*) return 0 ;;
42+ esac
43+ return 1
44+}
45+
46+missing=0
47+while IFS= read -r file; do
48+ is_source "$file" || continue
49+ is_excluded "$file" && continue
50+ [ -f "$root/$file" ] || continue
51+ if ! head -n 3 "$root/$file" | grep -qF "$marker"; then
52+ echo "missing licence header: $file"
53+ missing=$((missing + 1))
54+ fi
55+done < <(git -C "$root" ls-files --cached --others --exclude-standard)
56+
57+if [ "$missing" -gt 0 ]; then
58+ echo "$missing file(s) without the licence header (see .tickets/issues/0008-*.yaml, comment 1)" >&2
59+ exit 1
60+fi
61+echo "licence headers: ok"
added scripts/check-license-headers_test.sh +64 -0
new file mode 100755
@@ -0,0 +1,64 @@
1+#!/usr/bin/env bash
2+# Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
3+#
4+# Tests for check-license-headers.sh, on a throwaway git repository.
5+#
6+# Usage: scripts/check-license-headers_test.sh
7+
8+set -eu
9+
10+script="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/check-license-headers.sh"
11+header="// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE"
12+failures=0
13+
14+repository() {
15+ work="$(mktemp -d)"
16+ git -C "$work" init -q
17+ mkdir -p "$work/internal/app" "$work/frontend/bindings" "$work/agents/alice" "$work/build" "$work/schema/testdata"
18+ printf '%s\n\npackage app\n' "$header" > "$work/internal/app/app.go"
19+ printf '// generated\n' > "$work/frontend/bindings/index.ts"
20+ printf 'name: alice\n' > "$work/agents/alice/agent.yaml"
21+ printf 'version: 3\n' > "$work/build/config.yml"
22+ printf 'version: 1\n' > "$work/schema/testdata/sample.yaml"
23+ printf '#!/bin/bash\ngit push\n' > "$work/git.sh"
24+ printf '# notes\n' > "$work/README.md"
25+}
26+
27+check() {
28+ local name="$1" expected_status="$2" expected_output="$3"
29+ local output status=0
30+ output="$("$script" "$work" 2>&1)" || status=$?
31+ if [ "$status" -eq "$expected_status" ] && grep -qF "$expected_output" <<<"$output"; then
32+ echo "ok $name"
33+ else
34+ echo "FAIL $name (status $status)"
35+ echo "$output" | sed 's/^/ /'
36+ failures=$((failures + 1))
37+ fi
38+ rm -rf "$work"
39+}
40+
41+repository
42+check "files with the header and excluded files pass" 0 "licence headers: ok"
43+
44+repository
45+printf 'package app\n' > "$work/internal/app/missing.go"
46+check "a Go file without the header is reported" 1 "missing licence header: internal/app/missing.go"
47+
48+repository
49+printf '<script>let x = 1</script>\n' > "$work/frontend/Panel.svelte"
50+check "a Svelte file without the header is reported" 1 "missing licence header: frontend/Panel.svelte"
51+
52+repository
53+printf '#!/usr/bin/env bash\n# Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE\necho hi\n' > "$work/scripts.sh"
54+check "a header after the shebang is accepted" 0 "licence headers: ok"
55+
56+repository
57+printf 'node_modules/\n' > "$work/.gitignore"
58+mkdir -p "$work/node_modules/lib"
59+printf 'module.exports = 1\n' > "$work/node_modules/lib/index.js"
60+printf '# Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE\n' >> "$work/.gitignore"
61+check "ignored files are not checked" 0 "licence headers: ok"
62+
63+[ "$failures" -eq 0 ] && echo "all licence header tests passed"
64+[ "$failures" -eq 0 ]
new file mode 100755
@@ -0,0 +1,64 @@
1+#!/usr/bin/env bash
2+# Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
3+#
4+# Tests for check-license-headers.sh, on a throwaway git repository.
5+#
6+# Usage: scripts/check-license-headers_test.sh
7+
8+set -eu
9+
10+script="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/check-license-headers.sh"
11+header="// Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE"
12+failures=0
13+
14+repository() {
15+ work="$(mktemp -d)"
16+ git -C "$work" init -q
17+ mkdir -p "$work/internal/app" "$work/frontend/bindings" "$work/agents/alice" "$work/build" "$work/schema/testdata"
18+ printf '%s\n\npackage app\n' "$header" > "$work/internal/app/app.go"
19+ printf '// generated\n' > "$work/frontend/bindings/index.ts"
20+ printf 'name: alice\n' > "$work/agents/alice/agent.yaml"
21+ printf 'version: 3\n' > "$work/build/config.yml"
22+ printf 'version: 1\n' > "$work/schema/testdata/sample.yaml"
23+ printf '#!/bin/bash\ngit push\n' > "$work/git.sh"
24+ printf '# notes\n' > "$work/README.md"
25+}
26+
27+check() {
28+ local name="$1" expected_status="$2" expected_output="$3"
29+ local output status=0
30+ output="$("$script" "$work" 2>&1)" || status=$?
31+ if [ "$status" -eq "$expected_status" ] && grep -qF "$expected_output" <<<"$output"; then
32+ echo "ok $name"
33+ else
34+ echo "FAIL $name (status $status)"
35+ echo "$output" | sed 's/^/ /'
36+ failures=$((failures + 1))
37+ fi
38+ rm -rf "$work"
39+}
40+
41+repository
42+check "files with the header and excluded files pass" 0 "licence headers: ok"
43+
44+repository
45+printf 'package app\n' > "$work/internal/app/missing.go"
46+check "a Go file without the header is reported" 1 "missing licence header: internal/app/missing.go"
47+
48+repository
49+printf '<script>let x = 1</script>\n' > "$work/frontend/Panel.svelte"
50+check "a Svelte file without the header is reported" 1 "missing licence header: frontend/Panel.svelte"
51+
52+repository
53+printf '#!/usr/bin/env bash\n# Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE\necho hi\n' > "$work/scripts.sh"
54+check "a header after the shebang is accepted" 0 "licence headers: ok"
55+
56+repository
57+printf 'node_modules/\n' > "$work/.gitignore"
58+mkdir -p "$work/node_modules/lib"
59+printf 'module.exports = 1\n' > "$work/node_modules/lib/index.js"
60+printf '# Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE\n' >> "$work/.gitignore"
61+check "ignored files are not checked" 0 "licence headers: ok"
62+
63+[ "$failures" -eq 0 ] && echo "all licence header tests passed"
64+[ "$failures" -eq 0 ]
added scripts/install-test-config.sh +66 -0
new file mode 100755
@@ -0,0 +1,66 @@
1+#!/usr/bin/env bash
2+# Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
3+#
4+# install-test-config.sh — writes the configuration of the four test agents
5+# (examples/test-agents.yaml) at Sidekicks' default location, with absolute
6+# paths to this repository, so that Sidekicks works without --config.
7+#
8+# Usage: scripts/install-test-config.sh [target file]
9+# default target: macOS ~/Library/Application Support/sidekicks/config.yaml
10+# Linux ${XDG_CONFIG_HOME:-~/.config}/sidekicks/config.yaml
11+#
12+# An application opened from the Finder gets a minimal PATH (/usr/bin:/bin…)
13+# and would find neither mm nor jq: the current MM_BIN and PATH are written in
14+# the env of every agent. An existing file is kept as config.yaml.<date>.bak.
15+
16+set -eu
17+
18+repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
19+example="$repo_root/examples/test-agents.yaml"
20+
21+die() {
22+ echo "install-test-config.sh: $*" >&2
23+ exit 1
24+}
25+
26+default_target() {
27+ case "$(uname -s)" in
28+ Darwin) echo "$HOME/Library/Application Support/sidekicks/config.yaml" ;;
29+ *) echo "${XDG_CONFIG_HOME:-$HOME/.config}/sidekicks/config.yaml" ;;
30+ esac
31+}
32+
33+target="${1:-$(default_target)}"
34+mm_bin="${MM_BIN:-$(command -v mm || true)}"
35+[ -n "$mm_bin" ] || die "mm not found: install it or set MM_BIN"
36+case "$repo_root$mm_bin$PATH" in
37+ *'"'* | *'|'*) die "paths containing \" or | are not supported" ;;
38+esac
39+
40+mkdir -p "$(dirname "$target")"
41+if [ -f "$target" ]; then
42+ backup="$target.$(date +%Y%m%d-%H%M%S).bak"
43+ cp "$target" "$backup"
44+ echo "previous configuration kept as: $backup"
45+fi
46+
47+{
48+ echo "# Sidekicks configuration for the four test agents of $repo_root"
49+ echo "# Generated by scripts/install-test-config.sh from examples/test-agents.yaml:"
50+ echo "# run it again after moving the repository, mm or Homebrew."
51+ echo
52+ # Drop the comment header of the example, make its paths absolute, and
53+ # give every agent the environment it needs outside a terminal.
54+ sed -n '/^version:/,$p' "$example" |
55+ sed -e "s|\.\./scripts/agents\.sh|$repo_root/scripts/agents.sh|" \
56+ -e "s|\.\./agents/|$repo_root/agents/|" |
57+ awk -v mm="$mm_bin" -v path="$PATH" '
58+ { print }
59+ /^ workspace: / {
60+ print " env:"
61+ print " MM_BIN: \"" mm "\""
62+ print " PATH: \"" path "\""
63+ }'
64+} > "$target.tmp"
65+mv "$target.tmp" "$target"
66+echo "test agents configuration written to: $target"
new file mode 100755
@@ -0,0 +1,66 @@
1+#!/usr/bin/env bash
2+# Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE
3+#
4+# install-test-config.sh — writes the configuration of the four test agents
5+# (examples/test-agents.yaml) at Sidekicks' default location, with absolute
6+# paths to this repository, so that Sidekicks works without --config.
7+#
8+# Usage: scripts/install-test-config.sh [target file]
9+# default target: macOS ~/Library/Application Support/sidekicks/config.yaml
10+# Linux ${XDG_CONFIG_HOME:-~/.config}/sidekicks/config.yaml
11+#
12+# An application opened from the Finder gets a minimal PATH (/usr/bin:/bin…)
13+# and would find neither mm nor jq: the current MM_BIN and PATH are written in
14+# the env of every agent. An existing file is kept as config.yaml.<date>.bak.
15+
16+set -eu
17+
18+repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
19+example="$repo_root/examples/test-agents.yaml"
20+
21+die() {
22+ echo "install-test-config.sh: $*" >&2
23+ exit 1
24+}
25+
26+default_target() {
27+ case "$(uname -s)" in
28+ Darwin) echo "$HOME/Library/Application Support/sidekicks/config.yaml" ;;
29+ *) echo "${XDG_CONFIG_HOME:-$HOME/.config}/sidekicks/config.yaml" ;;
30+ esac
31+}
32+
33+target="${1:-$(default_target)}"
34+mm_bin="${MM_BIN:-$(command -v mm || true)}"
35+[ -n "$mm_bin" ] || die "mm not found: install it or set MM_BIN"
36+case "$repo_root$mm_bin$PATH" in
37+ *'"'* | *'|'*) die "paths containing \" or | are not supported" ;;
38+esac
39+
40+mkdir -p "$(dirname "$target")"
41+if [ -f "$target" ]; then
42+ backup="$target.$(date +%Y%m%d-%H%M%S).bak"
43+ cp "$target" "$backup"
44+ echo "previous configuration kept as: $backup"
45+fi
46+
47+{
48+ echo "# Sidekicks configuration for the four test agents of $repo_root"
49+ echo "# Generated by scripts/install-test-config.sh from examples/test-agents.yaml:"
50+ echo "# run it again after moving the repository, mm or Homebrew."
51+ echo
52+ # Drop the comment header of the example, make its paths absolute, and
53+ # give every agent the environment it needs outside a terminal.
54+ sed -n '/^version:/,$p' "$example" |
55+ sed -e "s|\.\./scripts/agents\.sh|$repo_root/scripts/agents.sh|" \
56+ -e "s|\.\./agents/|$repo_root/agents/|" |
57+ awk -v mm="$mm_bin" -v path="$PATH" '
58+ { print }
59+ /^ workspace: / {
60+ print " env:"
61+ print " MM_BIN: \"" mm "\""
62+ print " PATH: \"" path "\""
63+ }'
64+} > "$target.tmp"
65+mv "$target.tmp" "$target"
66+echo "test agents configuration written to: $target"
added tools/go.mod +219 -0
new file mode 100644
@@ -0,0 +1,219 @@
1+module git.rickub.com/bots-garden/sidekicks/tools
2+
3+go 1.26.0
4+
5+tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint
6+
7+require (
8+ 4d63.com/gocheckcompilerdirectives v1.4.0 // indirect
9+ 4d63.com/gochecknoglobals v0.2.2 // indirect
10+ charm.land/lipgloss/v2 v2.0.6 // indirect
11+ codeberg.org/chavacava/garif v0.2.1 // indirect
12+ codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect
13+ dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
14+ dev.gaijin.team/go/exhaustruct/v5 v5.2.0 // indirect
15+ dev.gaijin.team/go/golib v0.8.1 // indirect
16+ github.com/4meepo/tagalign v1.4.4 // indirect
17+ github.com/Abirdcfly/dupword v0.1.8 // indirect
18+ github.com/AdminBenni/iota-mixing v1.0.0 // indirect
19+ github.com/AlwxSin/noinlineerr v1.0.6 // indirect
20+ github.com/Antonboom/errname v1.1.2 // indirect
21+ github.com/Antonboom/nilnil v1.1.2 // indirect
22+ github.com/Antonboom/testifylint v1.6.4 // indirect
23+ github.com/BurntSushi/toml v1.6.0 // indirect
24+ github.com/ClickHouse/clickhouse-go-linter v1.2.1 // indirect
25+ github.com/Djarvur/go-err113 v0.1.1 // indirect
26+ github.com/Masterminds/semver/v3 v3.5.0 // indirect
27+ github.com/MirrexOne/unqueryvet v1.5.4 // indirect
28+ github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
29+ github.com/alecthomas/chroma/v2 v2.27.0 // indirect
30+ github.com/alecthomas/go-check-sumtype v0.5.1-0.20260828200218-ae6904d28606 // indirect
31+ github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
32+ github.com/alexkohler/prealloc v1.1.0 // indirect
33+ github.com/alfatraining/structtag v1.0.0 // indirect
34+ github.com/alingse/asasalint v0.0.11 // indirect
35+ github.com/alingse/nilnesserr v0.2.0 // indirect
36+ github.com/ashanbrown/forbidigo/v2 v2.3.1 // indirect
37+ github.com/ashanbrown/makezero/v2 v2.2.1 // indirect
38+ github.com/beorn7/perks v1.0.1 // indirect
39+ github.com/bkielbasa/cyclop v1.2.3 // indirect
40+ github.com/blizzy78/varnamelen v0.8.0 // indirect
41+ github.com/bombsimon/wsl/v4 v4.7.0 // indirect
42+ github.com/bombsimon/wsl/v5 v5.9.0 // indirect
43+ github.com/breml/bidichk v0.3.3 // indirect
44+ github.com/breml/errchkjson v0.4.1 // indirect
45+ github.com/butuzov/ireturn v0.4.1 // indirect
46+ github.com/butuzov/mirror v1.3.3 // indirect
47+ github.com/catenacyber/perfsprint v0.10.1 // indirect
48+ github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
49+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
50+ github.com/charithe/durationcheck v0.0.11 // indirect
51+ github.com/charmbracelet/colorprofile v0.4.3 // indirect
52+ github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 // indirect
53+ github.com/charmbracelet/x/ansi v0.11.8 // indirect
54+ github.com/charmbracelet/x/term v0.2.2 // indirect
55+ github.com/charmbracelet/x/termios v0.1.1 // indirect
56+ github.com/charmbracelet/x/windows v0.2.2 // indirect
57+ github.com/ckaznocha/intrange v0.3.1 // indirect
58+ github.com/clipperhouse/displaywidth v0.11.0 // indirect
59+ github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
60+ github.com/curioswitch/go-reassign v0.3.0 // indirect
61+ github.com/daixiang0/gci v0.13.7 // indirect
62+ github.com/dave/dst v0.27.3 // indirect
63+ github.com/denis-tingaikin/go-header v0.5.0 // indirect
64+ github.com/dlclark/regexp2/v2 v2.2.1 // indirect
65+ github.com/ettle/strcase v0.2.0 // indirect
66+ github.com/fatih/color v1.19.0 // indirect
67+ github.com/fatih/structtag v1.2.0 // indirect
68+ github.com/firefart/nonamedreturns v1.0.8 // indirect
69+ github.com/fsnotify/fsnotify v1.9.0 // indirect
70+ github.com/fzipp/gocyclo v0.6.0 // indirect
71+ github.com/ghostiam/protogetter v1.0.1 // indirect
72+ github.com/go-critic/go-critic v0.15.0 // indirect
73+ github.com/go-toolsmith/astcast v1.1.0 // indirect
74+ github.com/go-toolsmith/astcopy v1.1.0 // indirect
75+ github.com/go-toolsmith/astequal v1.2.0 // indirect
76+ github.com/go-toolsmith/astfmt v1.1.0 // indirect
77+ github.com/go-toolsmith/astp v1.1.0 // indirect
78+ github.com/go-toolsmith/strparse v1.1.0 // indirect
79+ github.com/go-toolsmith/typep v1.1.0 // indirect
80+ github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
81+ github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
82+ github.com/gobwas/glob v0.2.3 // indirect
83+ github.com/godoc-lint/godoc-lint v0.11.4 // indirect
84+ github.com/gofrs/flock v0.13.1 // indirect
85+ github.com/golang/protobuf v1.5.3 // indirect
86+ github.com/golangci/asciicheck v0.5.0 // indirect
87+ github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6 // indirect
88+ github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 // indirect
89+ github.com/golangci/go-printf-func-name v0.1.1 // indirect
90+ github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792 // indirect
91+ github.com/golangci/golangci-lint/v2 v2.14.0 // indirect
92+ github.com/golangci/golines v0.15.0 // indirect
93+ github.com/golangci/misspell v0.8.0 // indirect
94+ github.com/golangci/plugin-module-register v0.1.2 // indirect
95+ github.com/golangci/revgrep v0.8.0 // indirect
96+ github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba // indirect
97+ github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect
98+ github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect
99+ github.com/google/go-cmp v0.7.0 // indirect
100+ github.com/gordonklaus/ineffassign v0.2.0 // indirect
101+ github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
102+ github.com/gostaticanalysis/comment v1.5.0 // indirect
103+ github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
104+ github.com/gostaticanalysis/nilerr v0.1.2 // indirect
105+ github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
106+ github.com/hashicorp/go-version v1.9.0 // indirect
107+ github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
108+ github.com/hashicorp/hcl v1.0.0 // indirect
109+ github.com/hexops/gotextdiff v1.0.3 // indirect
110+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
111+ github.com/jgautheron/goconst v1.11.0 // indirect
112+ github.com/jjti/go-spancheck v0.6.5 // indirect
113+ github.com/julz/importas v0.2.0 // indirect
114+ github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect
115+ github.com/kisielk/errcheck v1.20.0 // indirect
116+ github.com/kkHAIKE/contextcheck v1.1.6 // indirect
117+ github.com/kulti/thelper v0.7.1 // indirect
118+ github.com/kunwardeep/paralleltest v1.0.15 // indirect
119+ github.com/ldez/exptostd v0.4.5 // indirect
120+ github.com/ldez/gomoddirectives v0.10.0 // indirect
121+ github.com/ldez/grignotin v0.10.1 // indirect
122+ github.com/ldez/structtags v0.6.1 // indirect
123+ github.com/ldez/tagliatelle v0.8.0 // indirect
124+ github.com/ldez/usetesting v0.5.0 // indirect
125+ github.com/leonklingele/grouper v1.1.2 // indirect
126+ github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
127+ github.com/macabu/inamedparam v0.2.0 // indirect
128+ github.com/magiconair/properties v1.8.6 // indirect
129+ github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect
130+ github.com/manuelarte/funcorder v0.6.0 // indirect
131+ github.com/maratori/testableexamples v1.0.1 // indirect
132+ github.com/maratori/testpackage v1.1.2 // indirect
133+ github.com/matoous/godox v1.1.0 // indirect
134+ github.com/mattn/go-colorable v0.1.15 // indirect
135+ github.com/mattn/go-isatty v0.0.22 // indirect
136+ github.com/mattn/go-runewidth v0.0.24 // indirect
137+ github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
138+ github.com/mgechev/revive v1.17.0 // indirect
139+ github.com/mitchellh/go-homedir v1.1.0 // indirect
140+ github.com/mitchellh/mapstructure v1.5.0 // indirect
141+ github.com/moricho/tparallel v0.3.2 // indirect
142+ github.com/muesli/cancelreader v0.2.2 // indirect
143+ github.com/nakabonne/nestif v0.3.1 // indirect
144+ github.com/nishanths/exhaustive v0.13.0 // indirect
145+ github.com/nishanths/predeclared v0.2.2 // indirect
146+ github.com/nunnatsa/ginkgolinter v0.24.0 // indirect
147+ github.com/pelletier/go-toml v1.9.5 // indirect
148+ github.com/pelletier/go-toml/v2 v2.4.3 // indirect
149+ github.com/prometheus/client_golang v1.12.1 // indirect
150+ github.com/prometheus/client_model v0.2.0 // indirect
151+ github.com/prometheus/common v0.32.1 // indirect
152+ github.com/prometheus/procfs v0.7.3 // indirect
153+ github.com/quasilyte/go-ruleguard v0.4.5 // indirect
154+ github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect
155+ github.com/quasilyte/gogrep v0.5.0 // indirect
156+ github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
157+ github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
158+ github.com/raeperd/recvcheck v0.3.1 // indirect
159+ github.com/rivo/uniseg v0.4.7 // indirect
160+ github.com/rogpeppe/go-internal v1.16.0 // indirect
161+ github.com/ryancurrah/gomodguard v1.4.1 // indirect
162+ github.com/ryancurrah/gomodguard/v2 v2.1.3 // indirect
163+ github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect
164+ github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
165+ github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect
166+ github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
167+ github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
168+ github.com/securego/gosec/v2 v2.29.0 // indirect
169+ github.com/sirupsen/logrus v1.10.2 // indirect
170+ github.com/sivchari/containedctx v1.0.3 // indirect
171+ github.com/sonatard/noctx v0.5.1 // indirect
172+ github.com/sourcegraph/go-diff v0.9.0 // indirect
173+ github.com/spf13/afero v1.15.0 // indirect
174+ github.com/spf13/cast v1.5.0 // indirect
175+ github.com/spf13/cobra v1.10.2 // indirect
176+ github.com/spf13/jwalterweatherman v1.1.0 // indirect
177+ github.com/spf13/pflag v1.0.10 // indirect
178+ github.com/spf13/viper v1.12.0 // indirect
179+ github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
180+ github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect
181+ github.com/stretchr/objx v0.5.3 // indirect
182+ github.com/stretchr/testify v1.12.1 // indirect
183+ github.com/subosito/gotenv v1.4.1 // indirect
184+ github.com/tetafro/godot v1.5.6 // indirect
185+ github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c // indirect
186+ github.com/timonwong/loggercheck v0.12.0 // indirect
187+ github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect
188+ github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
189+ github.com/ultraware/funlen v0.2.0 // indirect
190+ github.com/ultraware/whitespace v0.2.0 // indirect
191+ github.com/uudashr/gocognit v1.2.1 // indirect
192+ github.com/uudashr/iface v1.5.1 // indirect
193+ github.com/xen0n/gosmopolitan v1.3.0 // indirect
194+ github.com/xo/terminfo v1.0.0 // indirect
195+ github.com/yagipy/maintidx v1.0.0 // indirect
196+ github.com/yeya24/promlinter v0.3.0 // indirect
197+ github.com/ykadowak/zerologlint v0.1.5 // indirect
198+ gitlab.com/bosi/decorder v0.4.2 // indirect
199+ go-simpler.org/musttag v0.14.0 // indirect
200+ go-simpler.org/sloglint v0.12.0 // indirect
201+ go.augendre.info/arangolint v0.4.0 // indirect
202+ go.augendre.info/fatcontext v0.10.1 // indirect
203+ go.uber.org/multierr v1.10.0 // indirect
204+ go.uber.org/zap v1.27.0 // indirect
205+ go.yaml.in/yaml/v3 v3.0.5 // indirect
206+ golang.org/x/exp/typeparams v0.0.0-20260811152304-ee035b5b010f // indirect
207+ golang.org/x/mod v0.41.0 // indirect
208+ golang.org/x/sync v0.23.0 // indirect
209+ golang.org/x/sys v0.48.0 // indirect
210+ golang.org/x/text v0.41.0 // indirect
211+ golang.org/x/tools v0.50.0 // indirect
212+ google.golang.org/protobuf v1.36.12 // indirect
213+ gopkg.in/ini.v1 v1.67.0 // indirect
214+ gopkg.in/yaml.v2 v2.4.0 // indirect
215+ gopkg.in/yaml.v3 v3.0.1 // indirect
216+ honnef.co/go/tools v0.8.1 // indirect
217+ mvdan.cc/gofumpt v0.12.0 // indirect
218+ mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8 // indirect
219+)
new file mode 100644
@@ -0,0 +1,219 @@
1+module git.rickub.com/bots-garden/sidekicks/tools
2+
3+go 1.26.0
4+
5+tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint
6+
7+require (
8+ 4d63.com/gocheckcompilerdirectives v1.4.0 // indirect
9+ 4d63.com/gochecknoglobals v0.2.2 // indirect
10+ charm.land/lipgloss/v2 v2.0.6 // indirect
11+ codeberg.org/chavacava/garif v0.2.1 // indirect
12+ codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect
13+ dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
14+ dev.gaijin.team/go/exhaustruct/v5 v5.2.0 // indirect
15+ dev.gaijin.team/go/golib v0.8.1 // indirect
16+ github.com/4meepo/tagalign v1.4.4 // indirect
17+ github.com/Abirdcfly/dupword v0.1.8 // indirect
18+ github.com/AdminBenni/iota-mixing v1.0.0 // indirect
19+ github.com/AlwxSin/noinlineerr v1.0.6 // indirect
20+ github.com/Antonboom/errname v1.1.2 // indirect
21+ github.com/Antonboom/nilnil v1.1.2 // indirect
22+ github.com/Antonboom/testifylint v1.6.4 // indirect
23+ github.com/BurntSushi/toml v1.6.0 // indirect
24+ github.com/ClickHouse/clickhouse-go-linter v1.2.1 // indirect
25+ github.com/Djarvur/go-err113 v0.1.1 // indirect
26+ github.com/Masterminds/semver/v3 v3.5.0 // indirect
27+ github.com/MirrexOne/unqueryvet v1.5.4 // indirect
28+ github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
29+ github.com/alecthomas/chroma/v2 v2.27.0 // indirect
30+ github.com/alecthomas/go-check-sumtype v0.5.1-0.20260828200218-ae6904d28606 // indirect
31+ github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
32+ github.com/alexkohler/prealloc v1.1.0 // indirect
33+ github.com/alfatraining/structtag v1.0.0 // indirect
34+ github.com/alingse/asasalint v0.0.11 // indirect
35+ github.com/alingse/nilnesserr v0.2.0 // indirect
36+ github.com/ashanbrown/forbidigo/v2 v2.3.1 // indirect
37+ github.com/ashanbrown/makezero/v2 v2.2.1 // indirect
38+ github.com/beorn7/perks v1.0.1 // indirect
39+ github.com/bkielbasa/cyclop v1.2.3 // indirect
40+ github.com/blizzy78/varnamelen v0.8.0 // indirect
41+ github.com/bombsimon/wsl/v4 v4.7.0 // indirect
42+ github.com/bombsimon/wsl/v5 v5.9.0 // indirect
43+ github.com/breml/bidichk v0.3.3 // indirect
44+ github.com/breml/errchkjson v0.4.1 // indirect
45+ github.com/butuzov/ireturn v0.4.1 // indirect
46+ github.com/butuzov/mirror v1.3.3 // indirect
47+ github.com/catenacyber/perfsprint v0.10.1 // indirect
48+ github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
49+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
50+ github.com/charithe/durationcheck v0.0.11 // indirect
51+ github.com/charmbracelet/colorprofile v0.4.3 // indirect
52+ github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 // indirect
53+ github.com/charmbracelet/x/ansi v0.11.8 // indirect
54+ github.com/charmbracelet/x/term v0.2.2 // indirect
55+ github.com/charmbracelet/x/termios v0.1.1 // indirect
56+ github.com/charmbracelet/x/windows v0.2.2 // indirect
57+ github.com/ckaznocha/intrange v0.3.1 // indirect
58+ github.com/clipperhouse/displaywidth v0.11.0 // indirect
59+ github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
60+ github.com/curioswitch/go-reassign v0.3.0 // indirect
61+ github.com/daixiang0/gci v0.13.7 // indirect
62+ github.com/dave/dst v0.27.3 // indirect
63+ github.com/denis-tingaikin/go-header v0.5.0 // indirect
64+ github.com/dlclark/regexp2/v2 v2.2.1 // indirect
65+ github.com/ettle/strcase v0.2.0 // indirect
66+ github.com/fatih/color v1.19.0 // indirect
67+ github.com/fatih/structtag v1.2.0 // indirect
68+ github.com/firefart/nonamedreturns v1.0.8 // indirect
69+ github.com/fsnotify/fsnotify v1.9.0 // indirect
70+ github.com/fzipp/gocyclo v0.6.0 // indirect
71+ github.com/ghostiam/protogetter v1.0.1 // indirect
72+ github.com/go-critic/go-critic v0.15.0 // indirect
73+ github.com/go-toolsmith/astcast v1.1.0 // indirect
74+ github.com/go-toolsmith/astcopy v1.1.0 // indirect
75+ github.com/go-toolsmith/astequal v1.2.0 // indirect
76+ github.com/go-toolsmith/astfmt v1.1.0 // indirect
77+ github.com/go-toolsmith/astp v1.1.0 // indirect
78+ github.com/go-toolsmith/strparse v1.1.0 // indirect
79+ github.com/go-toolsmith/typep v1.1.0 // indirect
80+ github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
81+ github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
82+ github.com/gobwas/glob v0.2.3 // indirect
83+ github.com/godoc-lint/godoc-lint v0.11.4 // indirect
84+ github.com/gofrs/flock v0.13.1 // indirect
85+ github.com/golang/protobuf v1.5.3 // indirect
86+ github.com/golangci/asciicheck v0.5.0 // indirect
87+ github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6 // indirect
88+ github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 // indirect
89+ github.com/golangci/go-printf-func-name v0.1.1 // indirect
90+ github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792 // indirect
91+ github.com/golangci/golangci-lint/v2 v2.14.0 // indirect
92+ github.com/golangci/golines v0.15.0 // indirect
93+ github.com/golangci/misspell v0.8.0 // indirect
94+ github.com/golangci/plugin-module-register v0.1.2 // indirect
95+ github.com/golangci/revgrep v0.8.0 // indirect
96+ github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba // indirect
97+ github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect
98+ github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect
99+ github.com/google/go-cmp v0.7.0 // indirect
100+ github.com/gordonklaus/ineffassign v0.2.0 // indirect
101+ github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
102+ github.com/gostaticanalysis/comment v1.5.0 // indirect
103+ github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
104+ github.com/gostaticanalysis/nilerr v0.1.2 // indirect
105+ github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
106+ github.com/hashicorp/go-version v1.9.0 // indirect
107+ github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
108+ github.com/hashicorp/hcl v1.0.0 // indirect
109+ github.com/hexops/gotextdiff v1.0.3 // indirect
110+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
111+ github.com/jgautheron/goconst v1.11.0 // indirect
112+ github.com/jjti/go-spancheck v0.6.5 // indirect
113+ github.com/julz/importas v0.2.0 // indirect
114+ github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect
115+ github.com/kisielk/errcheck v1.20.0 // indirect
116+ github.com/kkHAIKE/contextcheck v1.1.6 // indirect
117+ github.com/kulti/thelper v0.7.1 // indirect
118+ github.com/kunwardeep/paralleltest v1.0.15 // indirect
119+ github.com/ldez/exptostd v0.4.5 // indirect
120+ github.com/ldez/gomoddirectives v0.10.0 // indirect
121+ github.com/ldez/grignotin v0.10.1 // indirect
122+ github.com/ldez/structtags v0.6.1 // indirect
123+ github.com/ldez/tagliatelle v0.8.0 // indirect
124+ github.com/ldez/usetesting v0.5.0 // indirect
125+ github.com/leonklingele/grouper v1.1.2 // indirect
126+ github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
127+ github.com/macabu/inamedparam v0.2.0 // indirect
128+ github.com/magiconair/properties v1.8.6 // indirect
129+ github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect
130+ github.com/manuelarte/funcorder v0.6.0 // indirect
131+ github.com/maratori/testableexamples v1.0.1 // indirect
132+ github.com/maratori/testpackage v1.1.2 // indirect
133+ github.com/matoous/godox v1.1.0 // indirect
134+ github.com/mattn/go-colorable v0.1.15 // indirect
135+ github.com/mattn/go-isatty v0.0.22 // indirect
136+ github.com/mattn/go-runewidth v0.0.24 // indirect
137+ github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
138+ github.com/mgechev/revive v1.17.0 // indirect
139+ github.com/mitchellh/go-homedir v1.1.0 // indirect
140+ github.com/mitchellh/mapstructure v1.5.0 // indirect
141+ github.com/moricho/tparallel v0.3.2 // indirect
142+ github.com/muesli/cancelreader v0.2.2 // indirect
143+ github.com/nakabonne/nestif v0.3.1 // indirect
144+ github.com/nishanths/exhaustive v0.13.0 // indirect
145+ github.com/nishanths/predeclared v0.2.2 // indirect
146+ github.com/nunnatsa/ginkgolinter v0.24.0 // indirect
147+ github.com/pelletier/go-toml v1.9.5 // indirect
148+ github.com/pelletier/go-toml/v2 v2.4.3 // indirect
149+ github.com/prometheus/client_golang v1.12.1 // indirect
150+ github.com/prometheus/client_model v0.2.0 // indirect
151+ github.com/prometheus/common v0.32.1 // indirect
152+ github.com/prometheus/procfs v0.7.3 // indirect
153+ github.com/quasilyte/go-ruleguard v0.4.5 // indirect
154+ github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect
155+ github.com/quasilyte/gogrep v0.5.0 // indirect
156+ github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
157+ github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
158+ github.com/raeperd/recvcheck v0.3.1 // indirect
159+ github.com/rivo/uniseg v0.4.7 // indirect
160+ github.com/rogpeppe/go-internal v1.16.0 // indirect
161+ github.com/ryancurrah/gomodguard v1.4.1 // indirect
162+ github.com/ryancurrah/gomodguard/v2 v2.1.3 // indirect
163+ github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect
164+ github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
165+ github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect
166+ github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
167+ github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
168+ github.com/securego/gosec/v2 v2.29.0 // indirect
169+ github.com/sirupsen/logrus v1.10.2 // indirect
170+ github.com/sivchari/containedctx v1.0.3 // indirect
171+ github.com/sonatard/noctx v0.5.1 // indirect
172+ github.com/sourcegraph/go-diff v0.9.0 // indirect
173+ github.com/spf13/afero v1.15.0 // indirect
174+ github.com/spf13/cast v1.5.0 // indirect
175+ github.com/spf13/cobra v1.10.2 // indirect
176+ github.com/spf13/jwalterweatherman v1.1.0 // indirect
177+ github.com/spf13/pflag v1.0.10 // indirect
178+ github.com/spf13/viper v1.12.0 // indirect
179+ github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
180+ github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect
181+ github.com/stretchr/objx v0.5.3 // indirect
182+ github.com/stretchr/testify v1.12.1 // indirect
183+ github.com/subosito/gotenv v1.4.1 // indirect
184+ github.com/tetafro/godot v1.5.6 // indirect
185+ github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c // indirect
186+ github.com/timonwong/loggercheck v0.12.0 // indirect
187+ github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect
188+ github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
189+ github.com/ultraware/funlen v0.2.0 // indirect
190+ github.com/ultraware/whitespace v0.2.0 // indirect
191+ github.com/uudashr/gocognit v1.2.1 // indirect
192+ github.com/uudashr/iface v1.5.1 // indirect
193+ github.com/xen0n/gosmopolitan v1.3.0 // indirect
194+ github.com/xo/terminfo v1.0.0 // indirect
195+ github.com/yagipy/maintidx v1.0.0 // indirect
196+ github.com/yeya24/promlinter v0.3.0 // indirect
197+ github.com/ykadowak/zerologlint v0.1.5 // indirect
198+ gitlab.com/bosi/decorder v0.4.2 // indirect
199+ go-simpler.org/musttag v0.14.0 // indirect
200+ go-simpler.org/sloglint v0.12.0 // indirect
201+ go.augendre.info/arangolint v0.4.0 // indirect
202+ go.augendre.info/fatcontext v0.10.1 // indirect
203+ go.uber.org/multierr v1.10.0 // indirect
204+ go.uber.org/zap v1.27.0 // indirect
205+ go.yaml.in/yaml/v3 v3.0.5 // indirect
206+ golang.org/x/exp/typeparams v0.0.0-20260811152304-ee035b5b010f // indirect
207+ golang.org/x/mod v0.41.0 // indirect
208+ golang.org/x/sync v0.23.0 // indirect
209+ golang.org/x/sys v0.48.0 // indirect
210+ golang.org/x/text v0.41.0 // indirect
211+ golang.org/x/tools v0.50.0 // indirect
212+ google.golang.org/protobuf v1.36.12 // indirect
213+ gopkg.in/ini.v1 v1.67.0 // indirect
214+ gopkg.in/yaml.v2 v2.4.0 // indirect
215+ gopkg.in/yaml.v3 v3.0.1 // indirect
216+ honnef.co/go/tools v0.8.1 // indirect
217+ mvdan.cc/gofumpt v0.12.0 // indirect
218+ mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8 // indirect
219+)
added tools/go.sum +926 -0
new file mode 100644
@@ -0,0 +1,926 @@
1+4d63.com/gocheckcompilerdirectives v1.4.0 h1:ZLq62rbGWVmQhiZ8kuNVIT/M09xCSTdJz9K3xOdT/CY=
2+4d63.com/gocheckcompilerdirectives v1.4.0/go.mod h1:9ZOAiMOjqC/nRwci2fcUXVHUNLG/cH6r6rhUh+jTFtQ=
3+4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
4+4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
5+charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ=
6+charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q=
7+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
8+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
9+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
10+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
11+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
12+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
13+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
14+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
15+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
16+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
17+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
18+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
19+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
20+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
21+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
22+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
23+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
24+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
25+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
26+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
27+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
28+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
29+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
30+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
31+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
32+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
33+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
34+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
35+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
36+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
37+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
38+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
39+codeberg.org/chavacava/garif v0.2.1 h1:K9oYxSlvlXrHXyW26Z4q61bTpJDo1wbvXcYKar/F/LM=
40+codeberg.org/chavacava/garif v0.2.1/go.mod h1:oHnDSmc0f9K1MeE+MQD/yjkiIB5Xsn5y3S9Dg96Xk84=
41+codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI=
42+codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8=
43+dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y=
44+dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI=
45+dev.gaijin.team/go/exhaustruct/v5 v5.2.0 h1:HZB49Fshn8qHJCi9VzYtrcnR1jbLteRHXfnQz8Adams=
46+dev.gaijin.team/go/exhaustruct/v5 v5.2.0/go.mod h1:5IshpqhgGdKIrF8lBmi3M6YKmI7T3hMzAL9nrflko88=
47+dev.gaijin.team/go/golib v0.8.1 h1:JYju4x9BSo+QD/AYeHULVDcvEhiFg8wOi6pT0IaZF5E=
48+dev.gaijin.team/go/golib v0.8.1/go.mod h1:c5fu7t1RSGMxSQgcUYO1sODbzsYnOCXJLmHeNG1Eb+0=
49+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
50+github.com/4meepo/tagalign v1.4.4 h1:052JDGj2pBORc6NkD30dDXpsUpoYszDeUwr9/5YZuVA=
51+github.com/4meepo/tagalign v1.4.4/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c=
52+github.com/Abirdcfly/dupword v0.1.8 h1:SrhcUuGsROBuChFxHALRYzyyPODWn9zwghmzPvD9Cd8=
53+github.com/Abirdcfly/dupword v0.1.8/go.mod h1:XZrhVnI7YGpsTiWZANSQaBJ4QpL/Tq5vIEdKJJAs9WI=
54+github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo=
55+github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY=
56+github.com/AlwxSin/noinlineerr v1.0.6 h1:KAvuxunTe9QxvqrFB7nZTdb/7Wzas4AvifslTnG0Ld8=
57+github.com/AlwxSin/noinlineerr v1.0.6/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc=
58+github.com/Antonboom/errname v1.1.2 h1:dxwONZJua3VB8Xh/VaCjqAcqF645sWWv7xj26zy7tdQ=
59+github.com/Antonboom/errname v1.1.2/go.mod h1:YeZIpgLMxT+SNkruGgYkLhzq/9vs3fsolTZegKaKDZI=
60+github.com/Antonboom/nilnil v1.1.2 h1:aNlFuJhaEseXe4fHO3xbjXlSeEiQVYa2lEkWD2s2hAY=
61+github.com/Antonboom/nilnil v1.1.2/go.mod h1:0ynwvphOLmAuMwTNDyBnDZmSwZoDpcFXmUHmzoHH2WA=
62+github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ=
63+github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4=
64+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
65+github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
66+github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
67+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
68+github.com/ClickHouse/clickhouse-go-linter v1.2.1 h1:zGEKIyd5YL08ieWG/LOUmlau2DxbxPVOfAeo+4Jz3ck=
69+github.com/ClickHouse/clickhouse-go-linter v1.2.1/go.mod h1:pLorS7ffPTfuUV9M0SJgfHA/h/WQPQUk2FWG9x74cQ4=
70+github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g=
71+github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k=
72+github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
73+github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
74+github.com/MirrexOne/unqueryvet v1.5.4 h1:38QOxShO7JmMWT+eCdDMbcUgGCOeJphVkzzRgyLJgsQ=
75+github.com/MirrexOne/unqueryvet v1.5.4/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU=
76+github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4=
77+github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
78+github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
79+github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
80+github.com/alecthomas/go-check-sumtype v0.5.1-0.20260828200218-ae6904d28606 h1:dXPJ+uOJLWb8rjlAuLX7JPiRJtZKw/53Ol+WxdDJ1Eo=
81+github.com/alecthomas/go-check-sumtype v0.5.1-0.20260828200218-ae6904d28606/go.mod h1:ZH31IJyNpxpStxP2ayCcPQz+3T2G/OnxIQqe4LWvSDw=
82+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
83+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
84+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
85+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
86+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
87+github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ=
88+github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q=
89+github.com/alexkohler/prealloc v1.1.0 h1:cKGRBqlXw5iyQGLYhrXrDlcHxugXpTq4tQ5c91wkf8M=
90+github.com/alexkohler/prealloc v1.1.0/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig=
91+github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc=
92+github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus=
93+github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw=
94+github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I=
95+github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w=
96+github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg=
97+github.com/ashanbrown/forbidigo/v2 v2.3.1 h1:KAZijvQ7zeIBKbhikT4jCm0TLYXC4u78bTiLh/8JROI=
98+github.com/ashanbrown/forbidigo/v2 v2.3.1/go.mod h1:2QDkLTzU6TV937eFROamXrW92M3paehdae4HCDCOZCM=
99+github.com/ashanbrown/makezero/v2 v2.2.1 h1:A7uU8dgB1PA9aelTxHMfHIQ8Qev8AB3JLxJUBUsejqM=
100+github.com/ashanbrown/makezero/v2 v2.2.1/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY=
101+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
102+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
103+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
104+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
105+github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w=
106+github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo=
107+github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=
108+github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
109+github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ=
110+github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg=
111+github.com/bombsimon/wsl/v5 v5.9.0 h1:WCrgZ7RQnZO5oEwbVTlYgBdU3wL294kR1BSWV8vTfsU=
112+github.com/bombsimon/wsl/v5 v5.9.0/go.mod h1:kjo4HiAV5FDkHC8/uzJq9mBffEEd6WT/nvN7DoMovDM=
113+github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE=
114+github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE=
115+github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg=
116+github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s=
117+github.com/butuzov/ireturn v0.4.1 h1:vWb3NO4t77iku/sjCQ/2pHTQeOmxEhjIriJqRLg1Y+I=
118+github.com/butuzov/ireturn v0.4.1/go.mod h1:q+DXKzTDV5guNuXLnIab9fKXizTn2miZHLhxH7V/GB4=
119+github.com/butuzov/mirror v1.3.3 h1:v0RsWBhfFc1RQqE/f3sHpSttKDtodFn0gFmtYyD4/hA=
120+github.com/butuzov/mirror v1.3.3/go.mod h1:h9BzzwYnTiHO0GzgvaTqIg7VSsOUhdIv51cHFFBmX1w=
121+github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ=
122+github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc=
123+github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc=
124+github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das=
125+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
126+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
127+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
128+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
129+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
130+github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk=
131+github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4=
132+github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
133+github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
134+github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 h1:rdnVWKgJpTVXKuKuJyxDJ+NFJdUaUqGvyGy61OcvlbA=
135+github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886/go.mod h1:nAw0d9PhFp1qdzi2xhQU5YOu5sVpDIHWlaW2Uz/bCro=
136+github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
137+github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
138+github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
139+github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
140+github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
141+github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
142+github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
143+github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
144+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
145+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
146+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
147+github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs=
148+github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk=
149+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
150+github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
151+github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
152+github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
153+github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
154+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
155+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
156+github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs=
157+github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88=
158+github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ=
159+github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ=
160+github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY=
161+github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc=
162+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
163+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
164+github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=
165+github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=
166+github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
167+github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
168+github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
169+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
170+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
171+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
172+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
173+github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
174+github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
175+github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
176+github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
177+github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
178+github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
179+github.com/firefart/nonamedreturns v1.0.8 h1:iB32Dl17zJl1zlVEj/WlUWgx0HiRyQ85OUw1WHa4/II=
180+github.com/firefart/nonamedreturns v1.0.8/go.mod h1:vxFNvm5AfP/8rgAKFzYmnqx0yp1HjrYsErZ9pHPTznA=
181+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
182+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
183+github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
184+github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
185+github.com/ghostiam/protogetter v1.0.1 h1:q4LLTx2KJHJpxrfO3BniUDWoFkoqWD/UlHhClBlUlLM=
186+github.com/ghostiam/protogetter v1.0.1/go.mod h1:50Fh5yZfs6SFGrd1FlmT04NcguyEOxyjNK5zGD6/EvM=
187+github.com/go-critic/go-critic v0.15.0 h1:MDUZIQKytIl+vPEc0Igw51m4uLTdi1ZGz2jatEqNs1g=
188+github.com/go-critic/go-critic v0.15.0/go.mod h1:3BPrw8nHATGm3o2n37cssOmJzpySGKCdY8JWkCzxgKQ=
189+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
190+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
191+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
192+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
193+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
194+github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
195+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
196+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
197+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
198+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
199+github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
200+github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=
201+github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=
202+github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw=
203+github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4=
204+github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ=
205+github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw=
206+github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY=
207+github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco=
208+github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4=
209+github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA=
210+github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA=
211+github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
212+github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw=
213+github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
214+github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
215+github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
216+github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
217+github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
218+github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=
219+github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
220+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
221+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
222+github.com/godoc-lint/godoc-lint v0.11.4 h1:N9zUYVJ/OkznBbrBsU/zODvua64EUplMBLQWStO3+1M=
223+github.com/godoc-lint/godoc-lint v0.11.4/go.mod h1:7usTXoxS0QYIi5usTSCPwR0w/6xLjcz450shXjLn3t4=
224+github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo=
225+github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE=
226+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
227+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
228+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
229+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
230+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
231+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
232+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
233+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
234+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
235+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
236+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
237+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
238+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
239+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
240+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
241+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
242+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
243+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
244+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
245+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
246+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
247+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
248+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
249+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
250+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
251+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
252+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
253+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
254+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
255+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
256+github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0=
257+github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ=
258+github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6 h1:fVLolA3dG6s0brGetsiDAtcnpMSwVa2LqXJEw/9RJG4=
259+github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6/go.mod h1:1xo+NFW5S+bEf2DKXhaxuzvcDN5AR6o/KMwvqC5Mkpo=
260+github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 h1:CbTB8KpqnViI6lIXxp03Oclc4VFHi3K4BWC1TacsZ+A=
261+github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=
262+github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U=
263+github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss=
264+github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792 h1:WL8YKrt3UbOBqSRU7GpP5BTtQTMWtVtj+mfPijgZeIg=
265+github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792/go.mod h1:te5hX0dW4C5r6YbXs+6ysNr8Q5UTmdIqGbb+mlFiYmA=
266+github.com/golangci/golangci-lint/v2 v2.14.0 h1:ot8QffRa4LzAAEgtvNYrVs9esxQ7xoTwoAv6uhp4ngA=
267+github.com/golangci/golangci-lint/v2 v2.14.0/go.mod h1:amHimCzNDP1GfVno3hIiNssjvqXmuW+CPZAHOdR4cIc=
268+github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0=
269+github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10=
270+github.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg=
271+github.com/golangci/misspell v0.8.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg=
272+github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg=
273+github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw=
274+github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s=
275+github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k=
276+github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba h1:lqtcnSMDuuJdu/LrKWi5RJzpSNLOJXYe/nzQutTI5kg=
277+github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba/go.mod h1:sCBNcpRmhJCtbFGz49+IM3ETTFf7QdJ30AeYCd43NKk=
278+github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM=
279+github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s=
280+github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM=
281+github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc=
282+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
283+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
284+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
285+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
286+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
287+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
288+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
289+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
290+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
291+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
292+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
293+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
294+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
295+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
296+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
297+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
298+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
299+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
300+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
301+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
302+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
303+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
304+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
305+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
306+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
307+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
308+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
309+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
310+github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs=
311+github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw=
312+github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
313+github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc=
314+github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM=
315+github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8=
316+github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc=
317+github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk=
318+github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY=
319+github.com/gostaticanalysis/nilerr v0.1.2 h1:S6nk8a9N8g062nsx63kUkF6AzbHGw7zzyHMcpu52xQU=
320+github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduwTSI4CsymaC2htPA=
321+github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=
322+github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo=
323+github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw=
324+github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
325+github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
326+github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
327+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
328+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
329+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
330+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
331+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
332+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
333+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
334+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
335+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
336+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
337+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
338+github.com/jgautheron/goconst v1.11.0 h1:KgN90z5qXt5f0Uzf3cWXev3hfMMFUyNeKdpkSBRvLDk=
339+github.com/jgautheron/goconst v1.11.0/go.mod h1:0p+wv1lFOiUr0IlNNT1nrm6+8DB8u2sU6KHGzFRXHDc=
340+github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8=
341+github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU=
342+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
343+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
344+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
345+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
346+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
347+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
348+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
349+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
350+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
351+github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ=
352+github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY=
353+github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0=
354+github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY=
355+github.com/kisielk/errcheck v1.20.0 h1:9rwHBNKzd4wkDWcROy3DvFGNqEPlkxBg305rvk7HabI=
356+github.com/kisielk/errcheck v1.20.0/go.mod h1:O+f80MKNwX8Oor2jwgpeQ9An7uJm+hRSgT+h22knRJU=
357+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
358+github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE=
359+github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
360+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
361+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
362+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
363+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
364+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
365+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
366+github.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98=
367+github.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs=
368+github.com/kunwardeep/paralleltest v1.0.15 h1:ZMk4Qt306tHIgKISHWFJAO1IDQJLc6uDyJMLyncOb6w=
369+github.com/kunwardeep/paralleltest v1.0.15/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk=
370+github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ=
371+github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM=
372+github.com/ldez/gomoddirectives v0.10.0 h1:Zb23rfMMkG21B6pZ9OfESzemByP5Qr7YvWcXQZE/S3Q=
373+github.com/ldez/gomoddirectives v0.10.0/go.mod h1:W9uh0721Vc7aaVoiMXTpKIveLmtHC9uaOSiHM4OK5a8=
374+github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o=
375+github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas=
376+github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk=
377+github.com/ldez/structtags v0.6.1/go.mod h1:YDxVSgDy/MON6ariaxLF2X09bh19qL7MtGBN5MrvbdY=
378+github.com/ldez/tagliatelle v0.8.0 h1:QczqVPa22JdT40p3m5JTTVmPMv5wDORqdomJn6/Pnn0=
379+github.com/ldez/tagliatelle v0.8.0/go.mod h1:K9n83Wv0wyVq3JYF9WI28BX8xXhDBVP3thHJ0ENuiEs=
380+github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc=
381+github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ=
382+github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY=
383+github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA=
384+github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
385+github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
386+github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE=
387+github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U=
388+github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
389+github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
390+github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww=
391+github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM=
392+github.com/manuelarte/funcorder v0.6.0 h1:0hBngc4fa1IgNiI65A7sFGkMvoMCc878RjqB5V7rWP0=
393+github.com/manuelarte/funcorder v0.6.0/go.mod h1:id3NDhXdQBmeqXH7eVC6Z89xS6JxvZ8kF9xUxpArU/g=
394+github.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8=
395+github.com/maratori/testableexamples v1.0.1/go.mod h1:XE2F/nQs7B9N08JgyRmdGjYVGqxWwClLPCGSQhXQSrQ=
396+github.com/maratori/testpackage v1.1.2 h1:ffDSh+AgqluCLMXhM19f/cpvQAKygKAJXFl9aUjmbqs=
397+github.com/maratori/testpackage v1.1.2/go.mod h1:8F24GdVDFW5Ew43Et02jamrVMNXLUNaOynhDssITGfc=
398+github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4=
399+github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs=
400+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
401+github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
402+github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
403+github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
404+github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
405+github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
406+github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
407+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
408+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
409+github.com/mgechev/revive v1.17.0 h1:hwTTVC3svSv9X2FA2hiYT17Uws+lfxDWhPxzBZZCREg=
410+github.com/mgechev/revive v1.17.0/go.mod h1:uiVNUF8ruVh4WHm/aPs7kKEeuo+m6oO3tuOfniyzP/g=
411+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
412+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
413+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
414+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
415+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
416+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
417+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
418+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
419+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
420+github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI=
421+github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U=
422+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
423+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
424+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
425+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
426+github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U=
427+github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
428+github.com/nishanths/exhaustive v0.13.0 h1:Rof0axOSNQaaPaPmLK5RxcRrJV2Vcv+vfUmuRy+2EAM=
429+github.com/nishanths/exhaustive v0.13.0/go.mod h1:3/adQjJgvD/UFHHSYBjpK9Wbz8DOvf0FBtzx+zI0Rlo=
430+github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=
431+github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
432+github.com/nunnatsa/ginkgolinter v0.24.0 h1:Mp0EagluLFP98JatP6nqp/gGEoljNG97uf9AcxcBVy8=
433+github.com/nunnatsa/ginkgolinter v0.24.0/go.mod h1:2ZMRuzX6+3XXyY6UZOwb6n+MCocVGbkIsDBC4vuWz5c=
434+github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
435+github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
436+github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
437+github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
438+github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
439+github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
440+github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
441+github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
442+github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
443+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
444+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
445+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
446+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
447+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
448+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
449+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
450+github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
451+github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk=
452+github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
453+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
454+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
455+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
456+github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
457+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
458+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
459+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
460+github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
461+github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
462+github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
463+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
464+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
465+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
466+github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
467+github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
468+github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
469+github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA=
470+github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE=
471+github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY=
472+github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
473+github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo=
474+github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng=
475+github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU=
476+github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
477+github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=
478+github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=
479+github.com/raeperd/recvcheck v0.3.1 h1:p7nZMxiCWkdUtY2i3o+pv1h4fJCR5gpvqAXr9326hEY=
480+github.com/raeperd/recvcheck v0.3.1/go.mod h1:CbE7iRocQ4uc71JwpW/CPSh4BShnkre4A9Gb508RypU=
481+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
482+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
483+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
484+github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
485+github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
486+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
487+github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g=
488+github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I=
489+github.com/ryancurrah/gomodguard/v2 v2.1.3 h1:E7sz3PJwE9Ba1reVxSpF6XLCPJZ74Kfw/LabTNM4GIA=
490+github.com/ryancurrah/gomodguard/v2 v2.1.3/go.mod h1:CQicdLGatWMxLX53JzoBjYlsNZhHbmLv2AVa0s2aivU=
491+github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg=
492+github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU=
493+github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0=
494+github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4=
495+github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
496+github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
497+github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw=
498+github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
499+github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ=
500+github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8=
501+github.com/securego/gosec/v2 v2.29.0 h1:pF2HSLcnY5voqpxQumEe0O5YAAbvF4Syu4qeIewpLyc=
502+github.com/securego/gosec/v2 v2.29.0/go.mod h1:47C7ej+L2bC1NOxteU2aCGnCi4cZaoKzRt5y9plIvuY=
503+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
504+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
505+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
506+github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
507+github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
508+github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE=
509+github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4=
510+github.com/sonatard/noctx v0.5.1 h1:wklWg9c9ZYugOAk7qG4yP4PBrlQsmSLPTvW1K4PRQMs=
511+github.com/sonatard/noctx v0.5.1/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas=
512+github.com/sourcegraph/go-diff v0.9.0 h1:7xcVsayWHhvxZGrqt1o89vvBApCa4nyCgO7tNxDHJ+Q=
513+github.com/sourcegraph/go-diff v0.9.0/go.mod h1:hWlcO7Al+UZStZAP8rBumHpCK5ZHQ5BXsMls8p4+F5E=
514+github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
515+github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
516+github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
517+github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
518+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
519+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
520+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
521+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
522+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
523+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
524+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
525+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
526+github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
527+github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
528+github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
529+github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
530+github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g=
531+github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ=
532+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
533+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
534+github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
535+github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
536+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
537+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
538+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
539+github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
540+github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
541+github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
542+github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
543+github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0=
544+github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY=
545+github.com/tetafro/godot v1.5.6 h1:IEkrFCwXaYHlOn4mGzGS3F3dkP6m9t0jpwqBFPIkKiA=
546+github.com/tetafro/godot v1.5.6/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU=
547+github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c h1:d/FUP8GD4QiY/rdFzZmBu3KqL9eBNVzMu7W/5pGkaMM=
548+github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c/go.mod h1:sDHLK7rb/59v/ZxZ7KtymgcoxuUMxjXq8gtu9VMOK8M=
549+github.com/timonwong/loggercheck v0.12.0 h1:7Cb9XGLJZSWYNjgL459DIhpSiEHupD+MLQ6vaN2Pl3g=
550+github.com/timonwong/loggercheck v0.12.0/go.mod h1:Wa6Y+SJHQR01mViZlLLY5GAZVOWOLNMbkXddMjFWQCs=
551+github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is=
552+github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo=
553+github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw=
554+github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
555+github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI=
556+github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA=
557+github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g=
558+github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8=
559+github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4=
560+github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q=
561+github.com/uudashr/iface v1.5.1 h1:BS3yrgaT55s3dAtJoxuTnsZNbsejq/mVuzp49YDyfBA=
562+github.com/uudashr/iface v1.5.1/go.mod h1:5UWoT6SvTdTww/KToRj6clO+n4Kg3/NrAQvgOB+5Ggw=
563+github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM=
564+github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4=
565+github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY=
566+github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
567+github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM=
568+github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk=
569+github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs=
570+github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4=
571+github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw=
572+github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg=
573+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
574+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
575+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
576+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
577+github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
578+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
579+gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=
580+gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8=
581+go-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo=
582+go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE=
583+go-simpler.org/sloglint v0.12.0 h1:UzWDlLWNE5FLqsvyq3tWYHuQMbqrervOhT8qPl4Mmw4=
584+go-simpler.org/sloglint v0.12.0/go.mod h1:jBjjC2bm8rYrs88oTRlFX497kWjJsyZWYoNaXkGRI6I=
585+go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50=
586+go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA=
587+go.augendre.info/fatcontext v0.10.1 h1:HCS/+eob32cM0mEtmZM6NiXdqvS5KiZmsR+fCubTpbc=
588+go.augendre.info/fatcontext v0.10.1/go.mod h1:pqpGvA9GlrXy+aXkp8L2dKz12Zp4g2FhzcAtwToU+2w=
589+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
590+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
591+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
592+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
593+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
594+go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
595+go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
596+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
597+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
598+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
599+go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
600+go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
601+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
602+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
603+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
604+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
605+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
606+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
607+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
608+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
609+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
610+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
611+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
612+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
613+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
614+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
615+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
616+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
617+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
618+golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
619+golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
620+golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
621+golang.org/x/exp/typeparams v0.0.0-20260811152304-ee035b5b010f h1:+lI8cDJ4uceLipg2f1ODay7fEuLkk0BIHXd6PB8icxo=
622+golang.org/x/exp/typeparams v0.0.0-20260811152304-ee035b5b010f/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo=
623+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
624+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
625+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
626+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
627+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
628+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
629+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
630+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
631+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
632+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
633+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
634+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
635+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
636+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
637+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
638+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
639+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
640+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
641+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
642+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
643+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
644+golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
645+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
646+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
647+golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
648+golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
649+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
650+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
651+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
652+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
653+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
654+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
655+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
656+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
657+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
658+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
659+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
660+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
661+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
662+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
663+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
664+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
665+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
666+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
667+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
668+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
669+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
670+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
671+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
672+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
673+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
674+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
675+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
676+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
677+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
678+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
679+golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
680+golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
681+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
682+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
683+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
684+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
685+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
686+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
687+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
688+golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
689+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
690+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
691+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
692+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
693+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
694+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
695+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
696+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
697+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
698+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
699+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
700+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
701+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
702+golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
703+golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
704+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
705+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
706+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
707+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
708+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
709+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
710+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
711+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
712+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
713+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
714+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
715+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
716+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
717+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
718+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
719+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
720+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
721+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
722+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
723+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
724+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
725+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
726+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
727+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
728+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
729+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
730+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
731+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
732+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
733+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
734+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
735+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
736+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
737+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
738+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
739+golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
740+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
741+golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
742+golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
743+golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
744+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
745+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
746+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
747+golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
748+golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
749+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
750+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
751+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
752+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
753+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
754+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
755+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
756+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
757+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
758+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
759+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
760+golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
761+golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
762+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
763+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
764+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
765+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
766+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
767+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
768+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
769+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
770+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
771+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
772+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
773+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
774+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
775+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
776+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
777+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
778+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
779+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
780+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
781+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
782+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
783+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
784+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
785+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
786+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
787+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
788+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
789+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
790+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
791+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
792+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
793+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
794+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
795+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
796+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
797+golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
798+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
799+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
800+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
801+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
802+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
803+golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
804+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
805+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
806+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
807+golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
808+golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
809+golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
810+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
811+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
812+golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
813+golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
814+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
815+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
816+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
817+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
818+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
819+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
820+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
821+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
822+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
823+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
824+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
825+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
826+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
827+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
828+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
829+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
830+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
831+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
832+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
833+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
834+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
835+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
836+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
837+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
838+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
839+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
840+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
841+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
842+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
843+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
844+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
845+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
846+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
847+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
848+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
849+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
850+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
851+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
852+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
853+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
854+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
855+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
856+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
857+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
858+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
859+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
860+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
861+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
862+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
863+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
864+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
865+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
866+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
867+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
868+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
869+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
870+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
871+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
872+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
873+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
874+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
875+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
876+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
877+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
878+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
879+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
880+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
881+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
882+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
883+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
884+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
885+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
886+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
887+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
888+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
889+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
890+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
891+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
892+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
893+google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
894+google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
895+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
896+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
897+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
898+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
899+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
900+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
901+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
902+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
903+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
904+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
905+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
906+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
907+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
908+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
909+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
910+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
911+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
912+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
913+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
914+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
915+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
916+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
917+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
918+honnef.co/go/tools v0.8.1 h1:+JKf3xJ1ni4CwrhVg4/pqsfPGP6vNAXcKbMXJodYx3w=
919+honnef.co/go/tools v0.8.1/go.mod h1:XA+OnlRA9EDh/ukGvXMNSZNKGwFQJ+5dER0ioUkOxks=
920+mvdan.cc/gofumpt v0.12.0 h1:1Lbudkz2kpM9Cjz2pL4M19u7q+GaEhCTNf7N9mfpcho=
921+mvdan.cc/gofumpt v0.12.0/go.mod h1:SmBHHrljiZu/uoypeKup3rFzP6eoC9UwCp2iH5E3jZA=
922+mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8 h1:Re1NRyLpiAt9kB+ImaaoapwWiQXrKwER4tY8fLOkDew=
923+mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8/go.mod h1:MrS/+zJ1M2xvGXhKktiHbNQeQPyg3Qel+KkzT+f7/i4=
924+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
925+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
926+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
new file mode 100644
@@ -0,0 +1,926 @@
1+4d63.com/gocheckcompilerdirectives v1.4.0 h1:ZLq62rbGWVmQhiZ8kuNVIT/M09xCSTdJz9K3xOdT/CY=
2+4d63.com/gocheckcompilerdirectives v1.4.0/go.mod h1:9ZOAiMOjqC/nRwci2fcUXVHUNLG/cH6r6rhUh+jTFtQ=
3+4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
4+4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
5+charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ=
6+charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q=
7+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
8+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
9+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
10+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
11+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
12+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
13+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
14+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
15+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
16+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
17+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
18+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
19+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
20+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
21+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
22+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
23+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
24+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
25+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
26+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
27+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
28+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
29+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
30+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
31+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
32+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
33+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
34+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
35+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
36+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
37+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
38+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
39+codeberg.org/chavacava/garif v0.2.1 h1:K9oYxSlvlXrHXyW26Z4q61bTpJDo1wbvXcYKar/F/LM=
40+codeberg.org/chavacava/garif v0.2.1/go.mod h1:oHnDSmc0f9K1MeE+MQD/yjkiIB5Xsn5y3S9Dg96Xk84=
41+codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI=
42+codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8=
43+dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y=
44+dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI=
45+dev.gaijin.team/go/exhaustruct/v5 v5.2.0 h1:HZB49Fshn8qHJCi9VzYtrcnR1jbLteRHXfnQz8Adams=
46+dev.gaijin.team/go/exhaustruct/v5 v5.2.0/go.mod h1:5IshpqhgGdKIrF8lBmi3M6YKmI7T3hMzAL9nrflko88=
47+dev.gaijin.team/go/golib v0.8.1 h1:JYju4x9BSo+QD/AYeHULVDcvEhiFg8wOi6pT0IaZF5E=
48+dev.gaijin.team/go/golib v0.8.1/go.mod h1:c5fu7t1RSGMxSQgcUYO1sODbzsYnOCXJLmHeNG1Eb+0=
49+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
50+github.com/4meepo/tagalign v1.4.4 h1:052JDGj2pBORc6NkD30dDXpsUpoYszDeUwr9/5YZuVA=
51+github.com/4meepo/tagalign v1.4.4/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c=
52+github.com/Abirdcfly/dupword v0.1.8 h1:SrhcUuGsROBuChFxHALRYzyyPODWn9zwghmzPvD9Cd8=
53+github.com/Abirdcfly/dupword v0.1.8/go.mod h1:XZrhVnI7YGpsTiWZANSQaBJ4QpL/Tq5vIEdKJJAs9WI=
54+github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo=
55+github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY=
56+github.com/AlwxSin/noinlineerr v1.0.6 h1:KAvuxunTe9QxvqrFB7nZTdb/7Wzas4AvifslTnG0Ld8=
57+github.com/AlwxSin/noinlineerr v1.0.6/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc=
58+github.com/Antonboom/errname v1.1.2 h1:dxwONZJua3VB8Xh/VaCjqAcqF645sWWv7xj26zy7tdQ=
59+github.com/Antonboom/errname v1.1.2/go.mod h1:YeZIpgLMxT+SNkruGgYkLhzq/9vs3fsolTZegKaKDZI=
60+github.com/Antonboom/nilnil v1.1.2 h1:aNlFuJhaEseXe4fHO3xbjXlSeEiQVYa2lEkWD2s2hAY=
61+github.com/Antonboom/nilnil v1.1.2/go.mod h1:0ynwvphOLmAuMwTNDyBnDZmSwZoDpcFXmUHmzoHH2WA=
62+github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ=
63+github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4=
64+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
65+github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
66+github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
67+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
68+github.com/ClickHouse/clickhouse-go-linter v1.2.1 h1:zGEKIyd5YL08ieWG/LOUmlau2DxbxPVOfAeo+4Jz3ck=
69+github.com/ClickHouse/clickhouse-go-linter v1.2.1/go.mod h1:pLorS7ffPTfuUV9M0SJgfHA/h/WQPQUk2FWG9x74cQ4=
70+github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g=
71+github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k=
72+github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
73+github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
74+github.com/MirrexOne/unqueryvet v1.5.4 h1:38QOxShO7JmMWT+eCdDMbcUgGCOeJphVkzzRgyLJgsQ=
75+github.com/MirrexOne/unqueryvet v1.5.4/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU=
76+github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4=
77+github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
78+github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
79+github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
80+github.com/alecthomas/go-check-sumtype v0.5.1-0.20260828200218-ae6904d28606 h1:dXPJ+uOJLWb8rjlAuLX7JPiRJtZKw/53Ol+WxdDJ1Eo=
81+github.com/alecthomas/go-check-sumtype v0.5.1-0.20260828200218-ae6904d28606/go.mod h1:ZH31IJyNpxpStxP2ayCcPQz+3T2G/OnxIQqe4LWvSDw=
82+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
83+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
84+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
85+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
86+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
87+github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ=
88+github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q=
89+github.com/alexkohler/prealloc v1.1.0 h1:cKGRBqlXw5iyQGLYhrXrDlcHxugXpTq4tQ5c91wkf8M=
90+github.com/alexkohler/prealloc v1.1.0/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig=
91+github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc=
92+github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus=
93+github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw=
94+github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I=
95+github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w=
96+github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg=
97+github.com/ashanbrown/forbidigo/v2 v2.3.1 h1:KAZijvQ7zeIBKbhikT4jCm0TLYXC4u78bTiLh/8JROI=
98+github.com/ashanbrown/forbidigo/v2 v2.3.1/go.mod h1:2QDkLTzU6TV937eFROamXrW92M3paehdae4HCDCOZCM=
99+github.com/ashanbrown/makezero/v2 v2.2.1 h1:A7uU8dgB1PA9aelTxHMfHIQ8Qev8AB3JLxJUBUsejqM=
100+github.com/ashanbrown/makezero/v2 v2.2.1/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY=
101+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
102+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
103+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
104+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
105+github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w=
106+github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo=
107+github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=
108+github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
109+github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ=
110+github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg=
111+github.com/bombsimon/wsl/v5 v5.9.0 h1:WCrgZ7RQnZO5oEwbVTlYgBdU3wL294kR1BSWV8vTfsU=
112+github.com/bombsimon/wsl/v5 v5.9.0/go.mod h1:kjo4HiAV5FDkHC8/uzJq9mBffEEd6WT/nvN7DoMovDM=
113+github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE=
114+github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE=
115+github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg=
116+github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s=
117+github.com/butuzov/ireturn v0.4.1 h1:vWb3NO4t77iku/sjCQ/2pHTQeOmxEhjIriJqRLg1Y+I=
118+github.com/butuzov/ireturn v0.4.1/go.mod h1:q+DXKzTDV5guNuXLnIab9fKXizTn2miZHLhxH7V/GB4=
119+github.com/butuzov/mirror v1.3.3 h1:v0RsWBhfFc1RQqE/f3sHpSttKDtodFn0gFmtYyD4/hA=
120+github.com/butuzov/mirror v1.3.3/go.mod h1:h9BzzwYnTiHO0GzgvaTqIg7VSsOUhdIv51cHFFBmX1w=
121+github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ=
122+github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc=
123+github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc=
124+github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das=
125+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
126+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
127+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
128+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
129+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
130+github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk=
131+github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4=
132+github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
133+github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
134+github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 h1:rdnVWKgJpTVXKuKuJyxDJ+NFJdUaUqGvyGy61OcvlbA=
135+github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886/go.mod h1:nAw0d9PhFp1qdzi2xhQU5YOu5sVpDIHWlaW2Uz/bCro=
136+github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
137+github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
138+github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
139+github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
140+github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
141+github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
142+github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
143+github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
144+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
145+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
146+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
147+github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs=
148+github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk=
149+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
150+github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
151+github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
152+github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
153+github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
154+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
155+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
156+github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs=
157+github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88=
158+github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ=
159+github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ=
160+github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY=
161+github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc=
162+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
163+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
164+github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=
165+github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=
166+github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
167+github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
168+github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
169+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
170+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
171+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
172+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
173+github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
174+github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
175+github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
176+github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
177+github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
178+github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
179+github.com/firefart/nonamedreturns v1.0.8 h1:iB32Dl17zJl1zlVEj/WlUWgx0HiRyQ85OUw1WHa4/II=
180+github.com/firefart/nonamedreturns v1.0.8/go.mod h1:vxFNvm5AfP/8rgAKFzYmnqx0yp1HjrYsErZ9pHPTznA=
181+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
182+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
183+github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
184+github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
185+github.com/ghostiam/protogetter v1.0.1 h1:q4LLTx2KJHJpxrfO3BniUDWoFkoqWD/UlHhClBlUlLM=
186+github.com/ghostiam/protogetter v1.0.1/go.mod h1:50Fh5yZfs6SFGrd1FlmT04NcguyEOxyjNK5zGD6/EvM=
187+github.com/go-critic/go-critic v0.15.0 h1:MDUZIQKytIl+vPEc0Igw51m4uLTdi1ZGz2jatEqNs1g=
188+github.com/go-critic/go-critic v0.15.0/go.mod h1:3BPrw8nHATGm3o2n37cssOmJzpySGKCdY8JWkCzxgKQ=
189+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
190+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
191+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
192+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
193+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
194+github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
195+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
196+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
197+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
198+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
199+github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
200+github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=
201+github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=
202+github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw=
203+github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4=
204+github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ=
205+github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw=
206+github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY=
207+github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco=
208+github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4=
209+github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA=
210+github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA=
211+github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
212+github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw=
213+github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
214+github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
215+github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
216+github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
217+github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
218+github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=
219+github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
220+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
221+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
222+github.com/godoc-lint/godoc-lint v0.11.4 h1:N9zUYVJ/OkznBbrBsU/zODvua64EUplMBLQWStO3+1M=
223+github.com/godoc-lint/godoc-lint v0.11.4/go.mod h1:7usTXoxS0QYIi5usTSCPwR0w/6xLjcz450shXjLn3t4=
224+github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo=
225+github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE=
226+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
227+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
228+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
229+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
230+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
231+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
232+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
233+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
234+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
235+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
236+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
237+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
238+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
239+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
240+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
241+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
242+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
243+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
244+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
245+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
246+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
247+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
248+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
249+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
250+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
251+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
252+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
253+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
254+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
255+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
256+github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0=
257+github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ=
258+github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6 h1:fVLolA3dG6s0brGetsiDAtcnpMSwVa2LqXJEw/9RJG4=
259+github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6/go.mod h1:1xo+NFW5S+bEf2DKXhaxuzvcDN5AR6o/KMwvqC5Mkpo=
260+github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 h1:CbTB8KpqnViI6lIXxp03Oclc4VFHi3K4BWC1TacsZ+A=
261+github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=
262+github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U=
263+github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss=
264+github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792 h1:WL8YKrt3UbOBqSRU7GpP5BTtQTMWtVtj+mfPijgZeIg=
265+github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792/go.mod h1:te5hX0dW4C5r6YbXs+6ysNr8Q5UTmdIqGbb+mlFiYmA=
266+github.com/golangci/golangci-lint/v2 v2.14.0 h1:ot8QffRa4LzAAEgtvNYrVs9esxQ7xoTwoAv6uhp4ngA=
267+github.com/golangci/golangci-lint/v2 v2.14.0/go.mod h1:amHimCzNDP1GfVno3hIiNssjvqXmuW+CPZAHOdR4cIc=
268+github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0=
269+github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10=
270+github.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg=
271+github.com/golangci/misspell v0.8.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg=
272+github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg=
273+github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw=
274+github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s=
275+github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k=
276+github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba h1:lqtcnSMDuuJdu/LrKWi5RJzpSNLOJXYe/nzQutTI5kg=
277+github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba/go.mod h1:sCBNcpRmhJCtbFGz49+IM3ETTFf7QdJ30AeYCd43NKk=
278+github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM=
279+github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s=
280+github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM=
281+github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc=
282+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
283+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
284+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
285+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
286+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
287+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
288+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
289+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
290+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
291+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
292+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
293+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
294+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
295+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
296+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
297+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
298+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
299+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
300+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
301+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
302+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
303+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
304+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
305+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
306+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
307+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
308+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
309+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
310+github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs=
311+github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw=
312+github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
313+github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc=
314+github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM=
315+github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8=
316+github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc=
317+github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk=
318+github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY=
319+github.com/gostaticanalysis/nilerr v0.1.2 h1:S6nk8a9N8g062nsx63kUkF6AzbHGw7zzyHMcpu52xQU=
320+github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduwTSI4CsymaC2htPA=
321+github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=
322+github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo=
323+github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw=
324+github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
325+github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
326+github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
327+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
328+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
329+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
330+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
331+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
332+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
333+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
334+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
335+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
336+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
337+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
338+github.com/jgautheron/goconst v1.11.0 h1:KgN90z5qXt5f0Uzf3cWXev3hfMMFUyNeKdpkSBRvLDk=
339+github.com/jgautheron/goconst v1.11.0/go.mod h1:0p+wv1lFOiUr0IlNNT1nrm6+8DB8u2sU6KHGzFRXHDc=
340+github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8=
341+github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU=
342+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
343+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
344+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
345+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
346+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
347+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
348+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
349+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
350+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
351+github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ=
352+github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY=
353+github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0=
354+github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY=
355+github.com/kisielk/errcheck v1.20.0 h1:9rwHBNKzd4wkDWcROy3DvFGNqEPlkxBg305rvk7HabI=
356+github.com/kisielk/errcheck v1.20.0/go.mod h1:O+f80MKNwX8Oor2jwgpeQ9An7uJm+hRSgT+h22knRJU=
357+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
358+github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE=
359+github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
360+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
361+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
362+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
363+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
364+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
365+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
366+github.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98=
367+github.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs=
368+github.com/kunwardeep/paralleltest v1.0.15 h1:ZMk4Qt306tHIgKISHWFJAO1IDQJLc6uDyJMLyncOb6w=
369+github.com/kunwardeep/paralleltest v1.0.15/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk=
370+github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ=
371+github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM=
372+github.com/ldez/gomoddirectives v0.10.0 h1:Zb23rfMMkG21B6pZ9OfESzemByP5Qr7YvWcXQZE/S3Q=
373+github.com/ldez/gomoddirectives v0.10.0/go.mod h1:W9uh0721Vc7aaVoiMXTpKIveLmtHC9uaOSiHM4OK5a8=
374+github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o=
375+github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas=
376+github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk=
377+github.com/ldez/structtags v0.6.1/go.mod h1:YDxVSgDy/MON6ariaxLF2X09bh19qL7MtGBN5MrvbdY=
378+github.com/ldez/tagliatelle v0.8.0 h1:QczqVPa22JdT40p3m5JTTVmPMv5wDORqdomJn6/Pnn0=
379+github.com/ldez/tagliatelle v0.8.0/go.mod h1:K9n83Wv0wyVq3JYF9WI28BX8xXhDBVP3thHJ0ENuiEs=
380+github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc=
381+github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ=
382+github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY=
383+github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA=
384+github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
385+github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
386+github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE=
387+github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U=
388+github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
389+github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
390+github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww=
391+github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM=
392+github.com/manuelarte/funcorder v0.6.0 h1:0hBngc4fa1IgNiI65A7sFGkMvoMCc878RjqB5V7rWP0=
393+github.com/manuelarte/funcorder v0.6.0/go.mod h1:id3NDhXdQBmeqXH7eVC6Z89xS6JxvZ8kF9xUxpArU/g=
394+github.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8=
395+github.com/maratori/testableexamples v1.0.1/go.mod h1:XE2F/nQs7B9N08JgyRmdGjYVGqxWwClLPCGSQhXQSrQ=
396+github.com/maratori/testpackage v1.1.2 h1:ffDSh+AgqluCLMXhM19f/cpvQAKygKAJXFl9aUjmbqs=
397+github.com/maratori/testpackage v1.1.2/go.mod h1:8F24GdVDFW5Ew43Et02jamrVMNXLUNaOynhDssITGfc=
398+github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4=
399+github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs=
400+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
401+github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
402+github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
403+github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
404+github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
405+github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
406+github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
407+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
408+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
409+github.com/mgechev/revive v1.17.0 h1:hwTTVC3svSv9X2FA2hiYT17Uws+lfxDWhPxzBZZCREg=
410+github.com/mgechev/revive v1.17.0/go.mod h1:uiVNUF8ruVh4WHm/aPs7kKEeuo+m6oO3tuOfniyzP/g=
411+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
412+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
413+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
414+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
415+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
416+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
417+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
418+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
419+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
420+github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI=
421+github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U=
422+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
423+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
424+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
425+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
426+github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U=
427+github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
428+github.com/nishanths/exhaustive v0.13.0 h1:Rof0axOSNQaaPaPmLK5RxcRrJV2Vcv+vfUmuRy+2EAM=
429+github.com/nishanths/exhaustive v0.13.0/go.mod h1:3/adQjJgvD/UFHHSYBjpK9Wbz8DOvf0FBtzx+zI0Rlo=
430+github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=
431+github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
432+github.com/nunnatsa/ginkgolinter v0.24.0 h1:Mp0EagluLFP98JatP6nqp/gGEoljNG97uf9AcxcBVy8=
433+github.com/nunnatsa/ginkgolinter v0.24.0/go.mod h1:2ZMRuzX6+3XXyY6UZOwb6n+MCocVGbkIsDBC4vuWz5c=
434+github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
435+github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
436+github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
437+github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
438+github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
439+github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
440+github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
441+github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
442+github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
443+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
444+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
445+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
446+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
447+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
448+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
449+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
450+github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
451+github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk=
452+github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
453+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
454+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
455+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
456+github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
457+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
458+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
459+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
460+github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
461+github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
462+github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
463+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
464+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
465+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
466+github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
467+github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
468+github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
469+github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA=
470+github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE=
471+github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY=
472+github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
473+github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo=
474+github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng=
475+github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU=
476+github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
477+github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=
478+github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=
479+github.com/raeperd/recvcheck v0.3.1 h1:p7nZMxiCWkdUtY2i3o+pv1h4fJCR5gpvqAXr9326hEY=
480+github.com/raeperd/recvcheck v0.3.1/go.mod h1:CbE7iRocQ4uc71JwpW/CPSh4BShnkre4A9Gb508RypU=
481+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
482+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
483+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
484+github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
485+github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
486+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
487+github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g=
488+github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I=
489+github.com/ryancurrah/gomodguard/v2 v2.1.3 h1:E7sz3PJwE9Ba1reVxSpF6XLCPJZ74Kfw/LabTNM4GIA=
490+github.com/ryancurrah/gomodguard/v2 v2.1.3/go.mod h1:CQicdLGatWMxLX53JzoBjYlsNZhHbmLv2AVa0s2aivU=
491+github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg=
492+github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU=
493+github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0=
494+github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4=
495+github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
496+github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
497+github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw=
498+github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
499+github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ=
500+github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8=
501+github.com/securego/gosec/v2 v2.29.0 h1:pF2HSLcnY5voqpxQumEe0O5YAAbvF4Syu4qeIewpLyc=
502+github.com/securego/gosec/v2 v2.29.0/go.mod h1:47C7ej+L2bC1NOxteU2aCGnCi4cZaoKzRt5y9plIvuY=
503+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
504+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
505+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
506+github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
507+github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
508+github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE=
509+github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4=
510+github.com/sonatard/noctx v0.5.1 h1:wklWg9c9ZYugOAk7qG4yP4PBrlQsmSLPTvW1K4PRQMs=
511+github.com/sonatard/noctx v0.5.1/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas=
512+github.com/sourcegraph/go-diff v0.9.0 h1:7xcVsayWHhvxZGrqt1o89vvBApCa4nyCgO7tNxDHJ+Q=
513+github.com/sourcegraph/go-diff v0.9.0/go.mod h1:hWlcO7Al+UZStZAP8rBumHpCK5ZHQ5BXsMls8p4+F5E=
514+github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
515+github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
516+github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
517+github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
518+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
519+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
520+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
521+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
522+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
523+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
524+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
525+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
526+github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
527+github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
528+github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
529+github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
530+github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g=
531+github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ=
532+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
533+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
534+github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
535+github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
536+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
537+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
538+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
539+github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
540+github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
541+github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
542+github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
543+github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0=
544+github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY=
545+github.com/tetafro/godot v1.5.6 h1:IEkrFCwXaYHlOn4mGzGS3F3dkP6m9t0jpwqBFPIkKiA=
546+github.com/tetafro/godot v1.5.6/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU=
547+github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c h1:d/FUP8GD4QiY/rdFzZmBu3KqL9eBNVzMu7W/5pGkaMM=
548+github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c/go.mod h1:sDHLK7rb/59v/ZxZ7KtymgcoxuUMxjXq8gtu9VMOK8M=
549+github.com/timonwong/loggercheck v0.12.0 h1:7Cb9XGLJZSWYNjgL459DIhpSiEHupD+MLQ6vaN2Pl3g=
550+github.com/timonwong/loggercheck v0.12.0/go.mod h1:Wa6Y+SJHQR01mViZlLLY5GAZVOWOLNMbkXddMjFWQCs=
551+github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is=
552+github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo=
553+github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw=
554+github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
555+github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI=
556+github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA=
557+github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g=
558+github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8=
559+github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4=
560+github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q=
561+github.com/uudashr/iface v1.5.1 h1:BS3yrgaT55s3dAtJoxuTnsZNbsejq/mVuzp49YDyfBA=
562+github.com/uudashr/iface v1.5.1/go.mod h1:5UWoT6SvTdTww/KToRj6clO+n4Kg3/NrAQvgOB+5Ggw=
563+github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM=
564+github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4=
565+github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY=
566+github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
567+github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM=
568+github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk=
569+github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs=
570+github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4=
571+github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw=
572+github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg=
573+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
574+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
575+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
576+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
577+github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
578+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
579+gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=
580+gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8=
581+go-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo=
582+go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE=
583+go-simpler.org/sloglint v0.12.0 h1:UzWDlLWNE5FLqsvyq3tWYHuQMbqrervOhT8qPl4Mmw4=
584+go-simpler.org/sloglint v0.12.0/go.mod h1:jBjjC2bm8rYrs88oTRlFX497kWjJsyZWYoNaXkGRI6I=
585+go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50=
586+go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA=
587+go.augendre.info/fatcontext v0.10.1 h1:HCS/+eob32cM0mEtmZM6NiXdqvS5KiZmsR+fCubTpbc=
588+go.augendre.info/fatcontext v0.10.1/go.mod h1:pqpGvA9GlrXy+aXkp8L2dKz12Zp4g2FhzcAtwToU+2w=
589+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
590+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
591+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
592+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
593+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
594+go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
595+go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
596+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
597+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
598+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
599+go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
600+go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
601+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
602+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
603+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
604+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
605+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
606+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
607+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
608+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
609+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
610+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
611+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
612+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
613+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
614+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
615+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
616+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
617+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
618+golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
619+golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
620+golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
621+golang.org/x/exp/typeparams v0.0.0-20260811152304-ee035b5b010f h1:+lI8cDJ4uceLipg2f1ODay7fEuLkk0BIHXd6PB8icxo=
622+golang.org/x/exp/typeparams v0.0.0-20260811152304-ee035b5b010f/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo=
623+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
624+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
625+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
626+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
627+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
628+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
629+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
630+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
631+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
632+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
633+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
634+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
635+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
636+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
637+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
638+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
639+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
640+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
641+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
642+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
643+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
644+golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
645+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
646+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
647+golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
648+golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
649+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
650+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
651+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
652+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
653+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
654+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
655+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
656+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
657+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
658+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
659+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
660+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
661+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
662+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
663+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
664+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
665+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
666+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
667+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
668+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
669+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
670+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
671+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
672+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
673+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
674+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
675+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
676+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
677+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
678+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
679+golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
680+golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
681+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
682+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
683+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
684+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
685+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
686+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
687+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
688+golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
689+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
690+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
691+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
692+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
693+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
694+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
695+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
696+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
697+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
698+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
699+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
700+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
701+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
702+golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
703+golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
704+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
705+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
706+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
707+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
708+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
709+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
710+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
711+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
712+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
713+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
714+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
715+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
716+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
717+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
718+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
719+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
720+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
721+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
722+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
723+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
724+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
725+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
726+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
727+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
728+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
729+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
730+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
731+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
732+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
733+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
734+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
735+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
736+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
737+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
738+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
739+golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
740+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
741+golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
742+golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
743+golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
744+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
745+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
746+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
747+golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
748+golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
749+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
750+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
751+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
752+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
753+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
754+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
755+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
756+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
757+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
758+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
759+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
760+golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
761+golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
762+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
763+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
764+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
765+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
766+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
767+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
768+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
769+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
770+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
771+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
772+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
773+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
774+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
775+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
776+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
777+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
778+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
779+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
780+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
781+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
782+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
783+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
784+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
785+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
786+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
787+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
788+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
789+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
790+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
791+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
792+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
793+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
794+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
795+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
796+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
797+golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
798+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
799+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
800+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
801+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
802+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
803+golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
804+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
805+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
806+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
807+golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
808+golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
809+golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
810+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
811+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
812+golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
813+golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
814+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
815+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
816+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
817+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
818+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
819+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
820+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
821+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
822+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
823+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
824+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
825+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
826+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
827+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
828+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
829+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
830+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
831+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
832+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
833+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
834+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
835+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
836+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
837+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
838+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
839+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
840+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
841+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
842+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
843+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
844+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
845+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
846+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
847+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
848+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
849+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
850+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
851+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
852+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
853+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
854+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
855+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
856+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
857+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
858+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
859+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
860+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
861+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
862+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
863+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
864+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
865+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
866+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
867+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
868+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
869+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
870+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
871+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
872+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
873+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
874+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
875+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
876+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
877+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
878+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
879+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
880+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
881+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
882+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
883+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
884+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
885+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
886+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
887+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
888+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
889+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
890+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
891+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
892+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
893+google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
894+google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
895+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
896+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
897+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
898+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
899+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
900+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
901+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
902+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
903+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
904+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
905+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
906+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
907+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
908+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
909+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
910+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
911+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
912+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
913+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
914+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
915+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
916+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
917+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
918+honnef.co/go/tools v0.8.1 h1:+JKf3xJ1ni4CwrhVg4/pqsfPGP6vNAXcKbMXJodYx3w=
919+honnef.co/go/tools v0.8.1/go.mod h1:XA+OnlRA9EDh/ukGvXMNSZNKGwFQJ+5dER0ioUkOxks=
920+mvdan.cc/gofumpt v0.12.0 h1:1Lbudkz2kpM9Cjz2pL4M19u7q+GaEhCTNf7N9mfpcho=
921+mvdan.cc/gofumpt v0.12.0/go.mod h1:SmBHHrljiZu/uoypeKup3rFzP6eoC9UwCp2iH5E3jZA=
922+mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8 h1:Re1NRyLpiAt9kB+ImaaoapwWiQXrKwER4tY8fLOkDew=
923+mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8/go.mod h1:MrS/+zJ1M2xvGXhKktiHbNQeQPyg3Qel+KkzT+f7/i4=
924+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
925+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
926+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=