🛟 Updated.
b34befd parent: b625b0f modified
.claude/skills/drawio-fancy-diagram/generator/examples/sidekicks_packages.py +29 -26 | @@ -1,7 +1,9 @@ | ||
| 1 | 1 | """The description of docs/diagrams/packages.{drawio,svg} (Sidekicks, ADR 0003). |
| 2 | 2 | |
| 3 | 3 | 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). | |
| 5 | 7 | """ |
| 6 | 8 | import os |
| 7 | 9 | import sys |
| @@ -12,41 +14,42 @@ from flowgen import Diagram | ||
| 12 | 14 | OUT = sys.argv[1] if len(sys.argv) > 1 else "docs/diagrams" |
| 13 | 15 | |
| 14 | 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 | 19 | # 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"]) | |
| 21 | 23 | d.edge([(290, 157), (475, 157)], "bindings · events", at=(382, 157), lw=130, both=True) |
| 22 | 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. | |
| 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"]) | |
| 29 | 31 | for cx in (165, 455, 745, 1035): |
| 30 | 32 | d.edge([(600, 215), (600, 255), (cx, 255), (cx, 300)]) |
| 31 | 33 | d.edge([(290, 355), (330, 355)]) |
| 32 | 34 | d.edge([(580, 355), (620, 355)]) |
| 33 | 35 | |
| 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) | |
| 41 | 45 | |
| 42 | 46 | # 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) | |
| 52 | 55 | 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/diagrams | 3 | 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). | ||
| 5 | """ | 7 | """ |
| 6 | import os | 8 | import os |
| 7 | import sys | 9 | 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 @@ | ||
| 2 | 2 | .run/ |
| 3 | 3 | # macOS |
| 4 | 4 | .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 | # macOS | 3 | # macOS |
| 4 | .DS_Store | 4 | .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 | ||
| 10 | 10 | |
| 11 | 11 | ## 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 | 14 | 2. Owner: decide whether to run the qlty gate (needs a sandbox with the dev-toolkit kit) before M1. |
| 15 | 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 | |
| @@ -10,7 +10,7 @@ Initialize is implemented on branch `feature/initialize` (not committed by the a | |||
| 10 | 10 | ||
| 11 | ## Next steps | 11 | ## 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 @@ | ||
| 34 | 34 | |
| 35 | 35 | - **Goal**: after the owner's successful macOS check, close #3 and the milestone. |
| 36 | 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. | |
| @@ -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 @@ | ||
| 4 | 4 | |
| 5 | 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 | 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`. | |
| 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. | |
| 18 | 17 | |
| 19 | 18 | ## Decisions in force |
| 20 | 19 | |
| 21 | 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 | 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 | 26 | - Config format v1: `docs/en/reference/configuration.md`; unknown keys are errors; cross-entry rules belong to the loader (#9). |
| 25 | 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 | 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) | | |
| 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` | | |
| 38 | 41 | |
| 39 | 42 | ## Facts about the test agents (mm v1.0.3) |
| 40 | 43 | |
| @@ -45,5 +48,7 @@ What exists: | ||
| 45 | 48 | ## Not yet established |
| 46 | 49 | |
| 47 | 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 | 54 | - 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 force | 18 | ## 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 established | 48 | ## 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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 8 |
| 3 | 3 | title: "Initialiser le projet Go + Wails" |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-29T07:20:06.000Z |
| 9 | -updatedAt: 2026-10-29T08:51:00.000Z | |
| 9 | +updatedAt: 2026-10-29T10:19:30.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - "kind::chore" |
| 12 | 12 | - "area::infra" |
| @@ -38,68 +38,84 @@ body: | | ||
| 38 | 38 | tasks: |
| 39 | 39 | - id: 1 |
| 40 | 40 | title: "Générer le projet Wails avec le template choisi" |
| 41 | - state: open | |
| 41 | + state: closed | |
| 42 | 42 | priority: high |
| 43 | 43 | author: |
| 44 | 44 | name: k33g |
| 45 | 45 | email: ph.charriere@gmail.com |
| 46 | 46 | createdAt: 2026-10-29T07:20:06.000Z |
| 47 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 48 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 47 | 49 | - id: 2 |
| 48 | 50 | title: "Arborescence cmd/ internal/ frontend/" |
| 49 | - state: open | |
| 51 | + state: closed | |
| 50 | 52 | priority: medium |
| 51 | 53 | author: |
| 52 | 54 | name: k33g |
| 53 | 55 | email: ph.charriere@gmail.com |
| 54 | 56 | createdAt: 2026-10-29T07:20:06.000Z |
| 57 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 58 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 55 | 59 | - id: 3 |
| 56 | 60 | title: "Taskfile : dev, build, test, lint" |
| 57 | - state: open | |
| 61 | + state: closed | |
| 58 | 62 | priority: medium |
| 59 | 63 | author: |
| 60 | 64 | name: k33g |
| 61 | 65 | email: ph.charriere@gmail.com |
| 62 | 66 | createdAt: 2026-10-29T07:20:06.000Z |
| 67 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 68 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 63 | 69 | - id: 4 |
| 64 | 70 | title: "Lint Go et frontend" |
| 65 | - state: open | |
| 71 | + state: closed | |
| 66 | 72 | priority: medium |
| 67 | 73 | author: |
| 68 | 74 | name: k33g |
| 69 | 75 | email: ph.charriere@gmail.com |
| 70 | 76 | createdAt: 2026-10-29T07:20:06.000Z |
| 77 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 78 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 71 | 79 | - id: 5 |
| 72 | 80 | title: "CI ou hook pre-push" |
| 73 | - state: open | |
| 81 | + state: closed | |
| 74 | 82 | priority: medium |
| 75 | 83 | author: |
| 76 | 84 | name: k33g |
| 77 | 85 | email: ph.charriere@gmail.com |
| 78 | 86 | createdAt: 2026-10-29T07:20:06.000Z |
| 87 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 88 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 79 | 89 | - id: 6 |
| 80 | 90 | title: "README développeur" |
| 81 | - state: open | |
| 91 | + state: closed | |
| 82 | 92 | priority: medium |
| 83 | 93 | author: |
| 84 | 94 | name: k33g |
| 85 | 95 | email: ph.charriere@gmail.com |
| 86 | 96 | createdAt: 2026-10-29T07:20:06.000Z |
| 97 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 98 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 87 | 99 | - id: 7 |
| 88 | 100 | title: "Ajouter l'en-tête de licence aux fichiers sources et aux templates" |
| 89 | - state: open | |
| 101 | + state: closed | |
| 90 | 102 | priority: high |
| 91 | 103 | author: |
| 92 | 104 | name: k33g |
| 93 | 105 | email: ph.charriere@gmail.com |
| 94 | 106 | createdAt: 2026-10-29T08:00:00.000Z |
| 107 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 108 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 95 | 109 | - id: 8 |
| 96 | 110 | title: "Vérifier la présence de l'en-tête dans task lint" |
| 97 | - state: open | |
| 111 | + state: closed | |
| 98 | 112 | priority: medium |
| 99 | 113 | author: |
| 100 | 114 | name: k33g |
| 101 | 115 | email: ph.charriere@gmail.com |
| 102 | 116 | createdAt: 2026-10-29T08:00:00.000Z |
| 117 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 118 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 103 | 119 | comments: |
| 104 | 120 | - id: 1 |
| 105 | 121 | author: |
| @@ -138,3 +154,17 @@ comments: | ||
| 138 | 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 | 155 | - Le `Makefile` est le point d'entrée unique (`make test`) ; ses cibles délèguent à la Taskfile de Wails (ADR 0002). |
| 140 | 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 ». | |
| @@ -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: 8 | 2 | id: 8 |
| 3 | title: "Initialiser le projet Go + Wails" | 3 | title: "Initialiser le projet Go + Wails" |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-29T07:20:06.000Z | 8 | createdAt: 2026-10-29T07:20:06.000Z |
| 9 | -updatedAt: 2026-10-29T08:51:00.000Z | 9 | +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: 1 | 39 | - 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: open | 41 | + state: closed |
| 42 | priority: high | 42 | priority: high |
| 43 | author: | 43 | author: |
| 44 | name: k33g | 44 | name: k33g |
| 45 | email: ph.charriere@gmail.com | 45 | email: ph.charriere@gmail.com |
| 46 | createdAt: 2026-10-29T07:20:06.000Z | 46 | 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: 2 | 49 | - id: 2 |
| 48 | title: "Arborescence cmd/ internal/ frontend/" | 50 | title: "Arborescence cmd/ internal/ frontend/" |
| 49 | - state: open | 51 | + state: closed |
| 50 | priority: medium | 52 | priority: medium |
| 51 | author: | 53 | author: |
| 52 | name: k33g | 54 | name: k33g |
| 53 | email: ph.charriere@gmail.com | 55 | email: ph.charriere@gmail.com |
| 54 | createdAt: 2026-10-29T07:20:06.000Z | 56 | 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: 3 | 59 | - id: 3 |
| 56 | title: "Taskfile : dev, build, test, lint" | 60 | title: "Taskfile : dev, build, test, lint" |
| 57 | - state: open | 61 | + state: closed |
| 58 | priority: medium | 62 | priority: medium |
| 59 | author: | 63 | author: |
| 60 | name: k33g | 64 | name: k33g |
| 61 | email: ph.charriere@gmail.com | 65 | email: ph.charriere@gmail.com |
| 62 | createdAt: 2026-10-29T07:20:06.000Z | 66 | 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: 4 | 69 | - id: 4 |
| 64 | title: "Lint Go et frontend" | 70 | title: "Lint Go et frontend" |
| 65 | - state: open | 71 | + state: closed |
| 66 | priority: medium | 72 | priority: medium |
| 67 | author: | 73 | author: |
| 68 | name: k33g | 74 | name: k33g |
| 69 | email: ph.charriere@gmail.com | 75 | email: ph.charriere@gmail.com |
| 70 | createdAt: 2026-10-29T07:20:06.000Z | 76 | 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: 5 | 79 | - id: 5 |
| 72 | title: "CI ou hook pre-push" | 80 | title: "CI ou hook pre-push" |
| 73 | - state: open | 81 | + state: closed |
| 74 | priority: medium | 82 | priority: medium |
| 75 | author: | 83 | author: |
| 76 | name: k33g | 84 | name: k33g |
| 77 | email: ph.charriere@gmail.com | 85 | email: ph.charriere@gmail.com |
| 78 | createdAt: 2026-10-29T07:20:06.000Z | 86 | 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: 6 | 89 | - id: 6 |
| 80 | title: "README développeur" | 90 | title: "README développeur" |
| 81 | - state: open | 91 | + state: closed |
| 82 | priority: medium | 92 | priority: medium |
| 83 | author: | 93 | author: |
| 84 | name: k33g | 94 | name: k33g |
| 85 | email: ph.charriere@gmail.com | 95 | email: ph.charriere@gmail.com |
| 86 | createdAt: 2026-10-29T07:20:06.000Z | 96 | 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: 7 | 99 | - 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: open | 101 | + state: closed |
| 90 | priority: high | 102 | priority: high |
| 91 | author: | 103 | author: |
| 92 | name: k33g | 104 | name: k33g |
| 93 | email: ph.charriere@gmail.com | 105 | email: ph.charriere@gmail.com |
| 94 | createdAt: 2026-10-29T08:00:00.000Z | 106 | 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: 8 | 109 | - 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: open | 111 | + state: closed |
| 98 | priority: medium | 112 | priority: medium |
| 99 | author: | 113 | author: |
| 100 | name: k33g | 114 | name: k33g |
| 101 | email: ph.charriere@gmail.com | 115 | email: ph.charriere@gmail.com |
| 102 | createdAt: 2026-10-29T08:00:00.000Z | 116 | 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: 1 | 120 | - 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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 9 |
| 3 | 3 | title: "Charger, valider et sauvegarder la configuration YAML" |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-29T07:20:07.000Z |
| 9 | -updatedAt: 2026-10-29T08:51:00.000Z | |
| 9 | +updatedAt: 2026-10-29T10:19:30.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - "kind::feature" |
| 12 | 12 | - "area::config" |
| @@ -35,60 +35,74 @@ body: | | ||
| 35 | 35 | tasks: |
| 36 | 36 | - id: 1 |
| 37 | 37 | title: "Structs Go et parsing (gopkg.in/yaml.v3)" |
| 38 | - state: open | |
| 38 | + state: closed | |
| 39 | 39 | priority: high |
| 40 | 40 | author: |
| 41 | 41 | name: k33g |
| 42 | 42 | email: ph.charriere@gmail.com |
| 43 | 43 | createdAt: 2026-10-29T07:20:07.000Z |
| 44 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 45 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 44 | 46 | - id: 2 |
| 45 | 47 | title: "Validation avec messages lisibles et numéros de ligne" |
| 46 | - state: open | |
| 48 | + state: closed | |
| 47 | 49 | priority: high |
| 48 | 50 | author: |
| 49 | 51 | name: k33g |
| 50 | 52 | email: ph.charriere@gmail.com |
| 51 | 53 | createdAt: 2026-10-29T07:20:07.000Z |
| 54 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 55 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 52 | 56 | - id: 3 |
| 53 | 57 | title: "Création d'un fichier par défaut commenté" |
| 54 | - state: open | |
| 58 | + state: closed | |
| 55 | 59 | priority: medium |
| 56 | 60 | author: |
| 57 | 61 | name: k33g |
| 58 | 62 | email: ph.charriere@gmail.com |
| 59 | 63 | createdAt: 2026-10-29T07:20:07.000Z |
| 64 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 65 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 60 | 66 | - id: 4 |
| 61 | 67 | title: "Résolution de l'emplacement (défaut, flag, env)" |
| 62 | - state: open | |
| 68 | + state: closed | |
| 63 | 69 | priority: medium |
| 64 | 70 | author: |
| 65 | 71 | name: k33g |
| 66 | 72 | email: ph.charriere@gmail.com |
| 67 | 73 | createdAt: 2026-10-29T07:20:07.000Z |
| 74 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 75 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 68 | 76 | - id: 5 |
| 69 | 77 | title: "Watch du fichier et événement de rechargement" |
| 70 | - state: open | |
| 78 | + state: closed | |
| 71 | 79 | priority: medium |
| 72 | 80 | author: |
| 73 | 81 | name: k33g |
| 74 | 82 | email: ph.charriere@gmail.com |
| 75 | 83 | createdAt: 2026-10-29T07:20:07.000Z |
| 84 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 85 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 76 | 86 | - id: 6 |
| 77 | 87 | title: "Sauvegarde préservant les commentaires" |
| 78 | - state: open | |
| 88 | + state: closed | |
| 79 | 89 | priority: medium |
| 80 | 90 | author: |
| 81 | 91 | name: k33g |
| 82 | 92 | email: ph.charriere@gmail.com |
| 83 | 93 | createdAt: 2026-10-29T07:20:07.000Z |
| 94 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 95 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 84 | 96 | - id: 7 |
| 85 | 97 | title: "Tests unitaires" |
| 86 | - state: open | |
| 98 | + state: closed | |
| 87 | 99 | priority: medium |
| 88 | 100 | author: |
| 89 | 101 | name: k33g |
| 90 | 102 | email: ph.charriere@gmail.com |
| 91 | 103 | createdAt: 2026-10-29T07:20:07.000Z |
| 104 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 105 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 92 | 106 | comments: |
| 93 | 107 | - id: 1 |
| 94 | 108 | author: |
| @@ -97,3 +111,18 @@ comments: | ||
| 97 | 111 | createdAt: 2026-10-29T08:51:00.000Z |
| 98 | 112 | body: | |
| 99 | 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. | |
| @@ -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: 9 | 2 | id: 9 |
| 3 | title: "Charger, valider et sauvegarder la configuration YAML" | 3 | title: "Charger, valider et sauvegarder la configuration YAML" |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-29T07:20:07.000Z | 8 | createdAt: 2026-10-29T07:20:07.000Z |
| 9 | -updatedAt: 2026-10-29T08:51:00.000Z | 9 | +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: 1 | 36 | - id: 1 |
| 37 | title: "Structs Go et parsing (gopkg.in/yaml.v3)" | 37 | title: "Structs Go et parsing (gopkg.in/yaml.v3)" |
| 38 | - state: open | 38 | + state: closed |
| 39 | priority: high | 39 | priority: high |
| 40 | author: | 40 | author: |
| 41 | name: k33g | 41 | name: k33g |
| 42 | email: ph.charriere@gmail.com | 42 | email: ph.charriere@gmail.com |
| 43 | createdAt: 2026-10-29T07:20:07.000Z | 43 | 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: 2 | 46 | - 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: open | 48 | + state: closed |
| 47 | priority: high | 49 | priority: high |
| 48 | author: | 50 | author: |
| 49 | name: k33g | 51 | name: k33g |
| 50 | email: ph.charriere@gmail.com | 52 | email: ph.charriere@gmail.com |
| 51 | createdAt: 2026-10-29T07:20:07.000Z | 53 | 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: 3 | 56 | - 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: open | 58 | + state: closed |
| 55 | priority: medium | 59 | priority: medium |
| 56 | author: | 60 | author: |
| 57 | name: k33g | 61 | name: k33g |
| 58 | email: ph.charriere@gmail.com | 62 | email: ph.charriere@gmail.com |
| 59 | createdAt: 2026-10-29T07:20:07.000Z | 63 | 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: 4 | 66 | - 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: open | 68 | + state: closed |
| 63 | priority: medium | 69 | priority: medium |
| 64 | author: | 70 | author: |
| 65 | name: k33g | 71 | name: k33g |
| 66 | email: ph.charriere@gmail.com | 72 | email: ph.charriere@gmail.com |
| 67 | createdAt: 2026-10-29T07:20:07.000Z | 73 | 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: 5 | 76 | - id: 5 |
| 69 | title: "Watch du fichier et événement de rechargement" | 77 | title: "Watch du fichier et événement de rechargement" |
| 70 | - state: open | 78 | + state: closed |
| 71 | priority: medium | 79 | priority: medium |
| 72 | author: | 80 | author: |
| 73 | name: k33g | 81 | name: k33g |
| 74 | email: ph.charriere@gmail.com | 82 | email: ph.charriere@gmail.com |
| 75 | createdAt: 2026-10-29T07:20:07.000Z | 83 | 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: 6 | 86 | - id: 6 |
| 77 | title: "Sauvegarde préservant les commentaires" | 87 | title: "Sauvegarde préservant les commentaires" |
| 78 | - state: open | 88 | + state: closed |
| 79 | priority: medium | 89 | priority: medium |
| 80 | author: | 90 | author: |
| 81 | name: k33g | 91 | name: k33g |
| 82 | email: ph.charriere@gmail.com | 92 | email: ph.charriere@gmail.com |
| 83 | createdAt: 2026-10-29T07:20:07.000Z | 93 | 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: 7 | 96 | - id: 7 |
| 85 | title: "Tests unitaires" | 97 | title: "Tests unitaires" |
| 86 | - state: open | 98 | + state: closed |
| 87 | priority: medium | 99 | priority: medium |
| 88 | author: | 100 | author: |
| 89 | name: k33g | 101 | name: k33g |
| 90 | email: ph.charriere@gmail.com | 102 | email: ph.charriere@gmail.com |
| 91 | createdAt: 2026-10-29T07:20:07.000Z | 103 | 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: 1 | 107 | - id: 1 |
| 94 | author: | 108 | author: |
| @@ -97,3 +111,18 @@ comments: | |||
| 97 | createdAt: 2026-10-29T08:51:00.000Z | 111 | 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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 10 |
| 3 | 3 | title: "Client ACP : transport stdio et handshake initialize" |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-29T07:20:08.000Z |
| 9 | -updatedAt: 2026-10-29T08:51:00.000Z | |
| 9 | +updatedAt: 2026-10-29T10:19:30.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - "kind::feature" |
| 12 | 12 | - "area::acp" |
| @@ -40,68 +40,84 @@ body: | | ||
| 40 | 40 | tasks: |
| 41 | 41 | - id: 1 |
| 42 | 42 | title: "Spawn du processus et pipes stdio" |
| 43 | - state: open | |
| 43 | + state: closed | |
| 44 | 44 | priority: high |
| 45 | 45 | author: |
| 46 | 46 | name: k33g |
| 47 | 47 | email: ph.charriere@gmail.com |
| 48 | 48 | createdAt: 2026-10-29T07:20:08.000Z |
| 49 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 50 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 49 | 51 | - id: 2 |
| 50 | 52 | title: "Codec JSON-RPC (corrélation des ids, notifications, requêtes entrantes)" |
| 51 | - state: open | |
| 53 | + state: closed | |
| 52 | 54 | priority: high |
| 53 | 55 | author: |
| 54 | 56 | name: k33g |
| 55 | 57 | email: ph.charriere@gmail.com |
| 56 | 58 | createdAt: 2026-10-29T07:20:08.000Z |
| 59 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 60 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 57 | 61 | - id: 3 |
| 58 | 62 | title: "Méthode initialize et stockage des capacités" |
| 59 | - state: open | |
| 63 | + state: closed | |
| 60 | 64 | priority: high |
| 61 | 65 | author: |
| 62 | 66 | name: k33g |
| 63 | 67 | email: ph.charriere@gmail.com |
| 64 | 68 | createdAt: 2026-10-29T07:20:08.000Z |
| 69 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 70 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 65 | 71 | - id: 4 |
| 66 | 72 | title: "Capture de stderr dans un log par agent" |
| 67 | - state: open | |
| 73 | + state: closed | |
| 68 | 74 | priority: medium |
| 69 | 75 | author: |
| 70 | 76 | name: k33g |
| 71 | 77 | email: ph.charriere@gmail.com |
| 72 | 78 | createdAt: 2026-10-29T07:20:08.000Z |
| 79 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 80 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 73 | 81 | - id: 5 |
| 74 | 82 | title: "Arrêt propre et gestion des crashs" |
| 75 | - state: open | |
| 83 | + state: closed | |
| 76 | 84 | priority: medium |
| 77 | 85 | author: |
| 78 | 86 | name: k33g |
| 79 | 87 | email: ph.charriere@gmail.com |
| 80 | 88 | createdAt: 2026-10-29T07:20:08.000Z |
| 89 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 90 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 81 | 91 | - id: 6 |
| 82 | 92 | title: "Faux agent pour les tests unitaires" |
| 83 | - state: open | |
| 93 | + state: closed | |
| 84 | 94 | priority: medium |
| 85 | 95 | author: |
| 86 | 96 | name: k33g |
| 87 | 97 | email: ph.charriere@gmail.com |
| 88 | 98 | createdAt: 2026-10-29T07:20:08.000Z |
| 99 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 100 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 89 | 101 | - id: 7 |
| 90 | 102 | title: "Test d'intégration avec Alice" |
| 91 | - state: open | |
| 103 | + state: closed | |
| 92 | 104 | priority: medium |
| 93 | 105 | author: |
| 94 | 106 | name: k33g |
| 95 | 107 | email: ph.charriere@gmail.com |
| 96 | 108 | createdAt: 2026-10-29T07:20:08.000Z |
| 109 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 110 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 97 | 111 | - id: 8 |
| 98 | 112 | title: "Build tag realagents pour les tests d'intégration (repris de #7)" |
| 99 | - state: open | |
| 113 | + state: closed | |
| 100 | 114 | priority: high |
| 101 | 115 | author: |
| 102 | 116 | name: k33g |
| 103 | 117 | email: ph.charriere@gmail.com |
| 104 | 118 | createdAt: 2026-10-29T08:51:00.000Z |
| 119 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 120 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 105 | 121 | comments: |
| 106 | 122 | - id: 1 |
| 107 | 123 | author: |
| @@ -110,3 +126,14 @@ comments: | ||
| 110 | 126 | createdAt: 2026-10-29T08:51:00.000Z |
| 111 | 127 | body: | |
| 112 | 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`). | |
| @@ -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: 10 | 2 | id: 10 |
| 3 | title: "Client ACP : transport stdio et handshake initialize" | 3 | title: "Client ACP : transport stdio et handshake initialize" |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-29T07:20:08.000Z | 8 | createdAt: 2026-10-29T07:20:08.000Z |
| 9 | -updatedAt: 2026-10-29T08:51:00.000Z | 9 | +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: 1 | 41 | - id: 1 |
| 42 | title: "Spawn du processus et pipes stdio" | 42 | title: "Spawn du processus et pipes stdio" |
| 43 | - state: open | 43 | + state: closed |
| 44 | priority: high | 44 | priority: high |
| 45 | author: | 45 | author: |
| 46 | name: k33g | 46 | name: k33g |
| 47 | email: ph.charriere@gmail.com | 47 | email: ph.charriere@gmail.com |
| 48 | createdAt: 2026-10-29T07:20:08.000Z | 48 | 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: 2 | 51 | - 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: open | 53 | + state: closed |
| 52 | priority: high | 54 | priority: high |
| 53 | author: | 55 | author: |
| 54 | name: k33g | 56 | name: k33g |
| 55 | email: ph.charriere@gmail.com | 57 | email: ph.charriere@gmail.com |
| 56 | createdAt: 2026-10-29T07:20:08.000Z | 58 | 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: 3 | 61 | - id: 3 |
| 58 | title: "Méthode initialize et stockage des capacités" | 62 | title: "Méthode initialize et stockage des capacités" |
| 59 | - state: open | 63 | + state: closed |
| 60 | priority: high | 64 | priority: high |
| 61 | author: | 65 | author: |
| 62 | name: k33g | 66 | name: k33g |
| 63 | email: ph.charriere@gmail.com | 67 | email: ph.charriere@gmail.com |
| 64 | createdAt: 2026-10-29T07:20:08.000Z | 68 | 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: 4 | 71 | - id: 4 |
| 66 | title: "Capture de stderr dans un log par agent" | 72 | title: "Capture de stderr dans un log par agent" |
| 67 | - state: open | 73 | + state: closed |
| 68 | priority: medium | 74 | priority: medium |
| 69 | author: | 75 | author: |
| 70 | name: k33g | 76 | name: k33g |
| 71 | email: ph.charriere@gmail.com | 77 | email: ph.charriere@gmail.com |
| 72 | createdAt: 2026-10-29T07:20:08.000Z | 78 | 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: 5 | 81 | - id: 5 |
| 74 | title: "Arrêt propre et gestion des crashs" | 82 | title: "Arrêt propre et gestion des crashs" |
| 75 | - state: open | 83 | + state: closed |
| 76 | priority: medium | 84 | priority: medium |
| 77 | author: | 85 | author: |
| 78 | name: k33g | 86 | name: k33g |
| 79 | email: ph.charriere@gmail.com | 87 | email: ph.charriere@gmail.com |
| 80 | createdAt: 2026-10-29T07:20:08.000Z | 88 | 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: 6 | 91 | - id: 6 |
| 82 | title: "Faux agent pour les tests unitaires" | 92 | title: "Faux agent pour les tests unitaires" |
| 83 | - state: open | 93 | + state: closed |
| 84 | priority: medium | 94 | priority: medium |
| 85 | author: | 95 | author: |
| 86 | name: k33g | 96 | name: k33g |
| 87 | email: ph.charriere@gmail.com | 97 | email: ph.charriere@gmail.com |
| 88 | createdAt: 2026-10-29T07:20:08.000Z | 98 | 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: 7 | 101 | - id: 7 |
| 90 | title: "Test d'intégration avec Alice" | 102 | title: "Test d'intégration avec Alice" |
| 91 | - state: open | 103 | + state: closed |
| 92 | priority: medium | 104 | priority: medium |
| 93 | author: | 105 | author: |
| 94 | name: k33g | 106 | name: k33g |
| 95 | email: ph.charriere@gmail.com | 107 | email: ph.charriere@gmail.com |
| 96 | createdAt: 2026-10-29T07:20:08.000Z | 108 | 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: 8 | 111 | - 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: open | 113 | + state: closed |
| 100 | priority: high | 114 | priority: high |
| 101 | author: | 115 | author: |
| 102 | name: k33g | 116 | name: k33g |
| 103 | email: ph.charriere@gmail.com | 117 | email: ph.charriere@gmail.com |
| 104 | createdAt: 2026-10-29T08:51:00.000Z | 118 | 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: 1 | 122 | - id: 1 |
| 107 | author: | 123 | author: |
| @@ -110,3 +126,14 @@ comments: | |||
| 110 | createdAt: 2026-10-29T08:51:00.000Z | 126 | 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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 11 |
| 3 | 3 | title: "Gérer le cycle de vie des processus agents" |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-29T07:20:09.000Z |
| 9 | -updatedAt: 2026-10-29T07:20:09.000Z | |
| 9 | +updatedAt: 2026-10-29T10:19:30.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - "kind::feature" |
| 12 | 12 | - "area::acp" |
| @@ -33,50 +33,73 @@ body: | | ||
| 33 | 33 | tasks: |
| 34 | 34 | - id: 1 |
| 35 | 35 | title: "AgentManager : démarrage paresseux" |
| 36 | - state: open | |
| 36 | + state: closed | |
| 37 | 37 | priority: high |
| 38 | 38 | author: |
| 39 | 39 | name: k33g |
| 40 | 40 | email: ph.charriere@gmail.com |
| 41 | 41 | createdAt: 2026-10-29T07:20:09.000Z |
| 42 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 43 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 42 | 44 | - id: 2 |
| 43 | 45 | title: "Arrêt après inactivité" |
| 44 | - state: open | |
| 46 | + state: closed | |
| 45 | 47 | priority: medium |
| 46 | 48 | author: |
| 47 | 49 | name: k33g |
| 48 | 50 | email: ph.charriere@gmail.com |
| 49 | 51 | createdAt: 2026-10-29T07:20:09.000Z |
| 52 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 53 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 50 | 54 | - id: 3 |
| 51 | 55 | title: "Limite d'agents simultanés" |
| 52 | - state: open | |
| 56 | + state: closed | |
| 53 | 57 | priority: high |
| 54 | 58 | author: |
| 55 | 59 | name: k33g |
| 56 | 60 | email: ph.charriere@gmail.com |
| 57 | 61 | createdAt: 2026-10-29T07:20:09.000Z |
| 62 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 63 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 58 | 64 | - id: 4 |
| 59 | 65 | title: "Machine à états et événements" |
| 60 | - state: open | |
| 66 | + state: closed | |
| 61 | 67 | priority: medium |
| 62 | 68 | author: |
| 63 | 69 | name: k33g |
| 64 | 70 | email: ph.charriere@gmail.com |
| 65 | 71 | createdAt: 2026-10-29T07:20:09.000Z |
| 72 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 73 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 66 | 74 | - id: 5 |
| 67 | 75 | title: "Gestion du crash et proposition de redémarrage" |
| 68 | - state: open | |
| 76 | + state: closed | |
| 69 | 77 | priority: medium |
| 70 | 78 | author: |
| 71 | 79 | name: k33g |
| 72 | 80 | email: ph.charriere@gmail.com |
| 73 | 81 | createdAt: 2026-10-29T07:20:09.000Z |
| 82 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 83 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 74 | 84 | - id: 6 |
| 75 | 85 | title: "Tests" |
| 76 | - state: open | |
| 86 | + state: closed | |
| 77 | 87 | priority: medium |
| 78 | 88 | author: |
| 79 | 89 | name: k33g |
| 80 | 90 | email: ph.charriere@gmail.com |
| 81 | 91 | 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: 11 | 2 | 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: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-29T07:20:09.000Z | 8 | createdAt: 2026-10-29T07:20:09.000Z |
| 9 | -updatedAt: 2026-10-29T07:20:09.000Z | 9 | +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: 1 | 34 | - id: 1 |
| 35 | title: "AgentManager : démarrage paresseux" | 35 | title: "AgentManager : démarrage paresseux" |
| 36 | - state: open | 36 | + state: closed |
| 37 | priority: high | 37 | priority: high |
| 38 | author: | 38 | author: |
| 39 | name: k33g | 39 | name: k33g |
| 40 | email: ph.charriere@gmail.com | 40 | email: ph.charriere@gmail.com |
| 41 | createdAt: 2026-10-29T07:20:09.000Z | 41 | 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: 2 | 44 | - id: 2 |
| 43 | title: "Arrêt après inactivité" | 45 | title: "Arrêt après inactivité" |
| 44 | - state: open | 46 | + state: closed |
| 45 | priority: medium | 47 | priority: medium |
| 46 | author: | 48 | author: |
| 47 | name: k33g | 49 | name: k33g |
| 48 | email: ph.charriere@gmail.com | 50 | email: ph.charriere@gmail.com |
| 49 | createdAt: 2026-10-29T07:20:09.000Z | 51 | 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: 3 | 54 | - id: 3 |
| 51 | title: "Limite d'agents simultanés" | 55 | title: "Limite d'agents simultanés" |
| 52 | - state: open | 56 | + state: closed |
| 53 | priority: high | 57 | priority: high |
| 54 | author: | 58 | author: |
| 55 | name: k33g | 59 | name: k33g |
| 56 | email: ph.charriere@gmail.com | 60 | email: ph.charriere@gmail.com |
| 57 | createdAt: 2026-10-29T07:20:09.000Z | 61 | 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: 4 | 64 | - id: 4 |
| 59 | title: "Machine à états et événements" | 65 | title: "Machine à états et événements" |
| 60 | - state: open | 66 | + state: closed |
| 61 | priority: medium | 67 | priority: medium |
| 62 | author: | 68 | author: |
| 63 | name: k33g | 69 | name: k33g |
| 64 | email: ph.charriere@gmail.com | 70 | email: ph.charriere@gmail.com |
| 65 | createdAt: 2026-10-29T07:20:09.000Z | 71 | 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: 5 | 74 | - id: 5 |
| 67 | title: "Gestion du crash et proposition de redémarrage" | 75 | title: "Gestion du crash et proposition de redémarrage" |
| 68 | - state: open | 76 | + state: closed |
| 69 | priority: medium | 77 | priority: medium |
| 70 | author: | 78 | author: |
| 71 | name: k33g | 79 | name: k33g |
| 72 | email: ph.charriere@gmail.com | 80 | email: ph.charriere@gmail.com |
| 73 | createdAt: 2026-10-29T07:20:09.000Z | 81 | 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: 6 | 84 | - id: 6 |
| 75 | title: "Tests" | 85 | title: "Tests" |
| 76 | - state: open | 86 | + state: closed |
| 77 | priority: medium | 87 | priority: medium |
| 78 | author: | 88 | author: |
| 79 | name: k33g | 89 | name: k33g |
| 80 | email: ph.charriere@gmail.com | 90 | email: ph.charriere@gmail.com |
| 81 | createdAt: 2026-10-29T07:20:09.000Z | 91 | 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: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-29T07:20:10.000Z |
| 9 | -updatedAt: 2026-10-29T07:20:10.000Z | |
| 9 | +updatedAt: 2026-10-29T11:58:59.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - "kind::feature" |
| 12 | 12 | - "area::ui" |
| @@ -37,42 +37,82 @@ body: | | ||
| 37 | 37 | tasks: |
| 38 | 38 | - id: 1 |
| 39 | 39 | title: "Layout docks + centre" |
| 40 | - state: open | |
| 40 | + state: closed | |
| 41 | 41 | priority: high |
| 42 | 42 | author: |
| 43 | 43 | name: k33g |
| 44 | 44 | email: ph.charriere@gmail.com |
| 45 | 45 | createdAt: 2026-10-29T07:20:10.000Z |
| 46 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 47 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 46 | 48 | - id: 2 |
| 47 | 49 | title: "Redimensionnement et repli" |
| 48 | - state: open | |
| 50 | + state: closed | |
| 49 | 51 | priority: medium |
| 50 | 52 | author: |
| 51 | 53 | name: k33g |
| 52 | 54 | email: ph.charriere@gmail.com |
| 53 | 55 | createdAt: 2026-10-29T07:20:10.000Z |
| 56 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 57 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 54 | 58 | - id: 3 |
| 55 | 59 | title: "Persistance de la disposition" |
| 56 | - state: open | |
| 60 | + state: closed | |
| 57 | 61 | priority: medium |
| 58 | 62 | author: |
| 59 | 63 | name: k33g |
| 60 | 64 | email: ph.charriere@gmail.com |
| 61 | 65 | createdAt: 2026-10-29T07:20:10.000Z |
| 66 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 67 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 62 | 68 | - id: 4 |
| 63 | 69 | title: "Registre de panels" |
| 64 | - state: open | |
| 70 | + state: closed | |
| 65 | 71 | priority: medium |
| 66 | 72 | author: |
| 67 | 73 | name: k33g |
| 68 | 74 | email: ph.charriere@gmail.com |
| 69 | 75 | createdAt: 2026-10-29T07:20:10.000Z |
| 76 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 77 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 70 | 78 | - id: 5 |
| 71 | 79 | title: "Thèmes clair et sombre" |
| 72 | - state: open | |
| 80 | + state: closed | |
| 73 | 81 | priority: medium |
| 74 | 82 | author: |
| 75 | 83 | name: k33g |
| 76 | 84 | email: ph.charriere@gmail.com |
| 77 | 85 | 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: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-29T07:20:10.000Z | 8 | createdAt: 2026-10-29T07:20:10.000Z |
| 9 | -updatedAt: 2026-10-29T07:20:10.000Z | 9 | +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: 1 | 38 | - id: 1 |
| 39 | title: "Layout docks + centre" | 39 | title: "Layout docks + centre" |
| 40 | - state: open | 40 | + state: closed |
| 41 | priority: high | 41 | priority: high |
| 42 | author: | 42 | author: |
| 43 | name: k33g | 43 | name: k33g |
| 44 | email: ph.charriere@gmail.com | 44 | email: ph.charriere@gmail.com |
| 45 | createdAt: 2026-10-29T07:20:10.000Z | 45 | 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: 2 | 48 | - id: 2 |
| 47 | title: "Redimensionnement et repli" | 49 | title: "Redimensionnement et repli" |
| 48 | - state: open | 50 | + state: closed |
| 49 | priority: medium | 51 | priority: medium |
| 50 | author: | 52 | author: |
| 51 | name: k33g | 53 | name: k33g |
| 52 | email: ph.charriere@gmail.com | 54 | email: ph.charriere@gmail.com |
| 53 | createdAt: 2026-10-29T07:20:10.000Z | 55 | 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: 3 | 58 | - id: 3 |
| 55 | title: "Persistance de la disposition" | 59 | title: "Persistance de la disposition" |
| 56 | - state: open | 60 | + state: closed |
| 57 | priority: medium | 61 | priority: medium |
| 58 | author: | 62 | author: |
| 59 | name: k33g | 63 | name: k33g |
| 60 | email: ph.charriere@gmail.com | 64 | email: ph.charriere@gmail.com |
| 61 | createdAt: 2026-10-29T07:20:10.000Z | 65 | 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: 4 | 68 | - id: 4 |
| 63 | title: "Registre de panels" | 69 | title: "Registre de panels" |
| 64 | - state: open | 70 | + state: closed |
| 65 | priority: medium | 71 | priority: medium |
| 66 | author: | 72 | author: |
| 67 | name: k33g | 73 | name: k33g |
| 68 | email: ph.charriere@gmail.com | 74 | email: ph.charriere@gmail.com |
| 69 | createdAt: 2026-10-29T07:20:10.000Z | 75 | 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: 5 | 78 | - id: 5 |
| 71 | title: "Thèmes clair et sombre" | 79 | title: "Thèmes clair et sombre" |
| 72 | - state: open | 80 | + state: closed |
| 73 | priority: medium | 81 | priority: medium |
| 74 | author: | 82 | author: |
| 75 | name: k33g | 83 | name: k33g |
| 76 | email: ph.charriere@gmail.com | 84 | email: ph.charriere@gmail.com |
| 77 | createdAt: 2026-10-29T07:20:10.000Z | 85 | 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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 13 |
| 3 | 3 | title: "Pont d'événements entre Go et le frontend" |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-29T07:20:11.000Z |
| 9 | -updatedAt: 2026-10-29T07:20:11.000Z | |
| 9 | +updatedAt: 2026-10-29T10:19:30.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - "kind::feature" |
| 12 | 12 | - "area::infra" |
| @@ -30,34 +30,54 @@ body: | | ||
| 30 | 30 | tasks: |
| 31 | 31 | - id: 1 |
| 32 | 32 | title: "Définir les types d'événements" |
| 33 | - state: open | |
| 33 | + state: closed | |
| 34 | 34 | priority: medium |
| 35 | 35 | author: |
| 36 | 36 | name: k33g |
| 37 | 37 | email: ph.charriere@gmail.com |
| 38 | 38 | createdAt: 2026-10-29T07:20:11.000Z |
| 39 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 40 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 39 | 41 | - id: 2 |
| 40 | 42 | title: "Émission côté Go" |
| 41 | - state: open | |
| 43 | + state: closed | |
| 42 | 44 | priority: medium |
| 43 | 45 | author: |
| 44 | 46 | name: k33g |
| 45 | 47 | email: ph.charriere@gmail.com |
| 46 | 48 | createdAt: 2026-10-29T07:20:11.000Z |
| 49 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 50 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 47 | 51 | - id: 3 |
| 48 | 52 | title: "Abonnement côté frontend avec nettoyage" |
| 49 | - state: open | |
| 53 | + state: closed | |
| 50 | 54 | priority: medium |
| 51 | 55 | author: |
| 52 | 56 | name: k33g |
| 53 | 57 | email: ph.charriere@gmail.com |
| 54 | 58 | createdAt: 2026-10-29T07:20:11.000Z |
| 59 | + updatedAt: 2026-10-29T10:19:30.000Z | |
| 60 | + closedAt: 2026-10-29T10:19:30.000Z | |
| 55 | 61 | - id: 4 |
| 56 | 62 | title: "Test de débit et batching" |
| 57 | - state: open | |
| 63 | + state: closed | |
| 58 | 64 | priority: medium |
| 59 | 65 | author: |
| 60 | 66 | name: k33g |
| 61 | 67 | email: ph.charriere@gmail.com |
| 62 | 68 | 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: 13 | 2 | 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: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-29T07:20:11.000Z | 8 | createdAt: 2026-10-29T07:20:11.000Z |
| 9 | -updatedAt: 2026-10-29T07:20:11.000Z | 9 | +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: 1 | 31 | - id: 1 |
| 32 | title: "Définir les types d'événements" | 32 | title: "Définir les types d'événements" |
| 33 | - state: open | 33 | + state: closed |
| 34 | priority: medium | 34 | priority: medium |
| 35 | author: | 35 | author: |
| 36 | name: k33g | 36 | name: k33g |
| 37 | email: ph.charriere@gmail.com | 37 | email: ph.charriere@gmail.com |
| 38 | createdAt: 2026-10-29T07:20:11.000Z | 38 | 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: 2 | 41 | - id: 2 |
| 40 | title: "Émission côté Go" | 42 | title: "Émission côté Go" |
| 41 | - state: open | 43 | + state: closed |
| 42 | priority: medium | 44 | priority: medium |
| 43 | author: | 45 | author: |
| 44 | name: k33g | 46 | name: k33g |
| 45 | email: ph.charriere@gmail.com | 47 | email: ph.charriere@gmail.com |
| 46 | createdAt: 2026-10-29T07:20:11.000Z | 48 | 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: 3 | 51 | - id: 3 |
| 48 | title: "Abonnement côté frontend avec nettoyage" | 52 | title: "Abonnement côté frontend avec nettoyage" |
| 49 | - state: open | 53 | + state: closed |
| 50 | priority: medium | 54 | priority: medium |
| 51 | author: | 55 | author: |
| 52 | name: k33g | 56 | name: k33g |
| 53 | email: ph.charriere@gmail.com | 57 | email: ph.charriere@gmail.com |
| 54 | createdAt: 2026-10-29T07:20:11.000Z | 58 | 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: 4 | 61 | - id: 4 |
| 56 | title: "Test de débit et batching" | 62 | title: "Test de débit et batching" |
| 57 | - state: open | 63 | + state: closed |
| 58 | priority: medium | 64 | priority: medium |
| 59 | author: | 65 | author: |
| 60 | name: k33g | 66 | name: k33g |
| 61 | email: ph.charriere@gmail.com | 67 | email: ph.charriere@gmail.com |
| 62 | createdAt: 2026-10-29T07:20:11.000Z | 68 | 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 @@ | ||
| 1 | 1 | # Copyright (c) 2026 Philippe Charrière — Sidekicks Source-Available License 1.0, see LICENSE |
| 2 | 2 | # |
| 3 | 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-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 | |
| 6 | 12 | |
| 7 | 13 | 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 | 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 | 56 | @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 LICENSE | 1 | # 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-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 | ||
| 6 | 12 | ||
| 7 | help: ## List the available targets | 13 | 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.sh | 56 | @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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 @@ | ||
| 1 | 1 | # ADR 0003 — Architecture, data model and events |
| 2 | 2 | |
| 3 | -- **Status**: accepted | |
| 3 | +- **Status**: accepted — amended on 2026-10-29 by milestone M1 (see "Amendment 1") | |
| 4 | 4 | - **Date**: 2026-10-29 |
| 5 | 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 | 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 | 14 |  |
| 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 | 18 | | Package | Responsibility | Depends on | |
| 19 | 19 | | --- | --- | --- | |
| 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. | — | | |
| 22 | 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 | 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 | 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 | 108 | ─▶ 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 | 119 | ## 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 | 122 | - The diagram and this table are part of the definition of done of any change that adds or re-wires a package. |
| 114 | 123 | - 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 events | 1 | # ADR 0003 — Architecture, data model and events |
| 2 | 2 | ||
| 3 | -- **Status**: accepted | 3 | +- **Status**: accepted — amended on 2026-10-29 by milestone M1 (see "Amendment 1") |
| 4 | - **Date**: 2026-10-29 | 4 | - **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 |  | 14 |  |
| 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:finished | 108 | ─▶ 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 | ## Consequences | 119 | ## 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 @@ | ||
| 1 | 1 | <mxfile host="app.diagrams.net"> |
| 2 | 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 | 4 | <root> |
| 5 | 5 | <mxCell id="0"/> |
| 6 | 6 | <mxCell id="1" parent="0"/> |
| 7 | - <mxCell id="title" value="<b style="font-size:25px;color:#1A237E">Sidekicks — packages and dependencies</b><br><i style="font-size:14px;color:#546E7A">arrows are imports · the frontend only talks to internal/app · nothing outside internal/acp sees the ACP SDK</i>" 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="<b style="font-size:25px;color:#1A237E">Sidekicks — packages and dependencies</b><br><i style="font-size:14px;color:#546E7A">arrows are imports · blue: exists (M1) · grey: planned · only main.go imports Wails</i>" 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 | 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 | 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 | 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 | 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 | 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 | 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> | |
| 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="💻<br><b style="font-size:16px">frontend/</b><br><span style="font-size:11.5px;font-weight:normal">Svelte 5 · panels · Markdown<br>CodeMirror · xterm.js</span>" 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="🔀<br><b style="font-size:16px">internal/app</b><br><span style="font-size:11.5px;font-weight:normal">Wails services bound to the UI<br>typed events to the UI</span>" 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="💻<br><b style="font-size:16px">frontend/</b><br><span style="font-size:11.5px;font-weight:normal">Svelte 5 · panels · state<br>generated TS bindings</span>" 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="🔀<br><b style="font-size:16px">internal/app</b><br><span style="font-size:11.5px;font-weight:normal">AgentService · ConfigService<br>config ↔ agents glue</span>" 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 | 25 | <mxCell id="c3" value="<b>★ only API of the UI</b>" 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="⚙️<br><b style="font-size:16px">cmd/sidekicks</b><br><span style="font-size:11.5px;font-weight:normal">main: reads flags, wires<br>the services, runs Wails</span>" 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="⚙️<br><b style="font-size:16px">main.go (root)</b><br><span style="font-size:11.5px;font-weight:normal">flags · Wails app · window<br>binds services and events</span>" 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 | 27 | <mxCell id="c6" value="<b style="font-size:12.5px;color:#263238">bindings · events</b>" 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 | 28 | <mxCell id="c8" value="<b style="font-size:12.5px;color:#263238">creates</b>" 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="💬<br><b style="font-size:16px">internal/channels</b><br><span style="font-size:11.5px;font-weight:normal">channels, members, routing<br>@mentions · /agent:command</span>" 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="🤖<br><b style="font-size:16px">internal/agents</b><br><span style="font-size:11.5px;font-weight:normal">agent processes: lazy start,<br>idle stop, limit, logs</span>" 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="📋<br><b style="font-size:16px">internal/config</b><br><span style="font-size:11.5px;font-weight:normal">load · validate · watch<br>save with comments</span>" 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="🧰<br><b style="font-size:16px">internal/workspace</b><br><span style="font-size:11.5px;font-weight:normal">files + exec: local,<br>container, sbx</span>" 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="📥<br><b style="font-size:16px">internal/store</b><br><span style="font-size:11.5px;font-weight:normal">Pebble: channels, messages,<br>session ids</span>" 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="🔧<br><b style="font-size:16px">internal/acp</b><br><span style="font-size:11.5px;font-weight:normal">ACP v1 client wrapper<br>SDK types stay inside</span>" 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="🛡️<br><b style="font-size:16px">schema</b><br><span style="font-size:11.5px;font-weight:normal">config JSON Schema<br>ValidateConfig()</span>" 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="<b>cockroachdb/pebble/v2</b>" 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="<b>coder/acp-go-sdk → mm -acp</b>" 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="<b>jsonschema/v6 · yaml/v3</b>" 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="<b>creack/pty · docker · sbx</b>" 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="<b style="font-size:12.5px;color:#263238">stdio</b>" 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="💬<br><b style="font-size:16px">internal/channels</b><br><span style="font-size:11.5px;font-weight:normal">planned · M3<br>channels, routing, @ and /</span>" 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="🤖<br><b style="font-size:16px">internal/agents</b><br><span style="font-size:11.5px;font-weight:normal">lazy start · LRU limit<br>idle stop · states · logs</span>" 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="📋<br><b style="font-size:16px">internal/config</b><br><span style="font-size:11.5px;font-weight:normal">load · validate · locate<br>default file · save · watch</span>" 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="🧰<br><b style="font-size:16px">internal/workspace</b><br><span style="font-size:11.5px;font-weight:normal">planned · M4<br>files + exec: local, container, sbx</span>" 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="📥<br><b style="font-size:16px">internal/store</b><br><span style="font-size:11.5px;font-weight:normal">planned · M3<br>Pebble: channels, messages</span>" 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="🔧<br><b style="font-size:16px">internal/acp</b><br><span style="font-size:11.5px;font-weight:normal">ACP v1 client over stdio<br>SDK types stay inside</span>" 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="🛡️<br><b style="font-size:16px">schema</b><br><span style="font-size:11.5px;font-weight:normal">config JSON Schema<br>ValidateConfig()</span>" 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="📋<br><b style="font-size:16px">internal/events</b><br><span style="font-size:11.5px;font-weight:normal">event names + payloads<br>Emitter interface</span>" 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="<b style="font-size:12.5px;color:#263238">emits</b>" 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="<b>cockroachdb/pebble/v2</b>" 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="<b>coder/acp-go-sdk → mm -acp</b>" 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="<b>jsonschema/v6 · yaml/v3</b>" 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="<b>fsnotify (config watch)</b>" 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="<b style="font-size:12.5px;color:#263238">stdio</b>" 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 | 44 | </root> |
| 43 | 45 | </mxGraphModel> |
| 44 | 46 | </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="<b style="font-size:25px;color:#1A237E">Sidekicks — packages and dependencies</b><br><i style="font-size:14px;color:#546E7A">arrows are imports · the frontend only talks to internal/app · nothing outside internal/acp sees the ACP SDK</i>" 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="<b style="font-size:25px;color:#1A237E">Sidekicks — packages and dependencies</b><br><i style="font-size:14px;color:#546E7A">arrows are imports · blue: exists (M1) · grey: planned · only main.go imports Wails</i>" 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="💻<br><b style="font-size:16px">frontend/</b><br><span style="font-size:11.5px;font-weight:normal">Svelte 5 · panels · Markdown<br>CodeMirror · xterm.js</span>" 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="💻<br><b style="font-size:16px">frontend/</b><br><span style="font-size:11.5px;font-weight:normal">Svelte 5 · panels · state<br>generated TS bindings</span>" 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="🔀<br><b style="font-size:16px">internal/app</b><br><span style="font-size:11.5px;font-weight:normal">Wails services bound to the UI<br>typed events to the UI</span>" 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="🔀<br><b style="font-size:16px">internal/app</b><br><span style="font-size:11.5px;font-weight:normal">AgentService · ConfigService<br>config ↔ agents glue</span>" 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="<b>★ only API of the UI</b>" 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="<b>★ only API of the UI</b>" 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="⚙️<br><b style="font-size:16px">cmd/sidekicks</b><br><span style="font-size:11.5px;font-weight:normal">main: reads flags, wires<br>the services, runs Wails</span>" 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="⚙️<br><b style="font-size:16px">main.go (root)</b><br><span style="font-size:11.5px;font-weight:normal">flags · Wails app · window<br>binds services and events</span>" 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="<b style="font-size:12.5px;color:#263238">bindings · events</b>" 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="<b style="font-size:12.5px;color:#263238">bindings · events</b>" 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="<b style="font-size:12.5px;color:#263238">creates</b>" 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="<b style="font-size:12.5px;color:#263238">creates</b>" 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="💬<br><b style="font-size:16px">internal/channels</b><br><span style="font-size:11.5px;font-weight:normal">channels, members, routing<br>@mentions · /agent:command</span>" 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="💬<br><b style="font-size:16px">internal/channels</b><br><span style="font-size:11.5px;font-weight:normal">planned · M3<br>channels, routing, @ and /</span>" 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="🤖<br><b style="font-size:16px">internal/agents</b><br><span style="font-size:11.5px;font-weight:normal">agent processes: lazy start,<br>idle stop, limit, logs</span>" 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="🤖<br><b style="font-size:16px">internal/agents</b><br><span style="font-size:11.5px;font-weight:normal">lazy start · LRU limit<br>idle stop · states · logs</span>" 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="📋<br><b style="font-size:16px">internal/config</b><br><span style="font-size:11.5px;font-weight:normal">load · validate · watch<br>save with comments</span>" 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="📋<br><b style="font-size:16px">internal/config</b><br><span style="font-size:11.5px;font-weight:normal">load · validate · locate<br>default file · save · watch</span>" 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="🧰<br><b style="font-size:16px">internal/workspace</b><br><span style="font-size:11.5px;font-weight:normal">files + exec: local,<br>container, sbx</span>" 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="🧰<br><b style="font-size:16px">internal/workspace</b><br><span style="font-size:11.5px;font-weight:normal">planned · M4<br>files + exec: local, container, sbx</span>" 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="📥<br><b style="font-size:16px">internal/store</b><br><span style="font-size:11.5px;font-weight:normal">Pebble: channels, messages,<br>session ids</span>" 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="📥<br><b style="font-size:16px">internal/store</b><br><span style="font-size:11.5px;font-weight:normal">planned · M3<br>Pebble: channels, messages</span>" 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="🔧<br><b style="font-size:16px">internal/acp</b><br><span style="font-size:11.5px;font-weight:normal">ACP v1 client wrapper<br>SDK types stay inside</span>" 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="🔧<br><b style="font-size:16px">internal/acp</b><br><span style="font-size:11.5px;font-weight:normal">ACP v1 client over stdio<br>SDK types stay inside</span>" 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="🛡️<br><b style="font-size:16px">schema</b><br><span style="font-size:11.5px;font-weight:normal">config JSON Schema<br>ValidateConfig()</span>" 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="🛡️<br><b style="font-size:16px">schema</b><br><span style="font-size:11.5px;font-weight:normal">config JSON Schema<br>ValidateConfig()</span>" 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="<b>cockroachdb/pebble/v2</b>" 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="📋<br><b style="font-size:16px">internal/events</b><br><span style="font-size:11.5px;font-weight:normal">event names + payloads<br>Emitter interface</span>" 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="<b>coder/acp-go-sdk → mm -acp</b>" 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="<b style="font-size:12.5px;color:#263238">emits</b>" 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="<b>jsonschema/v6 · yaml/v3</b>" 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="<b>cockroachdb/pebble/v2</b>" 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="<b>creack/pty · docker · sbx</b>" 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="<b>coder/acp-go-sdk → mm -acp</b>" 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="<b style="font-size:12.5px;color:#263238">stdio</b>" 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="<b>jsonschema/v6 · yaml/v3</b>" 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="<b>fsnotify (config watch)</b>" 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="<b style="font-size:12.5px;color:#263238">stdio</b>" 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 differ | Binary 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"> | |
| 2 | 2 | <defs> |
| 3 | 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 | 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 | 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 | 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 | 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 | 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 | 15 | <path d="M290,157 L475,157" fill="none" stroke="#37474F" stroke-width="2.2" marker-start="url(#arrow)" marker-end="url(#arrow)"/> |
| 16 | 16 | <path d="M910,157 L725,157" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/> |
| 17 | 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 | 20 | <path d="M600,215 L600,255 L1035,255 L1035,300" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/> |
| 21 | 21 | <path d="M290,355 L330,355" fill="none" stroke="#37474F" stroke-width="2.2" marker-end="url(#arrow)"/> |
| 22 | 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)"/> | |
| 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)"/> | |
| 30 | 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 | 31 | <text x="165.0" y="137.5" text-anchor="middle" font-size="22">💻</text> |
| 32 | 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> | |
| 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> | |
| 35 | 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 | 36 | <text x="600.0" y="137.5" text-anchor="middle" font-size="22">🔀</text> |
| 37 | 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> | |
| 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> | |
| 40 | 40 | <rect x="577.0" y="88" width="158.0" height="24" rx="12" fill="#6A1B9A"/> |
| 41 | 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 | 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> | |
| 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> | |
| 47 | 47 | <rect x="317.0" y="145.0" width="130" height="24" rx="5" fill="#FFFFFF"/> |
| 48 | 48 | <text x="382" y="161.5" text-anchor="middle" font-size="12.5" font-weight="700" fill="#263238">bindings · events</text> |
| 49 | 49 | <rect x="782.0" y="145.0" width="70" height="24" rx="5" fill="#FFFFFF"/> |
| 50 | 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 | 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> | |
| 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> | |
| 56 | 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 | 57 | <text x="455.0" y="335.0" text-anchor="middle" font-size="22">🤖</text> |
| 58 | 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> | |
| 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> | |
| 61 | 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 | 62 | <text x="745.0" y="335.0" text-anchor="middle" font-size="22">📋</text> |
| 63 | 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> | |
| 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> | |
| 67 | 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> | |
| 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> | |
| 97 | 104 | </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 @@ | ||
| 1 | 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 | 5 | This documentation follows [Diátaxis](https://diataxis.fr): pick the section that matches what you need right now. |
| 6 | 6 | |
| 7 | 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) | | |
| 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) | | |
| 12 | 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 | 14 | ## 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 records | 14 | ## 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 | ||
| 6 | 6 | |
| 7 | 7 |  |
| 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 | 11 | ## Why this split |
| 12 | 12 | |
| @@ -16,6 +16,10 @@ Three facts drove it: | ||
| 16 | 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 | 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 | 23 | ## What lives in the YAML and what lives in the database |
| 20 | 24 | |
| 21 | 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. |
| @@ -6,7 +6,7 @@ Before writing the application, the project fixed how its code is divided, what | |||
| 6 | 6 | ||
| 7 |  | 7 |  |
| 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 split | 11 | ## 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 database | 23 | ## 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 | ||
| 12 | 12 | |
| 13 | 13 | ## 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 | 17 | - agents start **when first called**, not when the application opens; |
| 18 | 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 | 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 | 22 | ## 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 product | 13 | ## 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 alternatives | 22 | ## 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 @@ | ||
| 1 | 1 | # 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 everything | |
| 5 | +## Run what the CI runs | |
| 6 | 6 | |
| 7 | 7 | ```bash |
| 8 | -make test | |
| 8 | +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 | 13 | ```text |
| 14 | -ok git.rickub.com/bots-garden/sidekicks/schema … | |
| 15 | 14 | 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 | 20 | ## 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 | | |
| 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. | |
| 26 | 39 | |
| 27 | 40 | ## 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`. | |
| 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. | |
| 32 | 45 | |
| 33 | 46 | ## 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 | 49 | - The tooling under test → [Reference: scripts/agents.sh](../reference/agents-script.md) |
| @@ -1,36 +1,49 @@ | |||
| 1 | # How to run the tests | 1 | # 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 everything | 5 | +## Run what the CI runs |
| 6 | 6 | ||
| 7 | ```bash | 7 | ```bash |
| 8 | -make test | 8 | +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 | ```text | 13 | ```text |
| 14 | -ok git.rickub.com/bots-garden/sidekicks/schema … | ||
| 15 | 14 passed, 0 failed | 14 | 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 part | 20 | ## 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 | ## Variants | 40 | ## 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 also | 46 | ## 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 @@ | ||
| 7 | 7 | |
| 8 | 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 | 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 | 55 | | Key | Type | Default | Meaning | |
| 56 | 56 | | --- | --- | --- | --- | |
| 57 | 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 | 60 | ### 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 | 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 | 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 | 102 | 3. every name in a channel's `agents` is the name of an agent; |
| 103 | 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 | 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 | |
| @@ -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 agent | 60 | ### 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 @@ | ||
| 1 | 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 | 5 | Cette documentation suit la méthode [Diátaxis](https://diataxis.fr) : choisissez la section qui correspond à votre besoin du moment. |
| 6 | 6 | |
| 7 | 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) | | |
| 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) | | |
| 12 | 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 | 14 | ## 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 conception | 14 | ## 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 | ||
| 6 | 6 | |
| 7 | 7 |  |
| 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 | 11 | ## Pourquoi ce découpage |
| 12 | 12 | |
| @@ -16,6 +16,10 @@ Trois faits l'ont guidé : | ||
| 16 | 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 | 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 | 23 | ## Ce qui vit dans le YAML et ce qui vit dans la base |
| 20 | 24 | |
| 21 | 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. |
| @@ -6,7 +6,7 @@ Avant d'écrire l'application, le projet a fixé le découpage du code, ce qui e | |||
| 6 | 6 | ||
| 7 |  | 7 |  |
| 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écoupage | 11 | ## 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 base | 23 | ## 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 | ||
| 12 | 12 | |
| 13 | 13 | ## 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 | 17 | - les agents démarrent **au premier appel**, pas à l'ouverture de l'application ; |
| 18 | 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 | 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 | 22 | ## 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 produit | 13 | ## 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ées | 22 | ## 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 @@ | ||
| 1 | 1 | # 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 lancer | |
| 5 | +## Lancer ce que lance la CI | |
| 6 | 6 | |
| 7 | 7 | ```bash |
| 8 | -make test | |
| 8 | +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 | 13 | ```text |
| 14 | -ok git.rickub.com/bots-garden/sidekicks/schema … | |
| 15 | 14 | 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 | 20 | ## 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 | | |
| 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. | |
| 26 | 39 | |
| 27 | 40 | ## 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`. | |
| 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. | |
| 32 | 45 | |
| 33 | 46 | ## 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 | 49 | - L'outillage testé → [Référence : scripts/agents.sh](../reference/script-agents.md) |
| @@ -1,36 +1,49 @@ | |||
| 1 | # Comment lancer les tests | 1 | # 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 lancer | 5 | +## Lancer ce que lance la CI |
| 6 | 6 | ||
| 7 | ```bash | 7 | ```bash |
| 8 | -make test | 8 | +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 | ```text | 13 | ```text |
| 14 | -ok git.rickub.com/bots-garden/sidekicks/schema … | ||
| 15 | 14 passed, 0 failed | 14 | 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 partie | 20 | ## 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 | ## Variantes | 40 | ## 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 aussi | 46 | ## 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 @@ | ||
| 7 | 7 | |
| 8 | 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 | 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 | 55 | | Clé | Type | Défaut | Signification | |
| 56 | 56 | | --- | --- | --- | --- | |
| 57 | 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 | 60 | ### Un agent |
| 61 | 61 | |
| @@ -93,7 +93,7 @@ Le runtime indique à Sidekicks comment atteindre les **fichiers et le terminal* | ||
| 93 | 93 | |
| 94 | 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 | 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 | 102 | 3. chaque nom de la liste `agents` d'un channel est le nom d'un agent ; |
| 103 | 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 | 107 | Chaque erreur indique la ligne et la clé, dans la langue de l'interface, et dit comment corriger. Exemples : |
| 108 | 108 | |
| @@ -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 agent | 60 | ### 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 @@ | ||
| 1 | 1 | module git.rickub.com/bots-garden/sidekicks |
| 2 | 2 | |
| 3 | -go 1.26 | |
| 3 | +go 1.26.0 | |
| 4 | 4 | |
| 5 | 5 | require ( |
| 6 | + github.com/coder/acp-go-sdk v0.13.5 | |
| 7 | + github.com/fsnotify/fsnotify v1.10.1 | |
| 6 | 8 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 |
| 9 | + github.com/wailsapp/wails/v3 v3.0.0 | |
| 7 | 10 | 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 // 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/sidekicks | 1 | module git.rickub.com/bots-garden/sidekicks |
| 2 | 2 | ||
| 3 | -go 1.26 | 3 | +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.3 | 8 | 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.5 | 10 | 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 // 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 | +) | ||
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= | |
| 3 | 29 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28= |
| 4 | 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 | 35 | go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= |
| 6 | 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= | |
| 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 @@ | ||
| 75 | 75 | "default": 2 |
| 76 | 76 | }, |
| 77 | 77 | "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 | + ], | |
| 80 | 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 | }, |
| @@ -75,10 +75,15 @@ | |||
| 75 | "default": 2 | 75 | "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= | ||