diff --git a/.github/workflows/control-plane.yml b/.github/workflows/control-plane.yml index b9498c005..b8c6df393 100644 --- a/.github/workflows/control-plane.yml +++ b/.github/workflows/control-plane.yml @@ -118,16 +118,39 @@ jobs: echo "Building for ${GOOS}/${GOARCH}" go build -o /tmp/agentfield-server-${GOOS}-${GOARCH} ./cmd/agentfield-server + # The macOS menu-bar tray (cmd/af-tray) is darwin-only CGO code: the Linux + # jobs compile its !darwin stub and the CGO_ENABLED=0 matrix can't build it, + # so without this job a type error in tray_darwin.go would only surface at + # release time on the goreleaser macOS runner. + tray-darwin: + runs-on: macos-14 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25.x' + cache: true + cache-dependency-path: control-plane/go.sum + + - name: Compile and vet the macOS tray + working-directory: control-plane + run: | + go build ./cmd/af-tray/ + go vet ./cmd/af-tray/ + # Summary job that depends on all critical checks required-checks: name: All Required Checks Passed runs-on: ubuntu-latest - needs: [linux-tests, compile-matrix] + needs: [linux-tests, compile-matrix, tray-darwin] if: always() steps: - name: Check all jobs run: | - if [ "${{ needs.linux-tests.result }}" != "success" ] || [ "${{ needs.compile-matrix.result }}" != "success" ]; then + if [ "${{ needs.linux-tests.result }}" != "success" ] || [ "${{ needs.compile-matrix.result }}" != "success" ] || [ "${{ needs.tray-darwin.result }}" != "success" ]; then echo "One or more required checks failed" exit 1 fi diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml new file mode 100644 index 000000000..af3d5893a --- /dev/null +++ b/.github/workflows/desktop.yml @@ -0,0 +1,70 @@ +name: Desktop CI + +# The desktop app ships to macOS and Windows, so it is checked on both: the +# pure logic is covered by vitest anywhere, but packaging (electron-builder +# config, extraResources, icons) and platform-guarded code paths only prove +# out on their own runners. + +on: + pull_request: + paths: + - 'desktop/**' + - '.github/workflows/desktop.yml' + push: + branches: [main] + paths: + - 'desktop/**' + - '.github/workflows/desktop.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + test-and-package: + strategy: + fail-fast: false + matrix: + os: [macos-14, windows-latest] + runs-on: ${{ matrix.os }} + env: + # No signing identities in CI; validate packaging unsigned. + CSC_IDENTITY_AUTO_DISCOVERY: 'false' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: desktop/package-lock.json + + - name: Install dependencies + working-directory: desktop + run: npm ci + + - name: Typecheck + working-directory: desktop + run: npm run typecheck + + - name: Unit tests + working-directory: desktop + run: npm test + + - name: Stub bundled CLI + working-directory: desktop + shell: bash + # A placeholder is enough to validate the extraResources wiring; the + # release pipeline drops the real per-platform af binary into vendor/. + run: | + if [ "$RUNNER_OS" = "Windows" ]; then + printf 'stub' > vendor/af.exe + else + printf 'stub' > vendor/af + fi + + - name: Package (unsigned, unpacked) + working-directory: desktop + run: npm run dist:dir diff --git a/.goreleaser.yml b/.goreleaser.yml index a4141fff2..6668dbff8 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -121,41 +121,47 @@ builds: - -X main.commit={{ .ShortCommit }} - -X main.date={{ .Date }} - # Windows build disabled - focus on Linux and macOS for now - # - id: agentfield-windows-amd64 - # dir: control-plane - # main: ./cmd/af - # binary: agentfield - # env: - # - CGO_ENABLED=1 - # - CC=gcc - # goos: - # - windows - # goarch: - # - amd64 - # ldflags: - # - -s -w - # - -X main.version={{ .Version }} - # - -X main.commit={{ .ShortCommit }} - # - -X main.date={{ .Date }} - # tags: - # - embedded - # - sqlite_fts5 - # no_unique_dist_dir: true + # Windows CLI/server binary (goreleaser appends .exe automatically). + # Groundwork only: the release workflow's build matrix filters by --id and + # does not build this id yet. CGO is required (mattn/go-sqlite3 with + # sqlite_fts5), so building it needs either a windows runner (drop the CC + # override) or mingw-w64 on the linux runner (mirrors the aarch64 + # cross-compile approach). + - id: agentfield-windows-amd64 + dir: control-plane + main: ./cmd/af + binary: agentfield-windows-amd64 + env: + - CGO_ENABLED=1 + - CC=x86_64-w64-mingw32-gcc + goos: + - windows + goarch: + - amd64 + ldflags: + - -s -w + - -X main.version={{ .Version }} + - -X main.commit={{ .ShortCommit }} + - -X main.date={{ .Date }} + tags: + - embedded + - sqlite_fts5 # Don't create archives - ship raw binaries archives: - id: default - builds: + ids: - agentfield-linux-amd64 - agentfield-linux-arm64 - agentfield-darwin-amd64 - agentfield-darwin-arm64 + - agentfield-windows-amd64 - agentfield-tray-darwin-amd64 - agentfield-tray-darwin-arm64 - format: binary + formats: [binary] # Each build's `binary:` is already the exact asset name we want - # (agentfield-- and agentfield-tray-darwin-). + # (agentfield-- and agentfield-tray-darwin-); goreleaser + # appends .exe for the windows build on its own. name_template: "{{ .Binary }}" checksum: diff --git a/control-plane/agentfield.yaml b/control-plane/agentfield.yaml deleted file mode 120000 index dc9d0c54d..000000000 --- a/control-plane/agentfield.yaml +++ /dev/null @@ -1 +0,0 @@ -config/agentfield.yaml \ No newline at end of file diff --git a/control-plane/cmd/af-tray/assets/appicon.icns b/control-plane/cmd/af-tray/assets/appicon.icns index 60a2f50ef..10aebb778 100644 Binary files a/control-plane/cmd/af-tray/assets/appicon.icns and b/control-plane/cmd/af-tray/assets/appicon.icns differ diff --git a/control-plane/cmd/af-tray/shared.go b/control-plane/cmd/af-tray/shared.go index ae5344aac..f59eae0cf 100644 --- a/control-plane/cmd/af-tray/shared.go +++ b/control-plane/cmd/af-tray/shared.go @@ -100,6 +100,36 @@ func uiPageURL(page string) string { return fmt.Sprintf("http://localhost:%d/ui/%s", serverPort(), page) } +// ---- Desktop app deep links -------------------------------------------------- +// +// The AgentField desktop app (desktop/ in this repo) registers the +// agentfield:// URL scheme. When it is installed, the tray opens views there; +// when it is not, `open agentfield://…` fails fast (no handler registered) +// and the tray falls back to the web UI in the browser (see openPage in +// tray_darwin.go). + +// deepLinkForPage maps a web-UI page name to the desktop-app view showing the +// same thing. Unknown/empty pages land on the app's dashboard. +func deepLinkForPage(page string) string { + switch page { + case "agents": + return "agentfield://agents" + case "executions": + return "agentfield://activity" + default: + return "agentfield://dashboard" + } +} + +// browserURLForPage is the web-UI fallback for the same page: the server root +// for the default page, /ui/ otherwise. +func browserURLForPage(page string) string { + if page == "" { + return dashboardURL() + } + return uiPageURL(page) +} + // checkHealth reports whether the given URL answers HTTP 200 within a short // timeout. The control plane's /health endpoint returns 200 when healthy and // 503 when not, so only a 200 counts as "running". diff --git a/control-plane/cmd/af-tray/shared_test.go b/control-plane/cmd/af-tray/shared_test.go index c5dde686d..2078ba1fd 100644 --- a/control-plane/cmd/af-tray/shared_test.go +++ b/control-plane/cmd/af-tray/shared_test.go @@ -54,6 +54,36 @@ func TestURLsUsePort(t *testing.T) { } } +// Contract: every page the tray can open has a desktop-app deep link, and +// unknown/empty pages land on the app's dashboard instead of failing. The +// scheme must stay "agentfield" — it is what the desktop app registers. +func TestDeepLinkForPage(t *testing.T) { + cases := map[string]string{ + "": "agentfield://dashboard", + "dashboard": "agentfield://dashboard", + "agents": "agentfield://agents", + "executions": "agentfield://activity", + "whatever": "agentfield://dashboard", + } + for page, want := range cases { + if got := deepLinkForPage(page); got != want { + t.Errorf("deepLinkForPage(%q) = %q, want %q", page, got, want) + } + } +} + +// Contract: the browser fallback opens the server root for the default page +// and the embedded web UI for named pages, on the resolved port. +func TestBrowserURLForPage(t *testing.T) { + t.Setenv("AGENTFIELD_PORT", "1234") + if got, want := browserURLForPage(""), "http://localhost:1234"; got != want { + t.Errorf("browserURLForPage(\"\") = %q, want %q", got, want) + } + if got, want := browserURLForPage("agents"), "http://localhost:1234/ui/agents"; got != want { + t.Errorf("browserURLForPage(\"agents\") = %q, want %q", got, want) + } +} + // Contract: a server is "running" only when /health answers HTTP 200; a 503 // (unhealthy) or an unreachable server both read as not running. func TestCheckHealth(t *testing.T) { diff --git a/control-plane/cmd/af-tray/tray_darwin.go b/control-plane/cmd/af-tray/tray_darwin.go index 81b6277d8..a822e9a46 100644 --- a/control-plane/cmd/af-tray/tray_darwin.go +++ b/control-plane/cmd/af-tray/tray_darwin.go @@ -249,15 +249,15 @@ func onReady() { case <-mOpen.ClickedCh: openDashboard() case <-mMore.ClickedCh: - openURL(uiPageURL("agents")) + openPage("agents") case <-mAgentsOpen.ClickedCh: - openURL(uiPageURL("agents")) + openPage("agents") case <-mSuccess.ClickedCh: - openURL(uiPageURL("executions")) + openPage("executions") case <-mResponse.ClickedCh: - openURL(uiPageURL("executions")) + openPage("executions") case <-mMemory.ClickedCh: - openURL(uiPageURL("dashboard")) + openPage("dashboard") case <-mEnterKey.ClickedCh: handleEnterAPIKey() refresh() @@ -339,14 +339,18 @@ func notify(title, body string) { _ = exec.Command("osascript", "-e", script).Start() } -func openDashboard() { - _ = exec.Command("open", dashboardURL()).Start() -} +func openDashboard() { openPage("") } -// openURL opens an arbitrary URL in the default browser (used for dashboard -// deep-links from the metric rows). -func openURL(u string) { - _ = exec.Command("open", u).Start() +// openPage opens a view, preferring the AgentField desktop app over the +// browser: `open agentfield://…` succeeds only when something has registered +// the scheme (the desktop app does on install) and fails fast when nothing +// has — in which case the same page opens as web UI in the default browser. +// Run (not Start) on the deep link because the fallback needs its exit code. +func openPage(page string) { + if exec.Command("open", deepLinkForPage(page)).Run() == nil { + return + } + _ = exec.Command("open", browserURLForPage(page)).Start() } func openLogs() { diff --git a/control-plane/cmd/af/main.go b/control-plane/cmd/af/main.go index 2f790bff7..0637ae0ec 100644 --- a/control-plane/cmd/af/main.go +++ b/control-plane/cmd/af/main.go @@ -338,6 +338,14 @@ func loadConfig(configFile string) (*config.Config, error) { // Set default storage mode to local if not specified if cfg.Storage.Mode == "" { cfg.Storage.Mode = "local" + } + // Default the local storage paths whenever they are unset — not only when + // the mode was defaulted. A config file may set mode: "local" while leaving + // the paths empty (the sample config/agentfield.yaml does exactly that, + // and it is picked up automatically when af runs next to it), which + // previously skipped this block and failed startup with "database path is + // empty". Mirrors cmd/agentfield-server. + if cfg.Storage.Mode == "local" { // Use the universal path management system if cfg.Storage.Local.DatabasePath == "" { dbPath, err := utils.GetDatabasePath() diff --git a/control-plane/internal/cli/logs.go b/control-plane/internal/cli/logs.go index c08917c76..4b4fbad72 100644 --- a/control-plane/internal/cli/logs.go +++ b/control-plane/internal/cli/logs.go @@ -4,8 +4,10 @@ import ( "errors" "fmt" "os" - "os/exec" // Added missing import + "os/exec" "path/filepath" + "runtime" + "strings" "github.com/Agent-Field/agentfield/control-plane/internal/logger" "github.com/Agent-Field/agentfield/control-plane/internal/packages" @@ -104,7 +106,7 @@ func (lv *LogViewer) ViewLogs(agentNodeName string) error { // tailLogs shows the last N lines of the log file func (lv *LogViewer) tailLogs(logFile string, lines int) error { - cmd := exec.Command("tail", "-n", fmt.Sprintf("%d", lines), logFile) + cmd := tailCommand(logFile, lines, false) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() @@ -112,8 +114,40 @@ func (lv *LogViewer) tailLogs(logFile string, lines int) error { // followLogs follows the log file in real-time func (lv *LogViewer) followLogs(logFile string) error { - cmd := exec.Command("tail", "-f", logFile) + cmd := tailCommand(logFile, 10, true) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } + +// tailCommand builds the platform command that prints the last n lines of a +// file, optionally following it. +func tailCommand(logFile string, n int, follow bool) *exec.Cmd { + program, args := tailCommandArgs(runtime.GOOS, logFile, n, follow) + return exec.Command(program, args...) +} + +// tailCommandArgs returns the program and arguments that tail a log file on +// the given GOOS. Unix uses tail(1); Windows has no tail, so PowerShell's +// Get-Content stands in (compile-verified only, not yet tested on a real +// Windows machine). Pure so both platform branches are unit-testable anywhere. +func tailCommandArgs(goos, logFile string, n int, follow bool) (string, []string) { + if goos == "windows" { + script := fmt.Sprintf("Get-Content -LiteralPath %s -Tail %d", psSingleQuote(logFile), n) + if follow { + script += " -Wait" + } + return "powershell", []string{"-NoProfile", "-Command", script} + } + args := []string{"-n", fmt.Sprintf("%d", n)} + if follow { + args = append(args, "-f") + } + return "tail", append(args, logFile) +} + +// psSingleQuote quotes s as a PowerShell single-quoted string literal, where +// the only escape is doubling embedded single quotes. +func psSingleQuote(s string) string { + return "'" + strings.ReplaceAll(s, "'", "''") + "'" +} diff --git a/control-plane/internal/cli/logs_tail_test.go b/control-plane/internal/cli/logs_tail_test.go new file mode 100644 index 000000000..421d7c396 --- /dev/null +++ b/control-plane/internal/cli/logs_tail_test.go @@ -0,0 +1,92 @@ +package cli + +import ( + "reflect" + "runtime" + "testing" +) + +// Contract: `af logs` tails via tail(1) on Unix and via PowerShell +// Get-Content on Windows (which has no tail), preserving the requested line +// count, the follow flag, and safe quoting of the log path. The helper is +// parameterized by GOOS so both platform branches are exercised regardless of +// the host running the tests. +func TestTailCommandArgs(t *testing.T) { + cases := []struct { + name string + goos string + file string + n int + follow bool + wantProg string + wantArgs []string + }{ + { + name: "unix no follow", goos: "linux", + file: "/var/log/agent.log", n: 7, + wantProg: "tail", + wantArgs: []string{"-n", "7", "/var/log/agent.log"}, + }, + { + name: "unix follow", goos: "darwin", + file: "/var/log/agent.log", n: 10, follow: true, + wantProg: "tail", + wantArgs: []string{"-n", "10", "-f", "/var/log/agent.log"}, + }, + { + name: "windows no follow", goos: "windows", + file: `C:\logs\agent.log`, n: 7, + wantProg: "powershell", + wantArgs: []string{ + "-NoProfile", "-Command", + `Get-Content -LiteralPath 'C:\logs\agent.log' -Tail 7`, + }, + }, + { + name: "windows follow with quote escaping", goos: "windows", + file: `C:\it's here\agent.log`, n: 10, follow: true, + wantProg: "powershell", + wantArgs: []string{ + "-NoProfile", "-Command", + `Get-Content -LiteralPath 'C:\it''s here\agent.log' -Tail 10 -Wait`, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + prog, args := tailCommandArgs(tc.goos, tc.file, tc.n, tc.follow) + if prog != tc.wantProg { + t.Fatalf("program = %q; want %q", prog, tc.wantProg) + } + if !reflect.DeepEqual(args, tc.wantArgs) { + t.Fatalf("args = %q; want %q", args, tc.wantArgs) + } + }) + } +} + +// Contract: tailCommand builds an exec.Cmd from the host platform's +// tailCommandArgs — the first Args entry is the program itself. +func TestTailCommandUsesHostGOOS(t *testing.T) { + cmd := tailCommand("/var/log/agent.log", 5, false) + prog, args := tailCommandArgs(runtime.GOOS, "/var/log/agent.log", 5, false) + want := append([]string{prog}, args...) + if !reflect.DeepEqual(cmd.Args, want) { + t.Fatalf("cmd.Args = %q; want %q", cmd.Args, want) + } +} + +// Contract: psSingleQuote produces a PowerShell single-quoted literal where +// embedded single quotes are doubled — the only escape that quoting form has. +func TestPSSingleQuote(t *testing.T) { + cases := map[string]string{ + `C:\logs\agent.log`: `'C:\logs\agent.log'`, + `C:\it's here\a.log`: `'C:\it''s here\a.log'`, + ``: `''`, + } + for in, want := range cases { + if got := psSingleQuote(in); got != want { + t.Errorf("psSingleQuote(%q) = %s; want %s", in, got, want) + } + } +} diff --git a/control-plane/internal/cli/proc_unix.go b/control-plane/internal/cli/proc_unix.go new file mode 100644 index 000000000..4b3be5e97 --- /dev/null +++ b/control-plane/internal/cli/proc_unix.go @@ -0,0 +1,21 @@ +//go:build !windows + +package cli + +import ( + "os" + "syscall" +) + +// signalGracefulStop asks a process to shut down gracefully. On Unix this +// sends SIGINT (os.Interrupt), matching the historical `af stop` behaviour. +// Callers fall back to process.Kill() when it returns an error. +func signalGracefulStop(process *os.Process) error { + return process.Signal(os.Interrupt) +} + +// isProcessAlive reports whether the process is still running. On Unix, +// signal 0 probes for liveness without delivering an actual signal. +func isProcessAlive(process *os.Process) bool { + return process.Signal(syscall.Signal(0)) == nil +} diff --git a/control-plane/internal/cli/proc_unix_test.go b/control-plane/internal/cli/proc_unix_test.go new file mode 100644 index 000000000..dfcbf2f4c --- /dev/null +++ b/control-plane/internal/cli/proc_unix_test.go @@ -0,0 +1,52 @@ +//go:build !windows + +package cli + +import ( + "os" + "os/exec" + "testing" +) + +// Contract: isProcessAlive must report true for a live process and false once +// the process has exited. `af stop` uses this to decide whether the graceful +// shutdown worked or a force-kill is needed. +func TestIsProcessAlive(t *testing.T) { + self, err := os.FindProcess(os.Getpid()) + if err != nil { + t.Fatalf("FindProcess(self): %v", err) + } + if !isProcessAlive(self) { + t.Fatal("isProcessAlive(current process) = false; want true") + } + + cmd := exec.Command("true") + if err := cmd.Start(); err != nil { + t.Fatalf("start: %v", err) + } + if err := cmd.Wait(); err != nil { + t.Fatalf("wait: %v", err) + } + if isProcessAlive(cmd.Process) { + t.Fatal("isProcessAlive(exited process) = true; want false") + } +} + +// Contract: signalGracefulStop must deliver an interrupt that terminates a +// well-behaved (default-signal-disposition) child process. +func TestSignalGracefulStop(t *testing.T) { + cmd := exec.Command("sleep", "30") + if err := cmd.Start(); err != nil { + t.Fatalf("start: %v", err) + } + if err := signalGracefulStop(cmd.Process); err != nil { + t.Fatalf("signalGracefulStop: %v", err) + } + // The child dies from the signal, so Wait returns a non-nil ExitError. + if err := cmd.Wait(); err == nil { + t.Fatal("child exited cleanly; want interrupt-terminated") + } + if isProcessAlive(cmd.Process) { + t.Fatal("process still alive after graceful stop") + } +} diff --git a/control-plane/internal/cli/proc_windows.go b/control-plane/internal/cli/proc_windows.go new file mode 100644 index 000000000..9cd8aed27 --- /dev/null +++ b/control-plane/internal/cli/proc_windows.go @@ -0,0 +1,38 @@ +//go:build windows + +package cli + +// NOTE: compile-verified only (GOOS=windows cross-build); not yet exercised on +// a real Windows machine. + +import ( + "os" + "os/exec" + "strconv" + "strings" +) + +// signalGracefulStop asks a process to shut down gracefully. Windows cannot +// deliver SIGINT/SIGTERM to an unrelated process (os.Process.Signal only +// supports os.Kill there), so use `taskkill` without /F, which requests the +// target to close. Callers fall back to process.Kill() when it returns an +// error. +func signalGracefulStop(process *os.Process) error { + return exec.Command("taskkill", "/PID", strconv.Itoa(process.Pid)).Run() +} + +// isProcessAlive reports whether the process is still running. On Windows +// os.FindProcess always succeeds and signal-0 probing is unsupported, so ask +// tasklist whether the PID is present. When the probe itself fails, report +// not-alive so callers skip the force-kill rather than killing blindly. +func isProcessAlive(process *os.Process) bool { + out, err := exec.Command( + "tasklist", "/FI", "PID eq "+strconv.Itoa(process.Pid), "/NH", "/FO", "CSV", + ).Output() + if err != nil { + return false + } + // CSV rows quote every field; a live PID appears as "...","",... + // A no-match run prints an INFO message instead and still exits 0. + return strings.Contains(string(out), `"`+strconv.Itoa(process.Pid)+`"`) +} diff --git a/control-plane/internal/cli/stop.go b/control-plane/internal/cli/stop.go index 6e96f3268..f9a13c850 100644 --- a/control-plane/internal/cli/stop.go +++ b/control-plane/internal/cli/stop.go @@ -7,7 +7,6 @@ import ( "net/http" "os" "path/filepath" - "syscall" "time" "github.com/Agent-Field/agentfield/control-plane/internal/packages" @@ -130,8 +129,8 @@ func (as *AgentNodeStopper) StopAgentNode(agentNodeName string) error { if !httpShutdownSuccess { fmt.Printf("🔄 Falling back to process signal shutdown for agent %s\n", agentNodeName) - // Send SIGTERM for graceful shutdown - if err := process.Signal(os.Interrupt); err != nil { + // Ask for graceful shutdown (SIGINT on Unix, taskkill on Windows) + if err := signalGracefulStop(process); err != nil { // If graceful shutdown fails, force kill if err := process.Kill(); err != nil { return fmt.Errorf("failed to kill process: %w", err) @@ -141,7 +140,7 @@ func (as *AgentNodeStopper) StopAgentNode(agentNodeName string) error { time.Sleep(3 * time.Second) // Check if process is still running - if err := process.Signal(syscall.Signal(0)); err == nil { + if isProcessAlive(process) { // Process still running, force kill fmt.Printf("⚠️ Process still running, force killing agent %s\n", agentNodeName) if err := process.Kill(); err != nil { diff --git a/control-plane/internal/core/services/agent_service.go b/control-plane/internal/core/services/agent_service.go index 5abf9a78f..68e39dadb 100644 --- a/control-plane/internal/core/services/agent_service.go +++ b/control-plane/internal/core/services/agent_service.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" "os" "os/exec" @@ -118,10 +119,14 @@ func (as *DefaultAgentService) runAgentGuarded(name string, options domain.RunOp // 5. Wait for agent node to be ready healthPath := "/health" + expectedNodeID := name if metadata, err := packages.ParsePackageMetadata(agentNode.Path); err == nil { healthPath = metadata.HealthcheckPath() + if metadata.AgentNode.NodeID != "" { + expectedNodeID = metadata.AgentNode.NodeID + } } - if err := as.waitForAgentNode(port, healthPath, nodeReadyTimeout()); err != nil { + if err := as.waitForAgentNode(port, healthPath, expectedNodeID, nodeReadyTimeout()); err != nil { // Kill the process if it failed to start properly if stopErr := as.processManager.Stop(pid); stopErr != nil { return nil, fmt.Errorf("agent node failed to start: %w (additionally failed to stop process: %v)", err, stopErr) @@ -521,6 +526,7 @@ func (as *DefaultAgentService) buildProcessConfig(agentNode packages.InstalledPa env = append(env, "AGENTFIELD_STRICT_PORT=1") env = append(env, fmt.Sprintf("AGENTFIELD_SERVER=%s", serverURL)) env = append(env, fmt.Sprintf("AGENTFIELD_SERVER_URL=%s", serverURL)) + env = packages.PythonUTF8Env(env) // Resolve declared variables from the encrypted secret store. Secrets are // injected only into this child process — never written to disk in plaintext. @@ -615,7 +621,7 @@ func (as *DefaultAgentService) buildProcessConfig(agentNode packages.InstalledPa // secret store, prompting for missing required ones. func (as *DefaultAgentService) resolveNodeEnvironment(nodeName string, metadata *packages.PackageMetadata) (map[string]string, error) { env := metadata.UserEnvironment - if len(env.Required) == 0 && len(env.Optional) == 0 { + if len(env.Required) == 0 && len(env.Optional) == 0 && len(env.RequireOneOf) == 0 { return map[string]string{}, nil } store, err := packages.NewSecretStore(as.agentfieldHome) @@ -640,26 +646,39 @@ func nodeReadyTimeout() time.Duration { return 30 * time.Second } -// waitForAgentNode waits for the agent node to become ready -func (as *DefaultAgentService) waitForAgentNode(port int, healthPath string, timeout time.Duration) error { +// waitForAgentNode waits for the agent node to become ready. A 200 on the +// health endpoint is only trusted when the payload's node_id (if it carries +// one) matches the node just started — on Windows the port probe can miss an +// existing listener (no SO_EXCLUSIVEADDRUSE), and without this check a +// squatter's health response makes a dead agent look started. An empty +// expectedNodeID or a payload without node_id skips the identity check. +func (as *DefaultAgentService) waitForAgentNode(port int, healthPath, expectedNodeID string, timeout time.Duration) error { if healthPath == "" { healthPath = "/health" } client := &http.Client{Timeout: 1 * time.Second} deadline := time.Now().Add(timeout) + impostor := "" for time.Now().Before(deadline) { resp, err := client.Get(fmt.Sprintf("http://localhost:%d%s", port, healthPath)) if err == nil && resp.StatusCode == 200 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) resp.Body.Close() - return nil - } - if resp != nil { + got := packages.HealthNodeID(body) + if got == "" || expectedNodeID == "" || packages.NodeIDsEquivalent(got, expectedNodeID) { + return nil + } + impostor = got + } else if resp != nil { resp.Body.Close() } time.Sleep(500 * time.Millisecond) } + if impostor != "" { + return fmt.Errorf("port %d is answering health checks as %q, not %q — another process is using the port", port, impostor, expectedNodeID) + } return fmt.Errorf("agent node did not become ready within %v", timeout) } diff --git a/control-plane/internal/core/services/coverage_additional_test.go b/control-plane/internal/core/services/coverage_additional_test.go index da0bb2d20..c916b1585 100644 --- a/control-plane/internal/core/services/coverage_additional_test.go +++ b/control-plane/internal/core/services/coverage_additional_test.go @@ -104,13 +104,13 @@ func TestAgentServiceWaitForAgentNode(t *testing.T) { defer server.Close() service := &DefaultAgentService{} - require.NoError(t, service.waitForAgentNode(port, "/health", 2*time.Second)) + require.NoError(t, service.waitForAgentNode(port, "/health", "", 2*time.Second)) }) t.Run("timeout", func(t *testing.T) { port := findFreePortInRange(t) service := &DefaultAgentService{} - err := service.waitForAgentNode(port, "/health", 750*time.Millisecond) + err := service.waitForAgentNode(port, "/health", "", 750*time.Millisecond) require.Error(t, err) assert.Contains(t, err.Error(), "did not become ready") }) @@ -233,6 +233,7 @@ func TestPackageServiceHelpers(t *testing.T) { require.NoError(t, os.MkdirAll(fakeBin, 0o755)) python3Path := filepath.Join(fakeBin, "python3") python3Script := fmt.Sprintf(`#!/bin/sh +if [ "$1" = "-c" ]; then echo "3.12.0"; exit 0; fi venv_path="$3" mkdir -p "$venv_path/bin" cat > "$venv_path/bin/pip" <<'EOF' @@ -351,66 +352,6 @@ exit 0 }) } -func TestDevServiceLoadDevEnvFile(t *testing.T) { - dir := t.TempDir() - require.NoError(t, os.WriteFile(filepath.Join(dir, ".env"), []byte(strings.Join([]string{ - "# comment", - "FOO=bar", - `QUOTED="hello world"`, - "SINGLE='quoted'", - "INVALID", - }, "\n")), 0o644)) - - service := &DefaultDevService{} - envVars, err := service.loadDevEnvFile(dir) - require.NoError(t, err) - assert.Equal(t, map[string]string{ - "FOO": "bar", - "QUOTED": "hello world", - "SINGLE": "quoted", - }, envVars) -} - -func TestDevServiceStartDevProcess(t *testing.T) { - dir := t.TempDir() - venvBin := filepath.Join(dir, "venv", "bin") - require.NoError(t, os.MkdirAll(venvBin, 0o755)) - outputPath := filepath.Join(dir, "env-output.txt") - script := fmt.Sprintf("#!/bin/sh\nprintf '%%s\\n' \"$PORT\" > %s\nprintf '%%s\\n' \"$AGENTFIELD_SERVER_URL\" >> %s\nprintf '%%s\\n' \"$AGENTFIELD_DEV_MODE\" >> %s\nprintf '%%s\\n' \"$TOKEN\" >> %s\n", outputPath, outputPath, outputPath, outputPath) - require.NoError(t, os.WriteFile(filepath.Join(venvBin, "python"), []byte(script), 0o755)) - require.NoError(t, os.WriteFile(filepath.Join(dir, ".env"), []byte("TOKEN=dev-secret\n"), 0o644)) - require.NoError(t, os.WriteFile(filepath.Join(dir, "main.py"), []byte("print('ignored')\n"), 0o644)) - - service := &DefaultDevService{} - cmd, err := service.startDevProcess(dir, 8124, domain.DevOptions{Verbose: true}) - require.NoError(t, err) - require.NoError(t, cmd.Wait()) - - data, err := os.ReadFile(outputPath) - require.NoError(t, err) - assert.Equal(t, "8124\nhttp://localhost:8080\ntrue\ndev-secret\n", string(data)) -} - -func TestDevServicePortHelpersWithoutManager(t *testing.T) { - service := &DefaultDevService{} - - port, err := service.getFreePort() - require.NoError(t, err) - assert.True(t, port >= 8001 && port <= 8999) - - busyListener, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - defer busyListener.Close() - busyPort := busyListener.Addr().(*net.TCPAddr).Port - assert.False(t, service.isPortAvailable(busyPort)) - - freeListener, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - freePort := freeListener.Addr().(*net.TCPAddr).Port - require.NoError(t, freeListener.Close()) - assert.True(t, service.isPortAvailable(freePort)) -} - func TestAgentServiceRunStopAndStatusWithLiveProcess(t *testing.T) { t.Run("run agent success", func(t *testing.T) { home := t.TempDir() diff --git a/control-plane/internal/core/services/coverage_gap_test.go b/control-plane/internal/core/services/coverage_gap_test.go index a4a19972d..765b52e63 100644 --- a/control-plane/internal/core/services/coverage_gap_test.go +++ b/control-plane/internal/core/services/coverage_gap_test.go @@ -36,7 +36,9 @@ func TestCoverageGapInstallDependenciesErrors(t *testing.T) { Dependencies: packages.DependencyConfig{Python: []string{"requests"}}, }) require.Error(t, err) - assert.Contains(t, err.Error(), "failed to create virtual environment") + // Both stubs fail to run at all, so interpreter selection reports the + // actionable no-working-interpreter error (the stock-Windows shape). + assert.Contains(t, err.Error(), "no working Python interpreter found on PATH") }) t.Run("requirements installation fails", func(t *testing.T) { @@ -46,6 +48,7 @@ func TestCoverageGapInstallDependenciesErrors(t *testing.T) { require.NoError(t, os.WriteFile(filepath.Join(packagePath, "requirements.txt"), []byte("broken\n"), 0o644)) python3Script := []byte(`#!/bin/sh +if [ "$1" = "-c" ]; then echo "3.12.0"; exit 0; fi mkdir -p "$3/bin" cat > "$3/bin/pip" <<'SH' #!/bin/sh @@ -75,6 +78,7 @@ chmod +x "$3/bin/pip" require.NoError(t, os.MkdirAll(fakeBin, 0o755)) python3Script := []byte(`#!/bin/sh +if [ "$1" = "-c" ]; then echo "3.12.0"; exit 0; fi mkdir -p "$3/bin" cat > "$3/bin/pip" <<'SH' #!/bin/sh diff --git a/control-plane/internal/core/services/dev_service_test.go b/control-plane/internal/core/services/dev_service_test.go index 1d2d996f8..6991bd835 100644 --- a/control-plane/internal/core/services/dev_service_test.go +++ b/control-plane/internal/core/services/dev_service_test.go @@ -5,8 +5,11 @@ package services import ( "context" "errors" + "fmt" + "net" "os" "path/filepath" + "strings" "testing" "time" @@ -15,66 +18,6 @@ import ( "github.com/stretchr/testify/require" ) -// Mock FileSystemAdapter for testing -type mockFileSystemAdapter struct { - readFileFunc func(string) ([]byte, error) - writeFileFunc func(string, []byte) error - existsFunc func(string) bool - createDirFunc func(string) error - listDirectoryFunc func(string) ([]string, error) - files map[string][]byte - directories map[string]bool -} - -func newMockFileSystemAdapter() *mockFileSystemAdapter { - return &mockFileSystemAdapter{ - files: make(map[string][]byte), - directories: make(map[string]bool), - } -} - -func (m *mockFileSystemAdapter) ReadFile(path string) ([]byte, error) { - if m.readFileFunc != nil { - return m.readFileFunc(path) - } - if data, ok := m.files[path]; ok { - return data, nil - } - return nil, errors.New("file not found") -} - -func (m *mockFileSystemAdapter) WriteFile(path string, data []byte) error { - if m.writeFileFunc != nil { - return m.writeFileFunc(path, data) - } - m.files[path] = data - return nil -} - -func (m *mockFileSystemAdapter) Exists(path string) bool { - if m.existsFunc != nil { - return m.existsFunc(path) - } - _, fileExists := m.files[path] - _, dirExists := m.directories[path] - return fileExists || dirExists -} - -func (m *mockFileSystemAdapter) CreateDirectory(path string) error { - if m.createDirFunc != nil { - return m.createDirFunc(path) - } - m.directories[path] = true - return nil -} - -func (m *mockFileSystemAdapter) ListDirectory(path string) ([]string, error) { - if m.listDirectoryFunc != nil { - return m.listDirectoryFunc(path) - } - return []string{}, nil -} - func TestNewDevService(t *testing.T) { processManager := newMockProcessManager() portManager := newMockPortManager() @@ -365,3 +308,69 @@ KEY=value assert.Equal(t, "value", envVars["KEY"]) assert.NotContains(t, envVars, "INVALID_LINE_WITHOUT_EQUALS") } + +// The three tests below were moved from coverage_additional_test.go: they +// exercise Unix-only DefaultDevService methods (loadDevEnvFile, +// startDevProcess, port helpers) that the Windows stub does not define, so +// they must live in this !windows-tagged file for the services package to +// compile under GOOS=windows. + +func TestDevServiceLoadDevEnvFile(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, ".env"), []byte(strings.Join([]string{ + "# comment", + "FOO=bar", + `QUOTED="hello world"`, + "SINGLE='quoted'", + "INVALID", + }, "\n")), 0o644)) + + service := &DefaultDevService{} + envVars, err := service.loadDevEnvFile(dir) + require.NoError(t, err) + assert.Equal(t, map[string]string{ + "FOO": "bar", + "QUOTED": "hello world", + "SINGLE": "quoted", + }, envVars) +} + +func TestDevServiceStartDevProcess(t *testing.T) { + dir := t.TempDir() + venvBin := filepath.Join(dir, "venv", "bin") + require.NoError(t, os.MkdirAll(venvBin, 0o755)) + outputPath := filepath.Join(dir, "env-output.txt") + script := fmt.Sprintf("#!/bin/sh\nprintf '%%s\\n' \"$PORT\" > %s\nprintf '%%s\\n' \"$AGENTFIELD_SERVER_URL\" >> %s\nprintf '%%s\\n' \"$AGENTFIELD_DEV_MODE\" >> %s\nprintf '%%s\\n' \"$TOKEN\" >> %s\n", outputPath, outputPath, outputPath, outputPath) + require.NoError(t, os.WriteFile(filepath.Join(venvBin, "python"), []byte(script), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, ".env"), []byte("TOKEN=dev-secret\n"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "main.py"), []byte("print('ignored')\n"), 0o644)) + + service := &DefaultDevService{} + cmd, err := service.startDevProcess(dir, 8124, domain.DevOptions{Verbose: true}) + require.NoError(t, err) + require.NoError(t, cmd.Wait()) + + data, err := os.ReadFile(outputPath) + require.NoError(t, err) + assert.Equal(t, "8124\nhttp://localhost:8080\ntrue\ndev-secret\n", string(data)) +} + +func TestDevServicePortHelpersWithoutManager(t *testing.T) { + service := &DefaultDevService{} + + port, err := service.getFreePort() + require.NoError(t, err) + assert.True(t, port >= 8001 && port <= 8999) + + busyListener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer busyListener.Close() + busyPort := busyListener.Addr().(*net.TCPAddr).Port + assert.False(t, service.isPortAvailable(busyPort)) + + freeListener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + freePort := freeListener.Addr().(*net.TCPAddr).Port + require.NoError(t, freeListener.Close()) + assert.True(t, service.isPortAvailable(freePort)) +} diff --git a/control-plane/internal/core/services/mocks_fs_test.go b/control-plane/internal/core/services/mocks_fs_test.go new file mode 100644 index 000000000..917ec0aee --- /dev/null +++ b/control-plane/internal/core/services/mocks_fs_test.go @@ -0,0 +1,68 @@ +package services + +// mockFileSystemAdapter is shared by test files across the package. It lives +// in its own untagged file (rather than dev_service_test.go, which is +// !windows) so that the platform-neutral tests that use it still compile +// under GOOS=windows. + +import "errors" + +// Mock FileSystemAdapter for testing +type mockFileSystemAdapter struct { + readFileFunc func(string) ([]byte, error) + writeFileFunc func(string, []byte) error + existsFunc func(string) bool + createDirFunc func(string) error + listDirectoryFunc func(string) ([]string, error) + files map[string][]byte + directories map[string]bool +} + +func newMockFileSystemAdapter() *mockFileSystemAdapter { + return &mockFileSystemAdapter{ + files: make(map[string][]byte), + directories: make(map[string]bool), + } +} + +func (m *mockFileSystemAdapter) ReadFile(path string) ([]byte, error) { + if m.readFileFunc != nil { + return m.readFileFunc(path) + } + if data, ok := m.files[path]; ok { + return data, nil + } + return nil, errors.New("file not found") +} + +func (m *mockFileSystemAdapter) WriteFile(path string, data []byte) error { + if m.writeFileFunc != nil { + return m.writeFileFunc(path, data) + } + m.files[path] = data + return nil +} + +func (m *mockFileSystemAdapter) Exists(path string) bool { + if m.existsFunc != nil { + return m.existsFunc(path) + } + _, fileExists := m.files[path] + _, dirExists := m.directories[path] + return fileExists || dirExists +} + +func (m *mockFileSystemAdapter) CreateDirectory(path string) error { + if m.createDirFunc != nil { + return m.createDirFunc(path) + } + m.directories[path] = true + return nil +} + +func (m *mockFileSystemAdapter) ListDirectory(path string) ([]string, error) { + if m.listDirectoryFunc != nil { + return m.listDirectoryFunc(path) + } + return []string{}, nil +} diff --git a/control-plane/internal/infrastructure/process/port_manager.go b/control-plane/internal/infrastructure/process/port_manager.go index f1385c851..c931dd50e 100644 --- a/control-plane/internal/infrastructure/process/port_manager.go +++ b/control-plane/internal/infrastructure/process/port_manager.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "sync" + "time" "github.com/Agent-Field/agentfield/control-plane/internal/core/interfaces" ) @@ -48,6 +49,14 @@ func (pm *DefaultPortManager) IsPortAvailable(port int) bool { } // If listening was successful, close the listener immediately as we only wanted to check. listener.Close() + // A successful bind is not proof on Windows: without SO_EXCLUSIVEADDRUSE a + // probe bind can succeed while another process is actively listening on + // the same port (observed with uvicorn agent nodes). If something accepts + // a connection, the port is taken. + if dial, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 250*time.Millisecond); err == nil { + dial.Close() + return false + } return true } diff --git a/control-plane/internal/packages/exe_suffix_test.go b/control-plane/internal/packages/exe_suffix_test.go new file mode 100644 index 000000000..7e45d1a61 --- /dev/null +++ b/control-plane/internal/packages/exe_suffix_test.go @@ -0,0 +1,115 @@ +package packages + +import ( + "os" + "path/filepath" + "runtime" + "testing" +) + +// Contract: compiled Go node binaries get the conventional .exe extension on +// Windows and stay untouched elsewhere; already-suffixed paths (any case) are +// never double-suffixed. +func TestWithExeSuffixFor(t *testing.T) { + cases := []struct { + name string + goos string + in string + want string + }{ + {"windows adds exe", "windows", "bin/swe-planner", "bin/swe-planner.exe"}, + {"windows keeps existing exe", "windows", "bin/swe-planner.exe", "bin/swe-planner.exe"}, + {"windows keeps uppercase exe", "windows", "bin/APP.EXE", "bin/APP.EXE"}, + {"linux untouched", "linux", "bin/swe-planner", "bin/swe-planner"}, + {"darwin untouched", "darwin", "bin/swe-planner", "bin/swe-planner"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := withExeSuffixFor(tc.goos, tc.in); got != tc.want { + t.Fatalf("withExeSuffixFor(%q, %q) = %q; want %q", tc.goos, tc.in, got, tc.want) + } + }) + } +} + +// Contract: an unusable build-package base falls back to bin/app (suffixed on +// Windows like every other derived binary name). +func TestDefaultGoBinNameFallback(t *testing.T) { + want := withExeSuffix(filepath.Join("bin", "app")) + if got := defaultGoBinName("..."); got != want { + t.Fatalf("defaultGoBinName(\"...\") = %q; want %q", got, want) + } +} + +// Contract: on Windows, a manifest's unix-style start path ("bin/app") +// resolves to the .exe the install-time build produced when the extensionless +// file is absent; everywhere else (and whenever the extensionless file exists) +// the plain resolved path wins. +func TestGoBinaryProgramForWindowsExeFallback(t *testing.T) { + writeFile := func(t *testing.T, dir, rel string) { + t.Helper() + p := filepath.Join(dir, rel) + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(p, []byte("bin"), 0o755); err != nil { + t.Fatal(err) + } + } + + t.Run("windows falls back to built exe", func(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, filepath.Join("bin", "app.exe")) + want := filepath.Join(dir, "bin", "app.exe") + if got := goBinaryProgramFor("windows", dir, "bin/app"); got != want { + t.Fatalf("goBinaryProgramFor = %q; want %q", got, want) + } + }) + + t.Run("windows prefers extensionless file when present", func(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, filepath.Join("bin", "app")) + writeFile(t, dir, filepath.Join("bin", "app.exe")) + want := filepath.Join(dir, "bin", "app") + if got := goBinaryProgramFor("windows", dir, "bin/app"); got != want { + t.Fatalf("goBinaryProgramFor = %q; want %q", got, want) + } + }) + + t.Run("windows with neither file returns resolved path", func(t *testing.T) { + dir := t.TempDir() + want := filepath.Join(dir, "bin", "app") + if got := goBinaryProgramFor("windows", dir, "bin/app"); got != want { + t.Fatalf("goBinaryProgramFor = %q; want %q", got, want) + } + }) + + t.Run("non-windows never substitutes exe", func(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, filepath.Join("bin", "app.exe")) + want := filepath.Join(dir, "bin", "app") + if got := goBinaryProgramFor("linux", dir, "bin/app"); got != want { + t.Fatalf("goBinaryProgramFor = %q; want %q", got, want) + } + }) + + t.Run("bare and special program tokens pass through", func(t *testing.T) { + dir := t.TempDir() + for _, program := range []string{"", "go", "app"} { + if got := goBinaryProgramFor("windows", dir, program); got != program { + t.Fatalf("goBinaryProgramFor(%q) = %q; want it unchanged", program, got) + } + } + abs := filepath.Join(dir, "bin", "app") + if got := goBinaryProgramFor("windows", dir, abs); got != abs { + t.Fatalf("goBinaryProgramFor(abs) = %q; want %q", got, abs) + } + }) + + t.Run("exported wrapper uses host GOOS", func(t *testing.T) { + dir := t.TempDir() + if got, want := GoBinaryProgram(dir, "bin/app"), goBinaryProgramFor(runtime.GOOS, dir, "bin/app"); got != want { + t.Fatalf("GoBinaryProgram = %q; want %q", got, want) + } + }) +} diff --git a/control-plane/internal/packages/gointerp.go b/control-plane/internal/packages/gointerp.go index e5e90bf79..212c7d5dc 100644 --- a/control-plane/internal/packages/gointerp.go +++ b/control-plane/internal/packages/gointerp.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strconv" "strings" ) @@ -221,19 +222,37 @@ func (m *PackageMetadata) goBuildTarget() (buildPkg, outBin string) { // The output is the start token when start is a bare binary path (not a // `go run ...` command). Otherwise derive a sensible default under bin/. if len(start) > 0 && start[0] != "go" { - return build, start[0] + return build, withExeSuffix(start[0]) } return build, defaultGoBinName(build) } // defaultGoBinName derives a bin/ output path from a Go build package spec -// like "./cmd/swe-planner" -> "bin/swe-planner". +// like "./cmd/swe-planner" -> "bin/swe-planner" ("bin/swe-planner.exe" on +// Windows). func defaultGoBinName(buildPkg string) string { name := filepath.Base(strings.TrimSpace(buildPkg)) if name == "" || name == "." || name == "/" || name == "..." { name = "app" } - return filepath.Join("bin", name) + return withExeSuffix(filepath.Join("bin", name)) +} + +// withExeSuffix appends ".exe" to a binary path on Windows (manifests declare +// unix-style paths like "bin/swe-planner"; the compiled output must carry the +// conventional executable extension there). A no-op on other platforms and on +// paths that already end in .exe. +func withExeSuffix(path string) string { + return withExeSuffixFor(runtime.GOOS, path) +} + +// withExeSuffixFor is withExeSuffix for an explicit GOOS, pure so the Windows +// branch is unit-testable on any platform. +func withExeSuffixFor(goos, path string) string { + if goos == "windows" && !strings.EqualFold(filepath.Ext(path), ".exe") { + return path + ".exe" + } + return path } // hasVendorDir reports whether the package ships a Go vendor/ directory, which @@ -365,11 +384,26 @@ func applyGoReplaceOverrides(goCmd, packagePath string) error { // already-absolute path is returned unchanged. It is the Go counterpart to the // venv-python substitution the runner does for Python nodes. func GoBinaryProgram(packageDir, program string) string { + return goBinaryProgramFor(runtime.GOOS, packageDir, program) +} + +// goBinaryProgramFor is GoBinaryProgram for an explicit GOOS, pure so the +// Windows .exe fallback is unit-testable on any platform. +func goBinaryProgramFor(goos, packageDir, program string) string { if program == "" || program == "go" || filepath.IsAbs(program) { return program } if strings.ContainsRune(program, '/') || strings.ContainsRune(program, filepath.Separator) { - return filepath.Join(packageDir, program) + resolved := filepath.Join(packageDir, program) + // Manifests declare unix-style binary paths ("bin/swe-planner"); on + // Windows the install-time build produced "bin/swe-planner.exe", so + // resolve to that when the extensionless path is absent. + if !fileExists(resolved) { + if withExe := withExeSuffixFor(goos, resolved); withExe != resolved && fileExists(withExe) { + return withExe + } + } + return resolved } return program } diff --git a/control-plane/internal/packages/install_deps_test.go b/control-plane/internal/packages/install_deps_test.go index 2f80bcc60..446332e02 100644 --- a/control-plane/internal/packages/install_deps_test.go +++ b/control-plane/internal/packages/install_deps_test.go @@ -9,11 +9,14 @@ import ( // fakePythonOnPath installs a stub `python3` whose `-m venv ` creates a // venv layout with a no-op `pip`, so dependency installation can be exercised -// offline without invoking real Python/pip. +// offline without invoking real Python/pip. It also answers the `-c` version +// probe, since interpreter selection only accepts candidates that actually +// run and report a version (the guard against Windows Store alias stubs). func fakePythonOnPath(t *testing.T) { t.Helper() binDir := t.TempDir() py := "#!/bin/sh\n" + + "if [ \"$1\" = \"-c\" ]; then echo \"3.12.0\"; exit 0; fi\n" + "if [ \"$1\" = \"-m\" ] && [ \"$2\" = \"venv\" ]; then\n" + " mkdir -p \"$3/bin\"\n" + " printf '#!/bin/sh\\nexit 0\\n' > \"$3/bin/pip\"\n" + diff --git a/control-plane/internal/packages/installer.go b/control-plane/internal/packages/installer.go index 45b66d175..12261a0cf 100644 --- a/control-plane/internal/packages/installer.go +++ b/control-plane/internal/packages/installer.go @@ -855,20 +855,21 @@ func InstallPythonDependencies(packagePath string, pyDeps, systemDeps []string) return err } - var cmd *exec.Cmd - if interp != "" { - cmd = exec.Command(interp, "-m", "venv", venvPath) - if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to create virtual environment with %s: %w\nOutput: %s", interp, err, output) - } - } else { - cmd = exec.Command("python3", "-m", "venv", venvPath) - if _, err := cmd.CombinedOutput(); err != nil { - cmd = exec.Command("python", "-m", "venv", venvPath) - if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to create virtual environment: %w\nOutput: %s", err, output) - } + if interp == "" { + // Legacy path (no requires-python declared): pick the first ambient + // interpreter that actually runs. Blindly trying "python3 -m venv" + // first breaks on stock Windows, where python3 (and often python) is + // a Microsoft Store stub that exits 9009; on default python.org + // installs (PATH box unchecked) only the "py" launcher works. + ambient, _, ok := ambientPythonInterpreter() + if !ok { + return fmt.Errorf("no working Python interpreter found on PATH (tried %s) - install Python 3, ensure it is on PATH, then run `af install` again", strings.Join(pythonCandidates, ", ")) } + interp = ambient + } + cmd := exec.Command(interp, "-m", "venv", venvPath) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to create virtual environment with %s: %w\nOutput: %s", interp, err, output) } pipPath := filepath.Join(venvPath, "bin", "pip") diff --git a/control-plane/internal/packages/installer_additional_test.go b/control-plane/internal/packages/installer_additional_test.go index b94628097..102686609 100644 --- a/control-plane/internal/packages/installer_additional_test.go +++ b/control-plane/internal/packages/installer_additional_test.go @@ -39,6 +39,10 @@ exit 0 pythonImpl := `#!/usr/bin/env bash set -eu +if [ "${1:-}" = "-c" ]; then + echo "3.12.0" + exit 0 +fi if [ "${1:-}" = "-m" ] && [ "${2:-}" = "venv" ]; then venv_path="${3:-}" mkdir -p "$venv_path/bin" @@ -104,11 +108,14 @@ func TestInstallDependenciesAdditionalCoverage(t *testing.T) { }, }, { + // Both candidates are dead stubs (the stock-Windows shape: Store + // aliases that exit non-zero) — interpreter selection must fail with + // the actionable no-working-interpreter error before venv creation. name: "venv creation failure", withRequirements: true, failPython3: true, failPython: true, - wantErr: "failed to create virtual environment", + wantErr: "no working Python interpreter found on PATH", }, { name: "requirements install failure", diff --git a/control-plane/internal/packages/installer_coverage_test.go b/control-plane/internal/packages/installer_coverage_test.go index f2d5e67ff..90723b969 100644 --- a/control-plane/internal/packages/installer_coverage_test.go +++ b/control-plane/internal/packages/installer_coverage_test.go @@ -58,7 +58,7 @@ func TestInstallerCoverage(t *testing.T) { t.Setenv("PATH", "") err := pi.installDependencies(pkgPath, metadata) - if err == nil || !strings.Contains(err.Error(), "failed to create virtual environment") { + if err == nil || !strings.Contains(err.Error(), "no working Python interpreter found on PATH") { t.Fatalf("expected python to fail, got %v", err) } }) @@ -72,10 +72,12 @@ func TestInstallerCoverage(t *testing.T) { }, } - // create a fake python that will succeed, but no pip + // create a fake python that answers the version probe and "succeeds" + // at venv creation without producing a pip fakebin := t.TempDir() pythonPath := filepath.Join(fakebin, "python3") - if err := os.WriteFile(pythonPath, []byte("#!/bin/sh\nexit 0"), 0755); err != nil { + script := "#!/bin/sh\nif [ \"$1\" = \"-c\" ]; then echo \"3.12.0\"; fi\nexit 0" + if err := os.WriteFile(pythonPath, []byte(script), 0755); err != nil { t.Fatal(err) } t.Setenv("PATH", fakebin) diff --git a/control-plane/internal/packages/node_identity.go b/control-plane/internal/packages/node_identity.go new file mode 100644 index 000000000..8c6921ec2 --- /dev/null +++ b/control-plane/internal/packages/node_identity.go @@ -0,0 +1,29 @@ +package packages + +import ( + "encoding/json" + "strings" +) + +// HealthNodeID extracts the node_id field from an agent health payload. +// Returns "" when the body is not JSON or carries no node_id — custom +// healthcheck endpoints are not required to identify themselves. +func HealthNodeID(body []byte) string { + var payload struct { + NodeID string `json:"node_id"` + } + if err := json.Unmarshal(body, &payload); err != nil { + return "" + } + return payload.NodeID +} + +// NodeIDsEquivalent compares node identifiers with the same tolerance the +// registry uses for name drift: case-insensitive, hyphens and underscores +// interchangeable. +func NodeIDsEquivalent(a, b string) bool { + fold := func(s string) string { + return strings.ToLower(strings.ReplaceAll(s, "-", "_")) + } + return fold(a) == fold(b) +} diff --git a/control-plane/internal/packages/node_identity_test.go b/control-plane/internal/packages/node_identity_test.go new file mode 100644 index 000000000..e5c148f67 --- /dev/null +++ b/control-plane/internal/packages/node_identity_test.go @@ -0,0 +1,114 @@ +package packages + +import ( + "fmt" + "net" + "net/http" + "strings" + "testing" + "time" +) + +func TestHealthNodeID(t *testing.T) { + cases := []struct { + body string + want string + }{ + {`{"status":"healthy","node_id":"swe-planner"}`, "swe-planner"}, + {`{"status":"healthy"}`, ""}, + {`not json`, ""}, + {``, ""}, + } + for _, c := range cases { + if got := HealthNodeID([]byte(c.body)); got != c.want { + t.Errorf("HealthNodeID(%q) = %q, want %q", c.body, got, c.want) + } + } +} + +func TestNodeIDsEquivalent(t *testing.T) { + if !NodeIDsEquivalent("swe-planner", "swe_planner") { + t.Error("hyphen/underscore drift should be equivalent") + } + if !NodeIDsEquivalent("Agent", "agent") { + t.Error("case drift should be equivalent") + } + if NodeIDsEquivalent("swe-planner", "smoke-agent") { + t.Error("different nodes must not be equivalent") + } +} + +// Contract: a healthy response from a DIFFERENT node on the expected port is +// not readiness — it is a squatter, and the error must say so. This is the +// live-caught Windows failure where the port probe misses an existing +// listener and a second agent's readiness poll hits the first agent. +func TestWaitForAgentNode_RejectsSquatter(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + port := listener.Addr().(*net.TCPAddr).Port + mux := http.NewServeMux() + mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) { + fmt.Fprint(w, `{"status":"healthy","node_id":"squatter-agent"}`) + }) + server := &http.Server{Handler: mux} + go func() { _ = server.Serve(listener) }() + t.Cleanup(func() { _ = server.Close() }) + + ar := &AgentNodeRunner{} + err = ar.waitForAgentNode(port, "/health", "real-agent", 1200*time.Millisecond) + if err == nil { + t.Fatal("expected squatter rejection, got success") + } + if !strings.Contains(err.Error(), "squatter-agent") || !strings.Contains(err.Error(), "real-agent") { + t.Fatalf("error should name both nodes, got: %v", err) + } +} + +// Contract: a matching node_id (including hyphen/underscore drift) and a +// payload without node_id both count as ready — custom healthchecks are not +// required to identify themselves. +func TestWaitForAgentNode_AcceptsMatchAndAnonymous(t *testing.T) { + for _, body := range []string{ + `{"status":"healthy","node_id":"real_agent"}`, + `{"status":"healthy"}`, + } { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + port := listener.Addr().(*net.TCPAddr).Port + payload := body + mux := http.NewServeMux() + mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) { + fmt.Fprint(w, payload) + }) + server := &http.Server{Handler: mux} + go func() { _ = server.Serve(listener) }() + + ar := &AgentNodeRunner{} + err = ar.waitForAgentNode(port, "/health", "real-agent", 2*time.Second) + _ = server.Close() + if err != nil { + t.Fatalf("body %q: expected ready, got %v", payload, err) + } + } +} + +// Contract: a port with an active listener is not available, even when a +// probe bind would succeed (the Windows no-SO_EXCLUSIVEADDRUSE case) — the +// dial probe must catch it. +func TestIsPortAvailable_DetectsActiveListener(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer listener.Close() + port := listener.Addr().(*net.TCPAddr).Port + + ar := &AgentNodeRunner{} + if ar.isPortAvailable(port) { + t.Fatalf("port %d has an active listener but was reported available", port) + } +} diff --git a/control-plane/internal/packages/pyinterp.go b/control-plane/internal/packages/pyinterp.go index 1c96a297d..516012f8c 100644 --- a/control-plane/internal/packages/pyinterp.go +++ b/control-plane/internal/packages/pyinterp.go @@ -212,11 +212,9 @@ func resolveVenvInterpreter(packagePath string) (string, error) { } // 1. Ambient interpreter already satisfies? - ambient := firstOnPath("python3", "python") - if ambient != "" { - if v, ok := interpreterVersion(ambient); ok && constraintSatisfied(v, requires) { - return ambient, nil - } + ambient, ambientV, ambientOK := ambientPythonInterpreter() + if ambientOK && constraintSatisfied(ambientV, requires) { + return ambient, nil } // 2. Provision via uv (downloads a standalone interpreter if needed). @@ -233,11 +231,9 @@ func resolveVenvInterpreter(packagePath string) (string, error) { return interp, nil } - found := "no python3 found on PATH" - if ambient != "" { - if v, ok := interpreterVersion(ambient); ok { - found = fmt.Sprintf("found %s (Python %s)", ambient, v) - } + found := "no working python3/python found on PATH" + if ambientOK { + found = fmt.Sprintf("found %s (Python %s)", ambient, ambientV) } req := uvRequest(requires) if req == "" { @@ -254,6 +250,8 @@ func resolveVenvInterpreter(packagePath string) (string, error) { } // firstOnPath returns the first of the given commands found on PATH, or "". +// Note: existence on PATH does not prove the command runs — for Python +// candidates use ambientPythonInterpreter, which also probes execution. func firstOnPath(cmds ...string) string { for _, c := range cmds { if _, err := exec.LookPath(c); err == nil { @@ -263,6 +261,31 @@ func firstOnPath(cmds ...string) string { return "" } +// pythonCandidates are the interpreter commands probed for an ambient Python, +// in preference order. "py" is the Windows launcher, which python.org +// installers register even when "add python to PATH" is left unchecked (the +// default) — on such machines it is the only working entry. It does not exist +// on Unix, so probing it there is a no-op. +var pythonCandidates = []string{"python3", "python", "py"} + +// ambientPythonInterpreter returns the first pythonCandidates entry that is +// both on PATH and actually runs, along with its reported version. Merely +// existing on PATH is not enough: stock Windows ships Microsoft Store "app +// execution alias" stubs named python3.exe/python.exe that exec.LookPath +// resolves like real binaries but that exit 9009 without running anything. +// Probing with interpreterVersion filters those out. +func ambientPythonInterpreter() (string, pyVersion, bool) { + for _, c := range pythonCandidates { + if _, err := exec.LookPath(c); err != nil { + continue + } + if v, ok := interpreterVersion(c); ok { + return c, v, true + } + } + return "", pyVersion{}, false +} + // displayVersion returns interp's version as a string for logging, or the // interpreter path itself when the version cannot be determined. func displayVersion(interp string) string { diff --git a/control-plane/internal/packages/pyinterp_test.go b/control-plane/internal/packages/pyinterp_test.go index 800d40467..a6a615544 100644 --- a/control-plane/internal/packages/pyinterp_test.go +++ b/control-plane/internal/packages/pyinterp_test.go @@ -145,6 +145,26 @@ func TestResolveVenvInterpreter(t *testing.T) { } }) + // The stock-Windows scenario: python3 resolves on PATH but is the + // Microsoft Store app-execution-alias stub, which exits non-zero without + // running anything. Resolution must skip it and use the real python. + t.Run("skips a dead python3 stub and uses the working python", func(t *testing.T) { + bin := t.TempDir() + stubBrokenPython(t, bin, "python3") + stubPython(t, bin, "python", "3.12.5") + t.Setenv("PATH", bin) + dir := t.TempDir() + writeFile(t, dir, "pyproject.toml", "[project]\nrequires-python = \">=3.12\"\n") + + interp, err := resolveVenvInterpreter(dir) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if interp != "python" { + t.Fatalf("got %q; want python (the working interpreter after the dead python3 stub)", interp) + } + }) + t.Run("incompatible and no provisioner -> actionable error", func(t *testing.T) { bin := t.TempDir() stubPython(t, bin, "python3", "3.10.13") // too old, no uv/pyenv on PATH @@ -185,6 +205,85 @@ func TestResolveVenvInterpreter(t *testing.T) { }) } +// TestAmbientPythonInterpreter pins the candidate-probing contract: a PATH +// hit alone is not enough — the interpreter must actually run. This is what +// keeps stock-Windows Store aliases (python3.exe stubs that exit 9009) from +// being selected, and what lets the "py" launcher serve as a last resort. +func TestAmbientPythonInterpreter(t *testing.T) { + t.Run("prefers python3 when it works", func(t *testing.T) { + bin := t.TempDir() + stubPython(t, bin, "python3", "3.12.5") + stubPython(t, bin, "python", "3.10.1") + t.Setenv("PATH", bin) + interp, v, ok := ambientPythonInterpreter() + if !ok || interp != "python3" || v.String() != "3.12.5" { + t.Fatalf("got (%q,%v,%v); want (python3,3.12.5,true)", interp, v, ok) + } + }) + + t.Run("skips dead stubs in order python3 -> python -> py", func(t *testing.T) { + bin := t.TempDir() + stubBrokenPython(t, bin, "python3") + stubBrokenPython(t, bin, "python") + stubPython(t, bin, "py", "3.11.9") + t.Setenv("PATH", bin) + interp, v, ok := ambientPythonInterpreter() + if !ok || interp != "py" || v.String() != "3.11.9" { + t.Fatalf("got (%q,%v,%v); want (py,3.11.9,true)", interp, v, ok) + } + }) + + t.Run("reports not-ok when nothing on PATH runs", func(t *testing.T) { + bin := t.TempDir() + stubBrokenPython(t, bin, "python3") + t.Setenv("PATH", bin) + if interp, _, ok := ambientPythonInterpreter(); ok { + t.Fatalf("got (%q,true); want not-ok", interp) + } + }) +} + +// TestInstallPythonDependencies_LegacyPathSkipsDeadStub drives the public API +// through the no-requires-python branch: the dead python3 stub must be +// skipped and the venv built with the working python. +func TestInstallPythonDependencies_LegacyPathSkipsDeadStub(t *testing.T) { + bin := t.TempDir() + stubBrokenPython(t, bin, "python3") + stubVenvPython(t, bin, "python", "3.12.5") + t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) + pkg := t.TempDir() + writeFile(t, pkg, "requirements.txt", "") + + if err := InstallPythonDependencies(pkg, nil, nil); err != nil { + t.Fatalf("InstallPythonDependencies: %v", err) + } + if _, err := os.Stat(filepath.Join(pkg, "venv", "bin", "pip")); err != nil { + t.Fatalf("expected a venv built via the working interpreter: %v", err) + } +} + +// TestInstallPythonDependencies_LegacyPathNoWorkingInterpreter pins the +// actionable error when every candidate is a dead stub (a stock Windows +// machine with no Python installed). +func TestInstallPythonDependencies_LegacyPathNoWorkingInterpreter(t *testing.T) { + bin := t.TempDir() + stubBrokenPython(t, bin, "python3") + stubBrokenPython(t, bin, "python") + stubBrokenPython(t, bin, "py") + t.Setenv("PATH", bin) + pkg := t.TempDir() + writeFile(t, pkg, "requirements.txt", "") + + err := InstallPythonDependencies(pkg, nil, nil) + if err == nil { + t.Fatal("expected an error when no candidate interpreter runs") + } + msg := err.Error() + if !strings.Contains(msg, "python3, python, py") || !strings.Contains(msg, "af install") { + t.Fatalf("error should list the probed candidates and tell the user what to do: %q", msg) + } +} + func TestProvisionViaPyenv(t *testing.T) { bin := t.TempDir() root := t.TempDir() @@ -273,6 +372,17 @@ func stubPython(t *testing.T, dir, name, version string) string { return p } +// stubBrokenPython writes an executable that mimics the Microsoft Store app +// execution alias: it prints the Store hint and exits non-zero without ever +// behaving like an interpreter. +func stubBrokenPython(t *testing.T, dir, name string) { + t.Helper() + script := "#!/bin/sh\n" + + "echo 'Python was not found; run without arguments to install from the Microsoft Store' >&2\n" + + "exit 49\n" + writeExecutable(t, filepath.Join(dir, name), script) +} + // stubUv writes a fake `uv` that returns findPath for `uv python find ...`. func stubUv(t *testing.T, dir, findPath string) { t.Helper() diff --git a/control-plane/internal/packages/runner.go b/control-plane/internal/packages/runner.go index edbc32974..588d13649 100644 --- a/control-plane/internal/packages/runner.go +++ b/control-plane/internal/packages/runner.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net" "net/http" "os" @@ -74,10 +75,14 @@ func (ar *AgentNodeRunner) runAgentNode(agentNodeName string, inProgress map[str // 5. Wait for agent node to be ready healthPath := "/health" + expectedNodeID := agentNodeName if metadata, err := ParsePackageMetadata(agentNode.Path); err == nil { healthPath = metadata.HealthcheckPath() + if metadata.AgentNode.NodeID != "" { + expectedNodeID = metadata.AgentNode.NodeID + } } - if err := ar.waitForAgentNode(port, healthPath, 10*time.Second); err != nil { + if err := ar.waitForAgentNode(port, healthPath, expectedNodeID, 10*time.Second); err != nil { if killErr := cmd.Process.Kill(); killErr != nil && !errors.Is(killErr, os.ErrProcessDone) { fmt.Printf("⚠️ Failed to kill agent node process: %v\n", killErr) } @@ -120,6 +125,14 @@ func (ar *AgentNodeRunner) isPortAvailable(port int) bool { return false } conn.Close() + // A successful bind is not proof on Windows: without SO_EXCLUSIVEADDRUSE + // a probe bind can succeed while another process is actively listening on + // the same port (observed with uvicorn agent nodes). If something accepts + // a connection, the port is taken. + if dial, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 250*time.Millisecond); err == nil { + dial.Close() + return false + } return true } @@ -141,6 +154,7 @@ func (ar *AgentNodeRunner) startAgentNodeProcess(agentNode InstalledPackage, por env = append(env, fmt.Sprintf("PORT=%d", port)) env = append(env, fmt.Sprintf("AGENTFIELD_SERVER=%s", serverURL)) env = append(env, fmt.Sprintf("AGENTFIELD_SERVER_URL=%s", serverURL)) + env = PythonUTF8Env(env) // Resolve declared variables from the encrypted secret store, prompting for // missing required ones and persisting them. Secrets are only ever injected @@ -232,6 +246,21 @@ func (ar *AgentNodeRunner) startNodeDependencies(node InstalledPackage, inProgre } } +// PythonUTF8Env appends PYTHONUTF8=1 unless the environment already pins a +// value. Spawned agents log through a redirected stdout/stderr; on Windows +// Python then encodes with the legacy ANSI code page (e.g. cp1252), which +// cannot represent the SDK's emoji log prefixes and floods the log file with +// UnicodeEncodeError tracebacks. UTF-8 mode is a no-op where UTF-8 is already +// the default, so this is safe to set on every platform. +func PythonUTF8Env(env []string) []string { + for _, kv := range env { + if strings.HasPrefix(kv, "PYTHONUTF8=") { + return env + } + } + return append(env, "PYTHONUTF8=1") +} + // venvPython returns the venv python interpreter path, or "" if no venv exists. func venvPython(venvPath string) string { if p := filepath.Join(venvPath, "bin", "python"); fileExists(p) { @@ -252,7 +281,7 @@ func fileExists(path string) bool { // secret store, prompting for missing required ones. func (ar *AgentNodeRunner) resolveEnvironment(nodeName string, metadata *PackageMetadata) (map[string]string, error) { env := metadata.UserEnvironment - if len(env.Required) == 0 && len(env.Optional) == 0 { + if len(env.Required) == 0 && len(env.Optional) == 0 && len(env.RequireOneOf) == 0 { return map[string]string{}, nil } store, err := NewSecretStore(ar.AgentFieldHome) @@ -263,26 +292,39 @@ func (ar *AgentNodeRunner) resolveEnvironment(nodeName string, metadata *Package return resolver.Resolve(env) } -// waitForAgentNode waits for the agent node to become ready -func (ar *AgentNodeRunner) waitForAgentNode(port int, healthPath string, timeout time.Duration) error { +// waitForAgentNode waits for the agent node to become ready. A 200 on the +// health endpoint is only trusted when the payload's node_id (if it carries +// one) matches the node just started — on Windows the port probe can miss an +// existing listener (no SO_EXCLUSIVEADDRUSE), and without this check a +// squatter's health response makes a dead agent look started. An empty +// expectedNodeID or a payload without node_id skips the identity check. +func (ar *AgentNodeRunner) waitForAgentNode(port int, healthPath, expectedNodeID string, timeout time.Duration) error { if healthPath == "" { healthPath = "/health" } client := &http.Client{Timeout: 1 * time.Second} deadline := time.Now().Add(timeout) + impostor := "" for time.Now().Before(deadline) { resp, err := client.Get(fmt.Sprintf("http://localhost:%d%s", port, healthPath)) if err == nil && resp.StatusCode == 200 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) resp.Body.Close() - return nil - } - if resp != nil { + got := HealthNodeID(body) + if got == "" || expectedNodeID == "" || NodeIDsEquivalent(got, expectedNodeID) { + return nil + } + impostor = got + } else if resp != nil { resp.Body.Close() } time.Sleep(500 * time.Millisecond) } + if impostor != "" { + return fmt.Errorf("port %d is answering health checks as %q, not %q — another process is using the port", port, impostor, expectedNodeID) + } return fmt.Errorf("agent node did not become ready within %v", timeout) } diff --git a/control-plane/internal/packages/runner_additional_test.go b/control-plane/internal/packages/runner_additional_test.go index 6f68d9c00..c05897008 100644 --- a/control-plane/internal/packages/runner_additional_test.go +++ b/control-plane/internal/packages/runner_additional_test.go @@ -32,6 +32,32 @@ func TestResolveEnvironment_NoDeclaredEnv(t *testing.T) { } } +// Contract: a manifest that declares ONLY require_one_of groups (no required/ +// optional lists) still goes through the resolver — a satisfied group's value +// is injected, and an unsatisfied group errors instead of being skipped. +func TestResolveEnvironment_RequireOneOfAloneIsResolved(t *testing.T) { + t.Setenv("ONE_OF_OPTION_B", "chosen") + ar := &AgentNodeRunner{AgentFieldHome: t.TempDir()} + meta := &PackageMetadata{ + UserEnvironment: UserEnvironmentConfig{ + RequireOneOf: []RequireOneOfGroup{{ + ID: "provider", + Options: []UserEnvironmentVar{ + {Name: "ONE_OF_OPTION_A"}, + {Name: "ONE_OF_OPTION_B"}, + }, + }}, + }, + } + env, err := ar.resolveEnvironment("demo", meta) + if err != nil { + t.Fatalf("resolveEnvironment: %v", err) + } + if env["ONE_OF_OPTION_B"] != "chosen" { + t.Fatalf("ONE_OF_OPTION_B = %q, want chosen", env["ONE_OF_OPTION_B"]) + } +} + // Contract: a declared required variable already present in the process // environment is resolved from there (no prompt needed) via the secret store. func TestResolveEnvironment_ResolvesFromProcessEnv(t *testing.T) { @@ -213,7 +239,7 @@ dependencies: // times out when nothing is listening. func TestWaitForAgentNode_DefaultHealthPath(t *testing.T) { ar := &AgentNodeRunner{} - err := ar.waitForAgentNode(1, "", 10*time.Millisecond) + err := ar.waitForAgentNode(1, "", "", 10*time.Millisecond) if err == nil { t.Fatalf("expected timeout when nothing is listening") } diff --git a/control-plane/internal/packages/runner_coverage_test.go b/control-plane/internal/packages/runner_coverage_test.go index ca3a1ea98..450beed7f 100644 --- a/control-plane/internal/packages/runner_coverage_test.go +++ b/control-plane/internal/packages/runner_coverage_test.go @@ -123,7 +123,7 @@ func TestRunnerErrorCases(t *testing.T) { t.Run("waitForAgentNode-timeout", func(t *testing.T) { ar := &AgentNodeRunner{} // just use a port that is not listening - err := ar.waitForAgentNode(1, "/health", 10*time.Millisecond) + err := ar.waitForAgentNode(1, "/health", "", 10*time.Millisecond) if err == nil { t.Fatal("expected timeout error") } diff --git a/control-plane/internal/packages/runner_test.go b/control-plane/internal/packages/runner_test.go index 7a95bfa2e..64624cc32 100644 --- a/control-plane/internal/packages/runner_test.go +++ b/control-plane/internal/packages/runner_test.go @@ -79,6 +79,23 @@ func TestAgentNodeRunnerPortEnvAndRegistry(t *testing.T) { } } +func TestPythonUTF8Env(t *testing.T) { + t.Run("appends PYTHONUTF8=1 when unset", func(t *testing.T) { + env := PythonUTF8Env([]string{"PORT=8001", "PATH=/usr/bin"}) + if env[len(env)-1] != "PYTHONUTF8=1" { + t.Fatalf("expected PYTHONUTF8=1 appended, got %v", env) + } + }) + + t.Run("respects an explicit caller value", func(t *testing.T) { + in := []string{"PYTHONUTF8=0", "PORT=8001"} + env := PythonUTF8Env(in) + if len(env) != len(in) { + t.Fatalf("caller's PYTHONUTF8 must win, got %v", env) + } + }) +} + func TestAgentNodeRunnerWaitDisplayAndStartProcess(t *testing.T) { // Cannot use t.Parallel() — this test modifies PATH via t.Setenv, // which is process-global and unsafe to share with parallel tests. @@ -109,7 +126,7 @@ func TestAgentNodeRunnerWaitDisplayAndStartProcess(t *testing.T) { }) waitForHTTPServer(t, fmt.Sprintf("127.0.0.1:%d", port)) - if err := runner.waitForAgentNode(port, "/health", 2*time.Second); err != nil { + if err := runner.waitForAgentNode(port, "/health", "", 2*time.Second); err != nil { t.Fatalf("waitForAgentNode: %v", err) } if err := runner.displayCapabilities(InstalledPackage{Name: "demo"}, port); err != nil { @@ -122,7 +139,7 @@ func TestAgentNodeRunnerWaitDisplayAndStartProcess(t *testing.T) { } unusedPort := unusedPortListener.Addr().(*net.TCPAddr).Port _ = unusedPortListener.Close() - if err := runner.waitForAgentNode(unusedPort, "/health", 600*time.Millisecond); err == nil || !strings.Contains(err.Error(), "did not become ready") { + if err := runner.waitForAgentNode(unusedPort, "/health", "", 600*time.Millisecond); err == nil || !strings.Contains(err.Error(), "did not become ready") { t.Fatalf("expected timeout error, got %v", err) } diff --git a/control-plane/internal/skillkit/catalog.go b/control-plane/internal/skillkit/catalog.go index 311ac1a7c..8ce3eb336 100644 --- a/control-plane/internal/skillkit/catalog.go +++ b/control-plane/internal/skillkit/catalog.go @@ -17,6 +17,10 @@ type Skill struct { Description string // one-line description for `af skill list` EmbedRoot string // root path inside SkillData where this skill's files live EntryFile string // relative path to the skill's main file (usually SKILL.md) + // Trigger is the "when must the agent read this skill" sentence rendered + // into marker-block targets' rules files (see renderPointerBlock) — each + // skill fires on different requests, so each carries its own trigger. + Trigger string } // Catalog is the registry of every skill the binary ships. Add a new entry @@ -30,6 +34,20 @@ var Catalog = []Skill{ Description: "Design and ship a multi-agent system on AgentField. Derive the orchestration from the problem: decompose by cognitive jobs, place each slot on the autonomy spectrum, assign a verification rung, choose the dynamism rung with budgets. Composite intelligence, deep dynamic call graphs, live SDK docs from agentfield.ai, async-first smoke tests.", EmbedRoot: "skill_data/agentfield", EntryFile: "SKILL.md", + Trigger: `When the user asks you to architect or build a multi-agent system on +AgentField (composite-intelligence backends, multi-reasoner pipelines, +financial reviewer / clinical triage / research agent / etc.), you MUST +read this skill first`, + }, + { + Name: "agentfield-use", + Version: "0.1.0", + Description: "Discover and call agents already running on a local AgentField control plane. Health check, capability discovery, sync/async execution and polling, sessions, failure triage, and the af CLI ops (run/stop/logs/secrets) that keep installed agents answering.", + EmbedRoot: "skill_data/agentfield-use", + EntryFile: "SKILL.md", + Trigger: `When the user asks you to use, call, query, or delegate work to an +installed AgentField agent, to list available agents or reasoners, or to +check on a running execution, you MUST read this skill first`, }, } diff --git a/control-plane/internal/skillkit/catalog_agentfield_use_test.go b/control-plane/internal/skillkit/catalog_agentfield_use_test.go new file mode 100644 index 000000000..ce00a4994 --- /dev/null +++ b/control-plane/internal/skillkit/catalog_agentfield_use_test.go @@ -0,0 +1,67 @@ +package skillkit + +import ( + "strings" + "testing" +) + +// Contract: the agentfield-use skill is registered and its content is really +// embedded — a missing go:embed directive would only surface at install time +// as "embedded skill is empty" without this guard. +func TestAgentfieldUseSkillEmbedded(t *testing.T) { + skill, err := CatalogByName("agentfield-use") + if err != nil { + t.Fatalf("CatalogByName: %v", err) + } + if skill.Version == "" || skill.Trigger == "" { + t.Fatalf("agentfield-use catalog entry incomplete: %+v", skill) + } + + files, err := skill.EnumerateFiles() + if err != nil { + t.Fatalf("EnumerateFiles: %v", err) + } + if _, ok := files[skill.EntryFile]; !ok { + t.Fatalf("entry file %q not embedded (got %d files)", skill.EntryFile, len(files)) + } + + content, err := skill.EntryContent() + if err != nil { + t.Fatalf("EntryContent: %v", err) + } + // The consumer skill must teach the durable discovery + execute surface. + for _, needle := range []string{ + "/api/v1/discovery/capabilities", + "/api/v1/execute/async/", + "/api/v1/executions/", + } { + if !strings.Contains(string(content), needle) { + t.Fatalf("agentfield-use SKILL.md is missing %q", needle) + } + } +} + +// Contract: each skill's marker block carries its own trigger sentence, so a +// rules file holding both skills routes build requests and use requests to +// the right SKILL.md. +func TestPointerBlocksAreSkillSpecific(t *testing.T) { + build, err := CatalogByName("agentfield") + if err != nil { + t.Fatalf("CatalogByName: %v", err) + } + use, err := CatalogByName("agentfield-use") + if err != nil { + t.Fatalf("CatalogByName: %v", err) + } + buildBlock := renderPointerBlock(build, "/canonical/agentfield/current") + useBlock := renderPointerBlock(use, "/canonical/agentfield-use/current") + if !strings.Contains(buildBlock, "architect or build") { + t.Fatalf("build skill block lost its trigger: %q", buildBlock) + } + if !strings.Contains(useBlock, "delegate work") { + t.Fatalf("use skill block lost its trigger: %q", useBlock) + } + if strings.Contains(useBlock, "architect or build") { + t.Fatal("use skill block reuses the build trigger") + } +} diff --git a/control-plane/internal/skillkit/embed.go b/control-plane/internal/skillkit/embed.go index 556932438..feb2d62df 100644 --- a/control-plane/internal/skillkit/embed.go +++ b/control-plane/internal/skillkit/embed.go @@ -34,4 +34,5 @@ import "embed" //go:embed skill_data/agentfield/SKILL.md //go:embed skill_data/agentfield/references/*.md //go:embed skill_data/agentfield/commands/*.md +//go:embed skill_data/agentfield-use/SKILL.md var SkillData embed.FS diff --git a/control-plane/internal/skillkit/skill_data/agentfield-use/SKILL.md b/control-plane/internal/skillkit/skill_data/agentfield-use/SKILL.md new file mode 100644 index 000000000..5defce2a6 --- /dev/null +++ b/control-plane/internal/skillkit/skill_data/agentfield-use/SKILL.md @@ -0,0 +1,143 @@ +--- +name: agentfield-use +description: Discover and call agents already running on a local AgentField control plane. Use when the user asks to use, call, query, run, or delegate work to an installed AgentField agent (swe-planner, pr-af, sec-af, …), to list what agents or reasoners are available, or to check on an execution. Not for building new agents — that is the agentfield skill. +--- + +# Using AgentField agents + +A machine with AgentField has a **control plane** (default `http://localhost:8080`, +override via `AGENTFIELD_SERVER`) and **agent nodes** installed under +`~/.agentfield`. Each node exposes **reasoners** — typed functions you call over +HTTP. You never talk to an agent's own port: every call goes through the control +plane, which routes it, records the workflow, and returns the result. + +In local mode there is no auth. If the server has an API key configured, send it +as `X-API-Key: ` on every request. + +## The flow + +1. Health-check the control plane. +2. Discover what agents and reasoners exist. +3. Execute — async for anything nontrivial. +4. Poll (or stream) until the execution finishes. + +## 1. Is the control plane up? + +```bash +curl -s http://localhost:8080/health +``` + +Healthy: `200` with `{"status":"healthy", ...}`. Connection refused means no +control plane is running — the user can open the AgentField desktop app, or you +can start one in the background (`af server` blocks, so background it and poll +`/health` until healthy). + +## 2. Discover agents and reasoners + +```bash +curl -s "http://localhost:8080/api/v1/discovery/capabilities?include_input_schema=true" \ + | jq '.capabilities[] | {agent: .agent_id, health: .health_status, reasoners: [.reasoners[].id]}' +``` + +This is the durable discovery endpoint. Reasoner names are `.reasoners[].id` +(NOT `.name`), and `include_input_schema=true` adds each reasoner's JSON input +schema — read it before calling so your `input` matches. + +Two gotchas: + +- The response's `invocation_target` field uses a **colon** (`agent:reasoner`). + The execute URL uses a **dot**. Build the target yourself: `.`. +- Discovery only lists agents that are **running and registered**. Installed but + stopped agents live in the local registry — check with `af ls`, start with + `af run ` (it detaches; the agent keeps running after the CLI exits). + +## 3. Call a reasoner + +Input kwargs are ALWAYS nested under `"input"` — never raw at the top level. + +**Async — the default for real work.** Returns `202` immediately: + +```bash +curl -s -X POST http://localhost:8080/api/v1/execute/async/swe-planner.plan \ + -H 'Content-Type: application/json' \ + -d '{"input": {"task": "add rate limiting to the API"}}' +# -> {"execution_id":"...", "run_id":"...", "status":"queued", ...} +``` + +**Sync — only for calls that finish fast** (hard 90s timeout, response carries +`result` directly): + +```bash +curl -s -X POST http://localhost:8080/api/v1/execute/swe-planner.plan \ + -H 'Content-Type: application/json' \ + -d '{"input": {"task": "..."}}' +``` + +## 4. Get the result + +Poll the execution until `status` is terminal (`succeeded` / `failed`, also +`cancelled` / `timeout`): + +```bash +curl -s http://localhost:8080/api/v1/executions/ \ + | jq '{status, result, error}' +``` + +Long-running agents can take minutes — poll with backoff (2s → 5s → 10s) and +tell the user what is in flight. For live progress, stream Server-Sent Events +from `GET /api/v1/executions//events`. To check several at once: +`POST /api/v1/executions/batch-status` with `{"execution_ids": [...]}`. + +There is **no** `GET /api/v1/executions` list endpoint — do not invent one. +Cancel with `POST /api/v1/executions//cancel`. + +## Sessions and multi-call work + +- `X-Session-ID: ` on execute requests groups multi-turn work; the + control plane forwards it to the agent and scopes session memory by it. +- Reuse `X-Run-ID` across several execute calls to group them into one + workflow; each response also returns its `run_id`. + +Agents share state through control-plane memory if you need to pass artifacts +around: `POST /api/v1/memory/set` with `{"key": ..., "data": , "scope": +"global"}` and `POST /api/v1/memory/get` with `{"key": ...}` (non-global scopes +resolve from the `X-Workflow-ID` / `X-Session-ID` / `X-Actor-ID` headers). + +## When things fail + +| Symptom | Meaning | Fix | +|---|---|---| +| connection refused on :8080 | control plane not running | desktop app, or background `af server` and poll `/health` | +| agent missing from discovery | node installed but not running (or not installed) | `af ls`, then `af run ` — or `af install ` | +| `missing required environment variables: X` from `af run` | required key not configured | `af secrets set X` (value via stdin/arg; `--node ` for node-scoped) — or desktop app → Agents → Keys | +| HTTP 502 with `error_message` | the agent itself errored | read `af logs `, fix, retry | +| execution stuck in `queued`/`running` | agent wedged or overloaded | `af stop && af run `, then re-submit | + +## Local ops cheat sheet (af CLI) + +```bash +af ls # installed agents + status +af run # start (detached); af stop +af logs # agent logs +af secrets set KEY # store an API key (encrypted; prompts for value) +af secrets ls # what's configured (values never shown) +af install # install a new agent node +``` + +## Audit trail + +Every execution is recorded. When provenance matters (or the user asks "what +did the agents actually do"), fetch the verifiable-credential chain for a +workflow: `GET /api/v1/did/workflow//vc-chain` (available when DID/VC +is enabled), and verify offline with `af verify audit.json`. + +## Hard rules + +- Every call goes through the control plane — never POST to an agent's own port. +- Kwargs live under `"input"`. Empty input is `{"input": {}}`. +- Async + poll for anything that might exceed a few seconds; sync is for quick + lookups only. +- Don't guess endpoints. The surface above is the contract; if something is + missing, say so instead of inventing a route. +- Building or modifying an agent (new reasoners, scaffolds, deploys) is the + **agentfield** skill's job — switch to it for that. diff --git a/control-plane/internal/skillkit/targets.go b/control-plane/internal/skillkit/targets.go index df6e73bc3..50e7f2942 100644 --- a/control-plane/internal/skillkit/targets.go +++ b/control-plane/internal/skillkit/targets.go @@ -121,25 +121,25 @@ func markerEnd(skill Skill) string { // flow through automatically — no need to re-edit every agent rules file. func renderPointerBlock(skill Skill, canonicalCurrentDir string) string { skillPath := filepath.Join(canonicalCurrentDir, skill.EntryFile) + trigger := skill.Trigger + if trigger == "" { + trigger = "When working with AgentField, you MUST read this skill first" + } return fmt.Sprintf(`%s ## %s -When the user asks you to architect or build a multi-agent system on -AgentField (composite-intelligence backends, multi-reasoner pipelines, -financial reviewer / clinical triage / research agent / etc.), you MUST -read this skill first: +%s: %s The skill is self-contained and every reference file is one level deep -from SKILL.md. It teaches the philosophy, the SDK primitives, the -canonical scaffold layout, the verification workflow, and the curl -smoke test. +from SKILL.md. Skill version: %s %s`, markerStart(skill), skill.Description, + trigger, skillPath, skill.Version, markerEnd(skill), diff --git a/desktop/.gitignore b/desktop/.gitignore new file mode 100644 index 000000000..81c123800 --- /dev/null +++ b/desktop/.gitignore @@ -0,0 +1,17 @@ +node_modules/ +dist/ +out/ +release/ +*.log + +# bundled af CLI staging area (built by scripts/bundle-cli.mjs or dropped in +# by the release pipeline); only the .gitkeep is tracked +vendor/* +!vendor/.gitkeep + +# The root .gitignore excludes build/, but electron-builder reads the app +# icons from desktop/build — track exactly those. +!build/ +build/* +!build/icon.png +!build/icon.icns diff --git a/desktop/README.md b/desktop/README.md new file mode 100644 index 000000000..bae7a55cb --- /dev/null +++ b/desktop/README.md @@ -0,0 +1,188 @@ +# AgentField Desktop + +A desktop companion for AgentField, in the spirit of Docker Desktop: one +window that shows the health of the local control plane, the agent nodes on +this machine, live execution activity — and installs curated agents with one +click. Designed Mac-first: no menu bar clutter, seamless titlebar, sidebar +navigation, light/dark from the OS. + +## Views + +- **Dashboard** — headline tiles (agents running, executing now, runs today, + success rate, from `GET /api/ui/v1/dashboard/summary`) plus recent activity. +- **Agents** — nodes from `~/.agentfield/installed.yaml` with a status badge + per agent, cross-checked against the control plane's `GET /api/v1/nodes`: + - `running` — registry says running and the control plane sees the node + - `stopped` — registry says stopped and the control plane does not see it + - `unknown` — registry and control plane disagree (stale registry / conflict) +- **Activity** — in-flight workflow runs (live pulse) and a short tail of + finished ones, from `GET /api/ui/v2/workflow-runs`. +- **Install** — a curated, hard-coded catalog (`src/shared/catalog.ts`). + Installing shells out to `af install ` and streams progress lines + into the row; the af CLI stays the single contract for installs. Entries + are keyed by the node's manifest name so installed state is detected. The + hard-coded list is the pre-marketplace seam — replace with a remote catalog + fetch when registry search lands. +- **Settings** — the "set it and forget it" surface: open at login (hidden, + tray-only, via an OS login item), start the control plane automatically, + and pick which agents auto-start. Persisted to `settings.json` in the + app's user-data dir. + +The renderer polls a single snapshot over IPC every 5 seconds. + +Agents can be started, stopped, and restarted from their rows — each action +shells out to `af run ` / `af stop ` (restart is stop-then-run; +the CLI has no restart verb). On every launch the app runs the autostart +sequence (`src/main/autostart.ts`): start the control plane when nothing is +listening (never when the port is taken — by a foreign service or a live +control plane), then bring up the selected agents; agents whose registry +entry went stale (running with no control-plane presence, e.g. after a +reboot) are restarted rather than skipped. The goal: by the time Claude, +Codex, or anything else queries your agents, they are already answering — +no one has to remember to start a server first. + +The control-plane probe only trusts `/health` responses that look like +AgentField's payload — an unrelated service on port 8080 renders as +"Port in use", never as a running control plane. + +## Agent keys (secrets) + +Agents declare the environment they need (API keys, tokens) in their +`agentfield-package.yaml` under `user_environment` — required variables, +`require_one_of` groups (e.g. an Anthropic **or** an OpenRouter key), and +optionals with defaults. `af run` resolves them process env → encrypted +secret store → manifest default, and fails headlessly when a required one +is missing. The app closes that gap in the UI: + +- Any agent that declares variables gets a **Keys** button and, while + something required is unresolved, a **Needs keys** chip. Clicking Start + in that state opens the editor instead of running into a guaranteed + "missing required environment variables" failure. +- The editor shows every declared variable with its resolution status + (from environment / stored / default / missing), a set field (password + input for `type: secret`), and a **Revoke** button for stored values. +- Reads and writes go through the af CLI — `af secrets ls` / `set` / `rm` + against the encrypted store (`~/.agentfield/secrets`, AES-256-GCM), the + exact store `af run` decrypts into the agent's process. Values are piped + over stdin (never argv), written to the scope the manifest names (global + by default, so a shared key is entered once), validated against the + manifest's regex when present, and **never read back** — the renderer + only ever sees status flags. + +All of this lives in `src/main/secrets.ts` (pure parsing/report logic, +unit-tested) with the catalog-style guard that the renderer can only name +variables the manifest declares. + +## Tray icon (Windows/Linux) and deep links + +On Windows and Linux the app puts a status icon in the tray: the brand dot +turns gold while the control plane is running and gray otherwise, and the +menu offers Open AgentField / Open web UI / Quit. Closing the window hides it +to the tray (Docker-Desktop style) — Quit lives in the tray menu. On macOS the +desktop app adds **no** tray: the menu-bar companion there is `af-tray`, +installed with AgentField itself (`control-plane/cmd/af-tray`). + +The app registers the `agentfield://` URL scheme (single-instance: a second +launch focuses the running app). `agentfield://dashboard|agents|activity|install` +opens the app on that view; a bare or unknown target lands on the dashboard. +This is how the macOS `af-tray` opens the desktop app when it is installed — +and why it can *detect* it: `open agentfield://…` fails fast when nothing has +registered the scheme, and the tray then falls back to the web UI. + +## Icons + +All icons render from the brand "•af" mark (the exact outlined paths from the +web UI logo). `npm run icons` regenerates `build/icon.{png,icns}`, the runtime +window icon, the tray glyphs, and af-tray's `appicon.icns` — outputs are +committed, so this only needs re-running when the mark changes. + +## Prerequisites + +- Node.js 20+ (developed on Node 22) +- An AgentField control plane on `http://localhost:8080` (optional — the app + degrades gracefully when it is not running, and can start one itself) +- Nothing else: the packaged app bundles the `af` CLI (see below). In dev, + either have `af` on PATH or run `npm run bundle-cli` once. + +## Bundled CLI, resolution, and updates + +The packaged app carries the af CLI (`resources/bin`, staged from `vendor/` +by `npm run bundle-cli` or the release pipeline) so a desktop-app-only +install works on a machine that never saw AgentField. On every launch the +app resolves which af to drive (`src/main/cli.ts`), in order: + +1. **managed** — `~/.agentfield/bin` (where the curl installer puts it, and + where the app provisions its bundled copy: the shared location both + installers converge on, so there is never a double install) +2. **PATH** — a developer's own `af` +3. **bundled** — the copy inside the app package + +A copy older than the app's minimum version is skipped — the app runs on its +bundled CLI meanwhile and Settings shows an "Update AgentField" button that +installs the bundled copy into the managed location (never over a newer +one; `Version: dev` builds are trusted as-is). On a machine with no CLI at +all, first launch auto-provisions `~/.agentfield/bin` (both `agentfield` +and the `af` alias, Windows user PATH registered) so terminals and coding +agents get a working `af` too. + +Unless switched off in Settings, the app also installs both bundled skills +on launch (`af skill install --non-interactive`, sequentially): +**agentfield** (how to build agents) and **agentfield-use** (how to discover +and call the agents installed here) — so detected coding agents (Claude +Code, Codex, Gemini, …) can drive this machine's agents without extra +setup. Idempotent via skillkit's state file, shared with the curl installer. + +## Development + +```bash +cd desktop +npm install +npm run dev # electron-vite dev server + Electron window (needs a display) +``` + +## Build, typecheck, test, package + +```bash +npm run typecheck # tsc --noEmit over main, preload, shared, and renderer +npm run build # typecheck + electron-vite production build into out/ +npm test # vitest unit tests for the data/install modules (headless) +npm run dist # package installers into release/ (DMG+zip on macOS, NSIS on Windows) +npm run dist:dir # unpacked app for a quick smoke test +``` + +Packaging is unsigned for now (no notarization/signing identities configured). + +## Architecture + +- **All Node-side data access lives in `src/main/agentfield.ts`** (registry + parsing, control-plane HTTP probes, badge derivation, executions/metrics + fetch, snapshot composition) and **installs in `src/main/installer.ts`** + (spawns `af install`, sanitizes spinner/ANSI output, only accepts catalog + names — never raw sources — from the renderer). Neither imports Electron, + so both are unit-tested directly with Vitest. +- **Secure Electron layout:** the renderer runs with `contextIsolation: true`, + `nodeIntegration: false`, `sandbox: true`. The preload + (`src/preload/index.ts`) exposes a small typed API via `contextBridge`. +- **Shared IPC types** live in `src/shared/types.ts`; the install catalog in + `src/shared/catalog.ts`; deep-link parsing in `src/shared/deeplink.ts`. +- **Tray presentation logic** (state/labels/glyph selection) is pure in + `src/main/tray-model.ts` (unit-tested); the Electron glue is `src/main/tray.ts`. +- **Mac-first chrome** in `src/main/index.ts`: `titleBarStyle: hiddenInset` + + sidebar vibrancy on macOS (minimal app menu since macOS needs one for + Cmd+Q/copy-paste), hidden titlebar with native control overlay on Windows, + no menu bar anywhere else. The sidebar and view header are draggable + regions. + +## Current limitations + +- Control plane URL is hard-coded to `http://localhost:8080` (not configurable yet). +- No stop control for the control plane itself yet (the app only starts it). +- macOS/Linux shell PATH setup stays with the curl installer — the app only + provisions `~/.agentfield/bin` there (absolute path always works). +- The registry is read directly from `~/.agentfield/installed.yaml`; once + `af list -o json` lands, the app should shell out to the CLI instead (see + the `TODO(af-cli)` seam in `src/main/agentfield.ts`). +- macOS chrome (traffic-light inset, vibrancy) is implemented per platform + guards but has only been exercised on Windows so far — needs one smoke run + on a real Mac. Same for macOS deep-link delivery (`open-url`). +- Packaging is unsigned; add signing/notarization before distribution. diff --git a/desktop/build/icon.icns b/desktop/build/icon.icns new file mode 100644 index 000000000..10aebb778 Binary files /dev/null and b/desktop/build/icon.icns differ diff --git a/desktop/build/icon.png b/desktop/build/icon.png new file mode 100644 index 000000000..eacea1336 Binary files /dev/null and b/desktop/build/icon.png differ diff --git a/desktop/electron.vite.config.ts b/desktop/electron.vite.config.ts new file mode 100644 index 000000000..be5eddd5c --- /dev/null +++ b/desktop/electron.vite.config.ts @@ -0,0 +1,14 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'electron-vite' + +// electron-vite defaults (kept deliberately): +// main: src/main/index.ts -> out/main/index.js (CJS, package has no "type": "module") +// preload: src/preload/index.ts -> out/preload/index.js (CJS — required for sandbox: true) +// renderer: src/renderer/index.html + src/renderer/src -> out/renderer +export default defineConfig({ + main: {}, + preload: {}, + renderer: { + plugins: [react()] + } +}) diff --git a/desktop/package-lock.json b/desktop/package-lock.json new file mode 100644 index 000000000..35598f717 --- /dev/null +++ b/desktop/package-lock.json @@ -0,0 +1,7252 @@ +{ + "name": "agentfield-desktop", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agentfield-desktop", + "version": "0.1.0", + "dependencies": { + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^22.10.1", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "electron": "^33.2.1", + "electron-builder": "^25.1.8", + "electron-vite": "^3.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "typescript": "^5.7.2", + "vite": "^6.0.5", + "vitest": "^3.0.5" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.1.tgz", + "integrity": "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==", + "dev": true, + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.1.tgz", + "integrity": "sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==", + "dev": true, + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "node-abi": "^3.45.0", + "node-api-version": "^0.2.0", + "node-gyp": "^9.0.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/rebuild/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/rebuild/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/rebuild/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", + "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==", + "dev": true, + "dependencies": { + "@electron/asar": "^3.2.7", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", + "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "dev": true, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.10", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz", + "integrity": "sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==", + "dev": true + }, + "node_modules/app-builder-lib": { + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.1.8.tgz", + "integrity": "sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==", + "dev": true, + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.1", + "@electron/rebuild": "3.6.1", + "@electron/universal": "2.0.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "chromium-pickle-js": "^0.2.0", + "config-file-ts": "0.2.8-rc1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "25.1.7", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.0", + "resedit": "^1.7.0", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "25.1.8", + "electron-builder-squirrel-windows": "25.1.8" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "dev": true + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "peer": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "peer": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "peer": true + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/bluebird-lst": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", + "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", + "dev": true, + "dependencies": { + "bluebird": "^3.5.5" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/builder-util": { + "version": "25.1.7", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-25.1.7.tgz", + "integrity": "sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==", + "dev": true, + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.10", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.2.10", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.2.10", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", + "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001803", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "peer": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/config-file-ts": { + "version": "0.2.8-rc1", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz", + "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==", + "dev": true, + "dependencies": { + "glob": "^10.3.12", + "typescript": "^5.4.3" + } + }, + "node_modules/config-file-ts/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/config-file-ts/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "peer": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "peer": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.8.tgz", + "integrity": "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==", + "dev": true, + "dependencies": { + "app-builder-lib": "25.1.8", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "33.4.11", + "resolved": "https://registry.npmjs.org/electron/-/electron-33.4.11.tgz", + "integrity": "sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.1.8.tgz", + "integrity": "sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==", + "dev": true, + "dependencies": { + "app-builder-lib": "25.1.8", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "chalk": "^4.1.2", + "dmg-builder": "25.1.8", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.8.tgz", + "integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==", + "dev": true, + "peer": true, + "dependencies": { + "app-builder-lib": "25.1.8", + "archiver": "^5.3.1", + "builder-util": "25.1.7", + "fs-extra": "^10.1.0" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "25.1.7", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz", + "integrity": "sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==", + "dev": true, + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "chalk": "^4.1.2", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.389", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "dev": true, + "license": "ISC" + }, + "node_modules/electron-vite": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-3.1.0.tgz", + "integrity": "sha512-M7aAzaRvSl5VO+6KN4neJCYLHLpF/iWo5ztchI/+wMxIieDZQqpbCYfaEHHHPH6eupEzfvZdLYdPdmvGqoVe0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.10", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "cac": "^6.7.14", + "esbuild": "^0.25.1", + "magic-string": "^0.30.17", + "picocolors": "^1.1.1" + }, + "bin": { + "electron-vite": "bin/electron-vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@swc/core": "^1.0.0", + "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "peer": true + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "peer": true + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "peer": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "peer": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "peer": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true, + "peer": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "peer": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "peer": true + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true, + "peer": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-api-version/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", + "dev": true, + "dependencies": { + "@xmldom/xmldom": "^0.9.10", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "peer": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "peer": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "peer": true + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "peer": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "peer": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "peer": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 000000000..bbd9cab9e --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,82 @@ +{ + "name": "agentfield-desktop", + "version": "0.1.0", + "private": true, + "description": "Read-only desktop dashboard for the AgentField control plane and locally installed agent nodes", + "main": "./out/main/index.js", + "scripts": { + "dev": "electron-vite dev", + "typecheck": "tsc --noEmit", + "build": "npm run typecheck && electron-vite build", + "preview": "electron-vite preview", + "test": "vitest run", + "icons": "electron scripts/make-icons.mjs", + "bundle-cli": "node scripts/bundle-cli.mjs", + "dist": "npm run build && electron-builder", + "dist:dir": "npm run build && electron-builder --dir" + }, + "build": { + "appId": "ai.agentfield.desktop", + "productName": "AgentField", + "protocols": [ + { + "name": "AgentField", + "schemes": [ + "agentfield" + ] + } + ], + "files": [ + "out/**", + "resources/**" + ], + "extraResources": [ + { + "from": "vendor", + "to": "bin", + "filter": [ + "af", + "af.exe" + ] + } + ], + "directories": { + "output": "release" + }, + "mac": { + "category": "public.app-category.developer-tools", + "target": [ + "dmg", + "zip" + ], + "darkModeSupport": true + }, + "win": { + "target": [ + "nsis" + ] + }, + "nsis": { + "oneClick": true, + "deleteAppDataOnUninstall": true + } + }, + "dependencies": { + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^22.10.1", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "electron": "^33.2.1", + "electron-builder": "^25.1.8", + "electron-vite": "^3.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "typescript": "^5.7.2", + "vite": "^6.0.5", + "vitest": "^3.0.5" + } +} diff --git a/desktop/resources/icon.png b/desktop/resources/icon.png new file mode 100644 index 000000000..152e1352c Binary files /dev/null and b/desktop/resources/icon.png differ diff --git a/desktop/resources/tray/tray-active-dark-16.png b/desktop/resources/tray/tray-active-dark-16.png new file mode 100644 index 000000000..57a1d024d Binary files /dev/null and b/desktop/resources/tray/tray-active-dark-16.png differ diff --git a/desktop/resources/tray/tray-active-dark-24.png b/desktop/resources/tray/tray-active-dark-24.png new file mode 100644 index 000000000..05239b957 Binary files /dev/null and b/desktop/resources/tray/tray-active-dark-24.png differ diff --git a/desktop/resources/tray/tray-active-dark-32.png b/desktop/resources/tray/tray-active-dark-32.png new file mode 100644 index 000000000..adb216854 Binary files /dev/null and b/desktop/resources/tray/tray-active-dark-32.png differ diff --git a/desktop/resources/tray/tray-active-light-16.png b/desktop/resources/tray/tray-active-light-16.png new file mode 100644 index 000000000..64003e3e7 Binary files /dev/null and b/desktop/resources/tray/tray-active-light-16.png differ diff --git a/desktop/resources/tray/tray-active-light-24.png b/desktop/resources/tray/tray-active-light-24.png new file mode 100644 index 000000000..3ecb4280b Binary files /dev/null and b/desktop/resources/tray/tray-active-light-24.png differ diff --git a/desktop/resources/tray/tray-active-light-32.png b/desktop/resources/tray/tray-active-light-32.png new file mode 100644 index 000000000..f4cd9a840 Binary files /dev/null and b/desktop/resources/tray/tray-active-light-32.png differ diff --git a/desktop/resources/tray/tray-inactive-dark-16.png b/desktop/resources/tray/tray-inactive-dark-16.png new file mode 100644 index 000000000..b4e073185 Binary files /dev/null and b/desktop/resources/tray/tray-inactive-dark-16.png differ diff --git a/desktop/resources/tray/tray-inactive-dark-24.png b/desktop/resources/tray/tray-inactive-dark-24.png new file mode 100644 index 000000000..6b1315329 Binary files /dev/null and b/desktop/resources/tray/tray-inactive-dark-24.png differ diff --git a/desktop/resources/tray/tray-inactive-dark-32.png b/desktop/resources/tray/tray-inactive-dark-32.png new file mode 100644 index 000000000..b9af4800c Binary files /dev/null and b/desktop/resources/tray/tray-inactive-dark-32.png differ diff --git a/desktop/resources/tray/tray-inactive-light-16.png b/desktop/resources/tray/tray-inactive-light-16.png new file mode 100644 index 000000000..550f30de5 Binary files /dev/null and b/desktop/resources/tray/tray-inactive-light-16.png differ diff --git a/desktop/resources/tray/tray-inactive-light-24.png b/desktop/resources/tray/tray-inactive-light-24.png new file mode 100644 index 000000000..fb59e94c6 Binary files /dev/null and b/desktop/resources/tray/tray-inactive-light-24.png differ diff --git a/desktop/resources/tray/tray-inactive-light-32.png b/desktop/resources/tray/tray-inactive-light-32.png new file mode 100644 index 000000000..b3286732c Binary files /dev/null and b/desktop/resources/tray/tray-inactive-light-32.png differ diff --git a/desktop/scripts/bundle-cli.mjs b/desktop/scripts/bundle-cli.mjs new file mode 100644 index 000000000..2ff5e21ef --- /dev/null +++ b/desktop/scripts/bundle-cli.mjs @@ -0,0 +1,35 @@ +// Build the af CLI from the sibling control-plane source and drop it into +// desktop/vendor/, where electron-builder's extraResources picks it up +// (resources/bin/ inside the packaged app). Run before `npm run dist`: +// +// npm run bundle-cli # plain build (no embedded web UI) +// npm run bundle-cli -- full # embedded web UI + sqlite FTS (needs CGO + +// # a prior `npm run build` in web/client) +// +// Release pipelines can skip this script and copy the goreleaser artifact +// for the target platform into vendor/ instead — anything named af/af.exe +// in vendor/ gets bundled. + +import { spawnSync } from 'node:child_process' +import { mkdirSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const desktopDir = dirname(dirname(fileURLToPath(import.meta.url))) +const controlPlaneDir = join(desktopDir, '..', 'control-plane') +const vendorDir = join(desktopDir, 'vendor') +const output = join(vendorDir, process.platform === 'win32' ? 'af.exe' : 'af') + +const full = process.argv.includes('full') +const args = ['build'] +if (full) args.push('-tags', 'embedded sqlite_fts5') +args.push('-o', output, './cmd/af') + +mkdirSync(vendorDir, { recursive: true }) +console.log(`go ${args.join(' ')} (in ${controlPlaneDir})`) +const result = spawnSync('go', args, { cwd: controlPlaneDir, stdio: 'inherit' }) +if (result.error) { + console.error('failed to run go — is Go installed and on PATH?', result.error.message) + process.exit(1) +} +process.exit(result.status ?? 1) diff --git a/desktop/scripts/make-icons.mjs b/desktop/scripts/make-icons.mjs new file mode 100644 index 000000000..d4f4ed7a0 --- /dev/null +++ b/desktop/scripts/make-icons.mjs @@ -0,0 +1,200 @@ +// Icon generator for AgentField Desktop (and the af-tray macOS bundle icon). +// +// Renders the brand "•af" mark — the exact outlined paths from +// control-plane/web/client/src/assets/logos/logo-short-*.svg, so there is no +// font dependency — into every raster the apps need, using an offscreen +// Electron window as the SVG rasterizer (no native image deps required). +// +// Run from desktop/: npx electron scripts/make-icons.mjs +// (unset ELECTRON_RUN_AS_NODE first if your shell inherits it) +// +// Outputs (all committed; regenerate only when the brand mark changes): +// build/icon.icns macOS app icon (Apple-grid margins) +// build/icon.png Windows/Linux app icon source (1024) +// resources/icon.png runtime window icon (win/linux, 256) +// resources/tray/tray---.png Windows tray glyphs +// s: active|inactive g: light|dark +// ../control-plane/cmd/af-tray/assets/appicon.icns same icns for af-tray + +import { BrowserWindow, app } from 'electron' +import { mkdirSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const desktopDir = dirname(dirname(fileURLToPath(import.meta.url))) + +// ---- Brand ----------------------------------------------------------------- + +const INK_TOP = '#221d15' // warm near-black gradient, top… +const INK_BOTTOM = '#0b0a08' // …to bottom +const CREAM = '#f5f0eb' +const GOLD = '#d4a24a' +const GRAY_LIGHT = '#9c9c9c' // inactive glyph on dark taskbars +const GRAY_DARK = '#6f6f6f' // inactive glyph on light taskbars + +// The "af" letterforms and the dot, verbatim from the logo SVG (1000×1000 +// viewBox). Mark bounding box: x 180…865.168, y 329…697. +const AF_PATH = + 'M656.324 689H585.824V636.5L581.324 632.5V536C581.324 520.333 576.824 508.333 567.824 500C558.824 491.667 545.824 487.5 528.824 487.5C506.491 487.5 485.158 495.833 464.824 512.5L424.324 464C456.324 438.667 493.824 426 536.824 426C561.158 426 582.158 430.5 599.824 439.5C617.824 448.167 631.658 460.667 641.324 477C651.324 493 656.324 512.167 656.324 534.5V689ZM510.324 697C491.324 697 474.824 693.5 460.824 686.5C446.824 679.5 435.824 669.667 427.824 657C420.158 644.333 416.324 629.5 416.324 612.5C416.324 586.167 426.991 565.833 448.324 551.5C469.658 536.833 499.491 529.5 537.824 529.5H586.324V578H542.324C528.324 578 517.158 580.833 508.824 586.5C500.824 592.167 496.824 599.833 496.824 609.5C496.824 617.833 499.991 624.833 506.324 630.5C512.991 635.833 521.491 638.5 531.824 638.5C541.491 638.5 550.324 636.167 558.324 631.5C566.324 626.833 572.824 620.5 577.824 612.5C583.158 604.167 586.158 594.833 586.824 584.5L605.324 593C605.324 614 601.324 632.333 593.324 648C585.658 663.333 574.658 675.333 560.324 684C546.324 692.667 529.658 697 510.324 697ZM811.168 689H729.668V406C729.668 354.667 754.168 329 803.168 329H865.168V392.5H834.168C825.835 392.5 819.835 394.5 816.168 398.5C812.835 402.167 811.168 408.833 811.168 418.5V689ZM865.168 499.5H693.168V434H865.168V499.5Z' + +const MARK = { minX: 180, maxX: 865.168, minY: 329, maxY: 697 } +const MARK_W = MARK.maxX - MARK.minX +const MARK_CX = (MARK.minX + MARK.maxX) / 2 +const MARK_CY = (MARK.minY + MARK.maxY) / 2 + +function mark(dotColor, textColor) { + return ( + `` + + `` + ) +} + +/** Center the mark at (cx, cy) scaled to targetW wide, in the icon's coords. */ +function placeMark(inner, cx, cy, targetW) { + const s = targetW / MARK_W + const tx = cx - MARK_CX * s + const ty = cy - MARK_CY * s + return `${inner}` +} + +const BG_DEFS = + `` + + `` + + `` + + `` + +// macOS app icon: Apple's grid — 824×824 squircle centered on a 1024 canvas +// with a baked-in soft shadow, mark at ~65% of the tile width. +function macIconSVG(size) { + return ( + `` + + `${BG_DEFS}` + + `` + + `` + + `` + + `` + + placeMark(mark(GOLD, CREAM), 512, 512, 536) + + `` + ) +} + +// Windows/Linux app icon: closer to full-bleed (taskbars don't add margins), +// same rounded-square language. +function winIconSVG(size) { + return ( + `` + + `${BG_DEFS}` + + `` + + placeMark(mark(GOLD, CREAM), 512, 512, 620) + + `` + ) +} + +// Tray glyph: transparent background, mark only. The dot doubles as the +// status light — gold when the control plane is running, gray otherwise. +function traySVG(size, active, glyph) { + const text = glyph === 'light' ? (active ? CREAM : GRAY_LIGHT) : active ? '#0c0b09' : GRAY_DARK + const dot = active ? GOLD : glyph === 'light' ? GRAY_LIGHT : GRAY_DARK + return ( + `` + + placeMark(mark(dot, text), size / 2, size / 2, size - 2) + + `` + ) +} + +// ---- ICNS container ---------------------------------------------------------- +// Modern icns is just a TOC of PNG blobs: 'icns' + length, then per-entry +// 4-char type + length + PNG bytes. + +const ICNS_TYPES = [ + ['ic10', 1024], + ['ic14', 512], + ['ic09', 512], + ['ic13', 256], + ['ic08', 256], + ['ic07', 128], + ['ic12', 64], + ['ic11', 32] +] + +function buildIcns(pngBySize) { + const chunks = [] + for (const [type, size] of ICNS_TYPES) { + const png = pngBySize.get(size) + const header = Buffer.alloc(8) + header.write(type, 0, 'ascii') + header.writeUInt32BE(png.length + 8, 4) + chunks.push(header, png) + } + const body = Buffer.concat(chunks) + const head = Buffer.alloc(8) + head.write('icns', 0, 'ascii') + head.writeUInt32BE(body.length + 8, 4) + return Buffer.concat([head, body]) +} + +// ---- Rasterizer --------------------------------------------------------------- + +app.disableHardwareAcceleration() + +async function rasterize(win, svg, size) { + const html = + `` + + `` + + svg + await win.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(html)) + await new Promise((r) => setTimeout(r, 120)) // let the offscreen frame settle + let image = await win.webContents.capturePage({ x: 0, y: 0, width: size, height: size }) + if (image.getSize().width !== size) { + image = image.resize({ width: size, height: size, quality: 'best' }) + } + return image.toPNG() +} + +app.whenReady().then(async () => { + const win = new BrowserWindow({ + width: 1024, + height: 1024, + show: false, + frame: false, + transparent: true, + useContentSize: true, + webPreferences: { offscreen: true } + }) + + const out = (...p) => join(desktopDir, ...p) + mkdirSync(out('build'), { recursive: true }) + mkdirSync(out('resources', 'tray'), { recursive: true }) + + // macOS icns members. + const macPngs = new Map() + for (const size of [1024, 512, 256, 128, 64, 32]) { + macPngs.set(size, await rasterize(win, macIconSVG(size), size)) + } + const icns = buildIcns(macPngs) + writeFileSync(out('build', 'icon.icns'), icns) + writeFileSync( + join(desktopDir, '..', 'control-plane', 'cmd', 'af-tray', 'assets', 'appicon.icns'), + icns + ) + + // Windows/Linux app + window icons. + writeFileSync(out('build', 'icon.png'), await rasterize(win, winIconSVG(1024), 1024)) + writeFileSync(out('resources', 'icon.png'), await rasterize(win, winIconSVG(256), 256)) + + // Tray glyphs: 16 (1x), 24 (1.5x), 32 (2x) per variant. + for (const active of [true, false]) { + for (const glyph of ['light', 'dark']) { + for (const size of [16, 24, 32]) { + const name = `tray-${active ? 'active' : 'inactive'}-${glyph}-${size}.png` + writeFileSync( + out('resources', 'tray', name), + await rasterize(win, traySVG(size, active, glyph), size) + ) + } + } + } + + console.log('icons written to build/, resources/, and af-tray assets') + app.exit(0) +}) diff --git a/desktop/src/main/agentfield.test.ts b/desktop/src/main/agentfield.test.ts new file mode 100644 index 000000000..143b149f5 --- /dev/null +++ b/desktop/src/main/agentfield.test.ts @@ -0,0 +1,506 @@ +import { promises as fs } from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { + checkControlPlane, + deriveAgentBadge, + fetchControlPlaneNodes, + fetchExecutions, + getAgentFieldHome, + getSnapshot, + readInstalledAgents, + type FetchLike +} from './agentfield' +import { installCommand, sanitizeInstallOutput } from './installer' +import { CATALOG, catalogEntry } from '../shared/catalog' + +const tmpDirs: string[] = [] + +async function makeHome(installedYaml?: string): Promise { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'agentfield-desktop-test-')) + tmpDirs.push(dir) + if (installedYaml !== undefined) { + await fs.writeFile(path.join(dir, 'installed.yaml'), installedYaml, 'utf8') + } + return dir +} + +afterEach(async () => { + await Promise.all( + tmpDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })) + ) +}) + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { 'content-type': 'application/json' } + }) +} + +const REGISTRY_FIXTURE = `installed: + pr-af: + name: pr-af + version: 0.1.0 + description: Opens draft pull requests from a task description + path: /home/abir/.agentfield/packages/pr-af + source: local + source_path: ./fix-praf + installed_at: "2026-07-08T10:35:03-04:00" + status: running + language: python + runtime: + port: 9001 + pid: 4242 + started_at: "2026-07-08T10:36:00-04:00" + log_file: /home/abir/.agentfield/logs/pr-af.log + swe-af: + version: 0.2.1 + description: Software engineering agent + status: stopped + runtime: + port: null + pid: null + started_at: null + log_file: /home/abir/.agentfield/logs/swe-af.log +` + +describe('getAgentFieldHome', () => { + it('is /.agentfield', () => { + expect(getAgentFieldHome()).toBe(path.join(os.homedir(), '.agentfield')) + }) +}) + +describe('readInstalledAgents', () => { + // Contract: registry with running + stopped entries (including null runtime + // fields and a missing optional language) parses into a correct agents array. + it('parses running and stopped entries, null runtime fields, optional language', async () => { + const home = await makeHome(REGISTRY_FIXTURE) + const result = await readInstalledAgents(home) + + expect(result.exists).toBe(true) + expect(result.error).toBeUndefined() + expect(result.agents).toHaveLength(2) + + const prAf = result.agents.find((a) => a.name === 'pr-af') + expect(prAf).toEqual({ + name: 'pr-af', + version: '0.1.0', + description: 'Opens draft pull requests from a task description', + language: 'python', + status: 'running', + path: '/home/abir/.agentfield/packages/pr-af', + port: 9001, + pid: 4242 + }) + + // Entry without a `name` field falls back to its registry key; nulls stay null. + const sweAf = result.agents.find((a) => a.name === 'swe-af') + expect(sweAf).toEqual({ + name: 'swe-af', + version: '0.2.1', + description: 'Software engineering agent', + language: undefined, + status: 'stopped', + path: null, + port: null, + pid: null + }) + }) + + // Contract: missing installed.yaml (or missing ~/.agentfield entirely) is a + // graceful empty state, not an error. + it('returns { exists: false, agents: [] } when installed.yaml is missing', async () => { + const home = await makeHome() // dir exists, no installed.yaml + expect(await readInstalledAgents(home)).toEqual({ exists: false, agents: [] }) + }) + + it('returns { exists: false, agents: [] } when the home dir itself is missing', async () => { + const home = await makeHome() + const missing = path.join(home, 'does-not-exist') + expect(await readInstalledAgents(missing)).toEqual({ exists: false, agents: [] }) + }) + + // Contract: malformed YAML surfaces as an error string — never throws. + it('surfaces malformed YAML as an error string without throwing', async () => { + const home = await makeHome('installed:\n pr-af: [unclosed\n') + const result = await readInstalledAgents(home) + expect(result.exists).toBe(true) + expect(result.agents).toEqual([]) + expect(result.error).toContain('installed.yaml') + }) + + it('treats a YAML doc without an installed map as an empty registry', async () => { + const home = await makeHome('something_else: true\n') + const result = await readInstalledAgents(home) + expect(result).toEqual({ exists: true, agents: [] }) + }) +}) + +describe('deriveAgentBadge', () => { + // Contract: full truth table. + it.each([ + // [registryStatus, cpReachable, nodeSeen, expected] + // CP view unavailable -> trust the registry + ['running', false, false, 'running'], + ['running', false, true, 'running'], + ['stopped', false, false, 'stopped'], + ['stopped', false, true, 'stopped'], + ['error', false, false, 'unknown'], + [undefined, false, false, 'unknown'], + // CP view available -> cross-check + ['running', true, true, 'running'], + ['running', true, false, 'unknown'], // stale registry + ['stopped', true, true, 'unknown'], // conflict + ['stopped', true, false, 'stopped'], + ['error', true, true, 'unknown'], + [undefined, true, false, 'unknown'] + ] as const)( + 'status=%s reachable=%s seen=%s -> %s', + (status, reachable, seen, expected) => { + expect(deriveAgentBadge(status, reachable, seen)).toBe(expected) + } + ) +}) + +describe('checkControlPlane', () => { + // Contract: 200 healthy body -> reachable + healthy. + it('maps a 200 healthy body to reachable/healthy', async () => { + const body = { + status: 'healthy', + timestamp: '2026-07-10T12:00:00Z', + version: '0.1.107', + checks: {} + } + const fetchImpl: FetchLike = async () => jsonResponse(body, 200) + const result = await checkControlPlane('http://localhost:8080', fetchImpl) + expect(result).toEqual({ reachable: true, recognized: true, healthy: true, raw: body }) + }) + + // Contract: 503 with an unhealthy body still means reachable, just not healthy. + it('maps a 503 unhealthy body to reachable but not healthy', async () => { + const body = { status: 'unhealthy', checks: { database: 'down' } } + const fetchImpl: FetchLike = async () => jsonResponse(body, 503) + const result = await checkControlPlane('http://localhost:8080', fetchImpl) + expect(result.reachable).toBe(true) + expect(result.recognized).toBe(true) + expect(result.healthy).toBe(false) + expect(result.raw).toEqual(body) + }) + + // Contract: a 200 from something that is NOT an AgentField control plane + // (default port 8080 is popular) must not read as healthy. Found live on + // Windows: an unrelated dev server answering {"status":"alive"} on /health + // lit the dashboard green. + it('rejects a foreign 200 /health payload as unrecognized', async () => { + const body = { status: 'alive', uptime_s: 3714 } + const fetchImpl: FetchLike = async () => jsonResponse(body, 200) + const result = await checkControlPlane('http://localhost:8080', fetchImpl) + expect(result.reachable).toBe(true) + expect(result.recognized).toBe(false) + expect(result.healthy).toBe(false) + expect(result.error).toContain('does not look like an AgentField control plane') + }) + + it('rejects a non-JSON 200 response as unrecognized', async () => { + const fetchImpl: FetchLike = async () => + new Response('hi', { status: 200, headers: { 'content-type': 'text/html' } }) + const result = await checkControlPlane('http://localhost:8080', fetchImpl) + expect(result.reachable).toBe(true) + expect(result.recognized).toBe(false) + expect(result.healthy).toBe(false) + }) + + // Contract: network error / timeout -> not reachable, error captured. + it('maps a rejected fetch to unreachable with an error message', async () => { + const fetchImpl: FetchLike = async () => { + throw new TypeError('fetch failed') + } + const result = await checkControlPlane('http://localhost:8080', fetchImpl) + expect(result).toEqual({ + reachable: false, + recognized: false, + healthy: false, + error: 'fetch failed' + }) + }) + + it('probes {baseUrl}/health', async () => { + let requested = '' + const fetchImpl: FetchLike = async (input) => { + requested = String(input) + return jsonResponse({ status: 'healthy' }) + } + await checkControlPlane('http://example.test:1234', fetchImpl) + expect(requested).toBe('http://example.test:1234/health') + }) +}) + +describe('fetchControlPlaneNodes', () => { + it('returns node ids from a 200 nodes payload', async () => { + const fetchImpl: FetchLike = async () => + jsonResponse({ + nodes: [ + { id: 'pr-af', health_status: 'active' }, + { id: 'swe-af', health_status: 'active' } + ], + count: 2 + }) + expect(await fetchControlPlaneNodes('http://localhost:8080', fetchImpl)).toEqual([ + 'pr-af', + 'swe-af' + ]) + }) + + it('returns null on a non-200 response', async () => { + const fetchImpl: FetchLike = async () => jsonResponse({ error: 'nope' }, 500) + expect(await fetchControlPlaneNodes('http://localhost:8080', fetchImpl)).toBeNull() + }) + + it('returns null when fetch rejects', async () => { + const fetchImpl: FetchLike = async () => { + throw new TypeError('fetch failed') + } + expect(await fetchControlPlaneNodes('http://localhost:8080', fetchImpl)).toBeNull() + }) + + it('returns null on an unexpected payload shape', async () => { + const fetchImpl: FetchLike = async () => jsonResponse({ items: [] }) + expect(await fetchControlPlaneNodes('http://localhost:8080', fetchImpl)).toBeNull() + }) +}) + +describe('fetchExecutions', () => { + const runRow = (overrides: Record) => ({ + run_id: 'run_1', + status: 'succeeded', + display_name: 'demo_echo', + agent_id: 'smoke-agent', + started_at: '2026-07-13T13:51:39Z', + duration_ms: 45, + terminal: true, + ...overrides + }) + + it('splits rows into running (non-terminal) and recent (terminal)', async () => { + const fetchImpl: FetchLike = async () => + jsonResponse({ + runs: [ + runRow({ run_id: 'run_live', status: 'running', terminal: false, duration_ms: null }), + runRow({ run_id: 'run_done' }) + ], + total_count: 2 + }) + const result = await fetchExecutions('http://localhost:8080', fetchImpl) + expect(result).not.toBeNull() + expect(result!.running.map((r) => r.runId)).toEqual(['run_live']) + expect(result!.recent.map((r) => r.runId)).toEqual(['run_done']) + expect(result!.recent[0]).toEqual({ + runId: 'run_done', + status: 'succeeded', + displayName: 'demo_echo', + agentId: 'smoke-agent', + startedAt: '2026-07-13T13:51:39Z', + durationMs: 45, + terminal: true + }) + }) + + it('caps recent executions at 5', async () => { + const runs = Array.from({ length: 9 }, (_, i) => runRow({ run_id: `run_${i}` })) + const fetchImpl: FetchLike = async () => jsonResponse({ runs, total_count: 9 }) + const result = await fetchExecutions('http://localhost:8080', fetchImpl) + expect(result!.recent).toHaveLength(5) + }) + + it('drops rows without a run_id instead of failing', async () => { + const fetchImpl: FetchLike = async () => + jsonResponse({ runs: [runRow({}), { status: 'running' }], total_count: 2 }) + const result = await fetchExecutions('http://localhost:8080', fetchImpl) + expect(result!.recent).toHaveLength(1) + expect(result!.running).toHaveLength(0) + }) + + it.each([ + ['non-200 response', async () => jsonResponse({ error: 'nope' }, 500)], + ['junk payload', async () => jsonResponse({ items: [] })], + [ + 'rejected fetch', + async () => { + throw new TypeError('fetch failed') + } + ] + ] as const)('returns null on %s', async (_name, fetchImpl) => { + expect(await fetchExecutions('http://localhost:8080', fetchImpl)).toBeNull() + }) +}) + +describe('install catalog', () => { + it('every entry has a name, description, and an https or af:// source', () => { + expect(CATALOG.length).toBeGreaterThan(0) + for (const entry of CATALOG) { + expect(entry.name).toMatch(/^[a-z0-9][a-z0-9-]*$/) + expect(entry.description.length).toBeGreaterThan(0) + expect(entry.source).toMatch(/^(https:\/\/|af:\/\/)/) + } + }) + + it('entry names are unique', () => { + const names = CATALOG.map((e) => e.name) + expect(new Set(names).size).toBe(names.length) + }) + + it('catalogEntry resolves known names and rejects unknown ones', () => { + expect(catalogEntry(CATALOG[0].name)).toEqual(CATALOG[0]) + expect(catalogEntry('definitely-not-real')).toBeUndefined() + }) +}) + +describe('installCommand', () => { + // Contract: the renderer sends catalog *names* over IPC; only vetted + // sources ever reach spawn, and unknown names are refused. + it('builds `af install ` for a catalog name', () => { + expect(installCommand(CATALOG[0].name)).toEqual({ + command: 'af', + args: ['install', CATALOG[0].source] + }) + }) + + it('returns null for names not in the catalog', () => { + expect(installCommand('evil; rm -rf /')).toBeNull() + expect(installCommand('')).toBeNull() + }) +}) + +describe('sanitizeInstallOutput', () => { + it('strips ANSI color and erase codes and splits spinner frames', () => { + const esc = String.fromCharCode(27) + const chunk = `${esc}[32m✓ Dependencies installed${esc}[0m\r${esc}[K✓ Installed swe-af v0.2.0\n` + expect(sanitizeInstallOutput(chunk)).toEqual([ + '✓ Dependencies installed', + '✓ Installed swe-af v0.2.0' + ]) + }) + + it('drops empty and whitespace-only lines', () => { + expect(sanitizeInstallOutput('\r\n \n\r')).toEqual([]) + }) +}) + +describe('getSnapshot', () => { + function routedFetch(routes: Record Response>): FetchLike { + return async (input) => { + const url = String(input) + const route = Object.keys(routes).find((suffix) => url.endsWith(suffix)) + if (!route) throw new TypeError(`unexpected fetch: ${url}`) + return routes[route]() + } + } + + it('composes control plane + registry with cross-checked badges', async () => { + const home = await makeHome(REGISTRY_FIXTURE) + const fetchImpl = routedFetch({ + '/health': () => jsonResponse({ status: 'healthy' }), + // Control plane sees pr-af but not swe-af. + '/api/v1/nodes': () => jsonResponse({ nodes: [{ id: 'pr-af' }], count: 1 }), + 'sort_order=desc': () => + jsonResponse({ + runs: [ + { + run_id: 'run_live', + status: 'running', + display_name: 'summarize', + agent_id: 'pr-af', + started_at: '2026-07-13T13:51:39Z', + duration_ms: null, + terminal: false + } + ], + total_count: 1 + }), + '/dashboard/summary': () => + jsonResponse({ + agents: { running: 1, total: 2 }, + executions: { today: 4, yesterday: 2 }, + success_rate: 100, + packages: { available: 1, installed: 0 } + }) + }) + + const snapshot = await getSnapshot({ homeDir: home, fetchImpl }) + + expect(snapshot.controlPlane.baseUrl).toBe('http://localhost:8080') + expect(snapshot.controlPlane.reachable).toBe(true) + expect(snapshot.controlPlane.healthy).toBe(true) + expect(snapshot.registry.exists).toBe(true) + expect(snapshot.executions?.running.map((r) => r.runId)).toEqual(['run_live']) + expect(snapshot.metrics).toEqual({ + agentsRunning: 1, + agentsTotal: 2, + executionsToday: 4, + executionsYesterday: 2, + successRate: 100 + }) + expect(Date.parse(snapshot.fetchedAt)).not.toBeNaN() + + const badges = Object.fromEntries( + snapshot.registry.agents.map((a) => [a.name, a.badge]) + ) + expect(badges).toEqual({ + 'pr-af': 'running', // registry running + seen on CP + 'swe-af': 'stopped' // registry stopped + not seen + }) + }) + + it('falls back to registry status when the nodes endpoint fails', async () => { + const home = await makeHome(REGISTRY_FIXTURE) + const fetchImpl = routedFetch({ + '/health': () => jsonResponse({ status: 'healthy' }), + '/api/v1/nodes': () => jsonResponse({ error: 'boom' }, 500) + }) + + const snapshot = await getSnapshot({ homeDir: home, fetchImpl }) + const badges = Object.fromEntries( + snapshot.registry.agents.map((a) => [a.name, a.badge]) + ) + // Nodes view unavailable -> trust registry statuses directly. + expect(badges).toEqual({ 'pr-af': 'running', 'swe-af': 'stopped' }) + }) + + it('does not consult the nodes view of an unrecognized service on the port', async () => { + const home = await makeHome(REGISTRY_FIXTURE) + const requested: string[] = [] + const fetchImpl: FetchLike = async (input) => { + requested.push(String(input)) + // A foreign service that would answer BOTH endpoints with junk. + return jsonResponse({ status: 'alive', nodes: [] }) + } + + const snapshot = await getSnapshot({ homeDir: home, fetchImpl }) + + expect(snapshot.controlPlane.recognized).toBe(false) + // Badges fall back to registry statuses — the foreign 200 on /api/v1/nodes + // must not flip a running agent to unknown. + const badges = Object.fromEntries( + snapshot.registry.agents.map((a) => [a.name, a.badge]) + ) + expect(badges).toEqual({ 'pr-af': 'running', 'swe-af': 'stopped' }) + expect(requested.some((url) => url.endsWith('/api/v1/nodes'))).toBe(false) + // Nor may its workflow runs show up as activity. + expect(snapshot.executions).toBeNull() + expect(requested.some((url) => url.includes('/workflow-runs'))).toBe(false) + }) + + it('reports an unreachable control plane and an absent registry gracefully', async () => { + const home = await makeHome() + const missing = path.join(home, 'nope') + const fetchImpl: FetchLike = async () => { + throw new TypeError('fetch failed') + } + + const snapshot = await getSnapshot({ homeDir: missing, fetchImpl }) + expect(snapshot.controlPlane.reachable).toBe(false) + expect(snapshot.registry).toEqual({ exists: false, agents: [], error: undefined }) + }) +}) diff --git a/desktop/src/main/agentfield.ts b/desktop/src/main/agentfield.ts new file mode 100644 index 000000000..c607baea0 --- /dev/null +++ b/desktop/src/main/agentfield.ts @@ -0,0 +1,322 @@ +// TODO(af-cli): this module currently reads ~/.agentfield/installed.yaml directly; +// a sibling branch is adding `af list -o json` — swap readInstalledAgents() to shell +// out to that once it lands, so the CLI stays the single source of truth for +// registry parsing. +// +// This is THE single data-access module for AgentField Desktop. Everything that +// touches the AgentField installation (~/.agentfield) or the control plane HTTP +// API lives here and nowhere else. It deliberately does NOT import from +// 'electron' so it stays unit-testable under plain vitest. + +import { promises as fs } from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import yaml from 'js-yaml' +import type { + AgentBadge, + AgentFieldSnapshot, + ControlPlaneStatus, + DashboardMetrics, + ExecutionsResult, + ExecutionSummary, + InstalledAgent, + RegistryResult +} from '../shared/types' + +export const DEFAULT_BASE_URL = 'http://localhost:8080' + +const HTTP_TIMEOUT_MS = 3000 + +/** Injectable fetch so tests never hit the network. */ +export type FetchLike = typeof fetch + +/** Root of the local AgentField installation. os.homedir() is platform-aware + * (resolves %USERPROFILE% on Windows, $HOME elsewhere). */ +export function getAgentFieldHome(): string { + return path.join(os.homedir(), '.agentfield') +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function errorMessage(err: unknown): string { + return err instanceof Error ? err.message : String(err) +} + +/** + * Probe GET {baseUrl}/health. + * - 200 {"status":"healthy",...} -> { reachable: true, recognized: true, healthy: true } + * - 503 {"status":"unhealthy",...} -> { reachable: true, recognized: true, healthy: false } + * (an HTTP response — even 503 — still means the control plane is reachable) + * - any response whose body is not an AgentField health payload + * -> { reachable: true, recognized: false, healthy: false, error } + * (default port 8080 is popular — an unrelated dev server answering 200 on + * /health must not light up the dashboard as a running control plane) + * - network error / timeout (3s) -> { reachable: false, recognized: false, healthy: false, error } + */ +export async function checkControlPlane( + baseUrl: string = DEFAULT_BASE_URL, + fetchImpl: FetchLike = fetch +): Promise { + try { + const res = await fetchImpl(`${baseUrl}/health`, { + signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) + }) + let raw: unknown + try { + raw = await res.json() + } catch { + raw = undefined + } + const status = isRecord(raw) && typeof raw.status === 'string' ? raw.status : undefined + const recognized = status === 'healthy' || status === 'unhealthy' + if (!recognized) { + return { + reachable: true, + recognized: false, + healthy: false, + raw, + error: `A service answered ${baseUrl}/health but does not look like an AgentField control plane — another app may be using the port.` + } + } + return { reachable: true, recognized: true, healthy: res.ok && status === 'healthy', raw } + } catch (err) { + return { reachable: false, recognized: false, healthy: false, error: errorMessage(err) } + } +} + +function toInstalledAgent(key: string, entry: unknown): InstalledAgent { + const record = isRecord(entry) ? entry : {} + const runtime = isRecord(record.runtime) ? record.runtime : {} + return { + name: typeof record.name === 'string' && record.name !== '' ? record.name : key, + version: typeof record.version === 'string' ? record.version : '', + description: typeof record.description === 'string' ? record.description : '', + language: typeof record.language === 'string' ? record.language : undefined, + status: typeof record.status === 'string' ? record.status : 'unknown', + path: typeof record.path === 'string' && record.path !== '' ? record.path : null, + port: typeof runtime.port === 'number' ? runtime.port : null, + pid: typeof runtime.pid === 'number' ? runtime.pid : null + } +} + +/** + * Read /installed.yaml (the local agent-node registry). + * - Missing file or missing ~/.agentfield dir -> { exists: false, agents: [] } + * (graceful empty state, NOT an error). + * - Malformed YAML -> error surfaced as a string in the result; never throws, + * so nothing blows up across the IPC boundary. + */ +export async function readInstalledAgents( + homeDir: string = getAgentFieldHome() +): Promise { + const registryPath = path.join(homeDir, 'installed.yaml') + let text: string + try { + text = await fs.readFile(registryPath, 'utf8') + } catch (err) { + const code = (err as NodeJS.ErrnoException).code + if (code === 'ENOENT' || code === 'ENOTDIR') { + return { exists: false, agents: [] } + } + return { exists: false, agents: [], error: errorMessage(err) } + } + + let doc: unknown + try { + doc = yaml.load(text) + } catch (err) { + return { + exists: true, + agents: [], + error: `Failed to parse ${registryPath}: ${errorMessage(err)}` + } + } + + const installed = isRecord(doc) && isRecord(doc.installed) ? doc.installed : {} + const agents = Object.entries(installed).map(([key, entry]) => toInstalledAgent(key, entry)) + return { exists: true, agents } +} + +/** + * GET {baseUrl}/api/v1/nodes -> {"nodes":[{"id":...,"health_status":...},...],"count":N} + * (the server's default filter returns active nodes only). + * Returns the list of node ids, or null on any failure — callers treat null as + * "control plane view unavailable" and fall back to registry status alone. + */ +export async function fetchControlPlaneNodes( + baseUrl: string = DEFAULT_BASE_URL, + fetchImpl: FetchLike = fetch +): Promise { + try { + const res = await fetchImpl(`${baseUrl}/api/v1/nodes`, { + signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) + }) + if (!res.ok) return null + const body: unknown = await res.json() + if (!isRecord(body) || !Array.isArray(body.nodes)) return null + return body.nodes + .filter(isRecord) + .map((node) => (typeof node.id === 'string' ? node.id : '')) + .filter((id) => id.length > 0) + } catch { + return null + } +} + +/** + * Pure badge derivation. `controlPlaneReachable` here means "we have a usable + * control-plane node view" (health reachable AND the nodes list fetched). + * + * CP view unavailable — trust the registry: + * 'running' -> 'running' | 'stopped' -> 'stopped' | other/absent -> 'unknown' + * CP view available — cross-check: + * registry running + node seen -> 'running' + * registry running + node NOT seen -> 'unknown' (stale registry) + * registry stopped + node seen -> 'unknown' (conflict) + * registry stopped + node NOT seen -> 'stopped' + * other/absent registry status -> 'unknown' + */ +export function deriveAgentBadge( + registryStatus: string | undefined, + controlPlaneReachable: boolean, + nodeSeenOnControlPlane: boolean +): AgentBadge { + if (!controlPlaneReachable) { + if (registryStatus === 'running') return 'running' + if (registryStatus === 'stopped') return 'stopped' + return 'unknown' + } + if (registryStatus === 'running') { + return nodeSeenOnControlPlane ? 'running' : 'unknown' + } + if (registryStatus === 'stopped') { + return nodeSeenOnControlPlane ? 'unknown' : 'stopped' + } + return 'unknown' +} + +const RECENT_EXECUTIONS_LIMIT = 5 + +function toExecutionSummary(row: Record): ExecutionSummary | null { + const runId = typeof row.run_id === 'string' ? row.run_id : '' + if (!runId) return null + return { + runId, + status: typeof row.status === 'string' ? row.status : 'unknown', + displayName: + typeof row.display_name === 'string' && row.display_name !== '' + ? row.display_name + : runId, + agentId: typeof row.agent_id === 'string' ? row.agent_id : '', + startedAt: typeof row.started_at === 'string' ? row.started_at : '', + durationMs: typeof row.duration_ms === 'number' ? row.duration_ms : null, + terminal: row.terminal === true + } +} + +/** + * GET {baseUrl}/api/ui/v2/workflow-runs (newest first) and split the rows into + * in-flight runs and a short tail of finished ones. Returns null on any + * failure — callers render "activity unavailable", never an error page. + */ +export async function fetchExecutions( + baseUrl: string = DEFAULT_BASE_URL, + fetchImpl: FetchLike = fetch +): Promise { + try { + const res = await fetchImpl( + `${baseUrl}/api/ui/v2/workflow-runs?page=1&page_size=25&sort_by=updated_at&sort_order=desc`, + { signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) } + ) + if (!res.ok) return null + const body: unknown = await res.json() + if (!isRecord(body) || !Array.isArray(body.runs)) return null + const summaries = body.runs + .filter(isRecord) + .map(toExecutionSummary) + .filter((s): s is ExecutionSummary => s !== null) + return { + running: summaries.filter((s) => !s.terminal), + recent: summaries.filter((s) => s.terminal).slice(0, RECENT_EXECUTIONS_LIMIT) + } + } catch { + return null + } +} + +/** + * GET {baseUrl}/api/ui/v1/dashboard/summary -> headline dashboard numbers. + * Returns null on any failure; the dashboard renders placeholders instead. + */ +export async function fetchDashboardMetrics( + baseUrl: string = DEFAULT_BASE_URL, + fetchImpl: FetchLike = fetch +): Promise { + try { + const res = await fetchImpl(`${baseUrl}/api/ui/v1/dashboard/summary`, { + signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) + }) + if (!res.ok) return null + const body: unknown = await res.json() + if (!isRecord(body)) return null + const agents = isRecord(body.agents) ? body.agents : {} + const executions = isRecord(body.executions) ? body.executions : {} + return { + agentsRunning: typeof agents.running === 'number' ? agents.running : 0, + agentsTotal: typeof agents.total === 'number' ? agents.total : 0, + executionsToday: typeof executions.today === 'number' ? executions.today : 0, + executionsYesterday: + typeof executions.yesterday === 'number' ? executions.yesterday : 0, + successRate: typeof body.success_rate === 'number' ? body.success_rate : null + } + } catch { + return null + } +} + +export interface SnapshotOptions { + baseUrl?: string + homeDir?: string + fetchImpl?: FetchLike +} + +/** + * Compose everything into the single IPC payload the renderer polls. + * Options exist only for tests; production callers use the defaults. + */ +export async function getSnapshot(options: SnapshotOptions = {}): Promise { + const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL + const fetchImpl = options.fetchImpl ?? fetch + + const [controlPlane, registry] = await Promise.all([ + checkControlPlane(baseUrl, fetchImpl), + readInstalledAgents(options.homeDir) + ]) + + // Only consult a recognized control plane; an unrelated service on the + // port must not influence badges or show foreign runs as activity. + const [nodeIds, executions, metrics] = controlPlane.recognized + ? await Promise.all([ + fetchControlPlaneNodes(baseUrl, fetchImpl), + fetchExecutions(baseUrl, fetchImpl), + fetchDashboardMetrics(baseUrl, fetchImpl) + ]) + : [null, null, null] + const hasControlPlaneView = nodeIds !== null + const seen = new Set(nodeIds ?? []) + + const agents = registry.agents.map((agent) => ({ + ...agent, + badge: deriveAgentBadge(agent.status, hasControlPlaneView, seen.has(agent.name)) + })) + + return { + controlPlane: { ...controlPlane, baseUrl }, + registry: { exists: registry.exists, agents, error: registry.error }, + executions, + metrics, + fetchedAt: new Date().toISOString() + } +} diff --git a/desktop/src/main/agents.ts b/desktop/src/main/agents.ts new file mode 100644 index 000000000..0fc08c42e --- /dev/null +++ b/desktop/src/main/agents.ts @@ -0,0 +1,153 @@ +// Agent + control-plane lifecycle seam: shells out to the af CLI (the single +// contract — the app never reimplements start/stop). No electron imports so +// the module stays unit-testable. +// +// CLI semantics this leans on (control-plane/internal/cli): +// - `af run ` spawns the agent detached, waits for its local /health, +// and exits — the agent survives the CLI (and this app) exiting. +// - `af stop ` shuts down gracefully (HTTP /shutdown, then signal, +// then force) and flips the registry entry to stopped. +// - there is no `af restart` — restart here is stop-then-run. +// - `af server` always blocks, so the control plane is spawned detached +// with its output appended to ~/.agentfield/logs/control-plane.log. + +import { spawn } from 'node:child_process' +import { closeSync, mkdirSync, openSync } from 'node:fs' +import { join } from 'node:path' +import type { AgentActionResult } from '../shared/types' +import { checkControlPlane, getAgentFieldHome, readInstalledAgents } from './agentfield' +import { getCliCommand } from './cli' +import { sanitizeInstallOutput } from './installer' + +export type AgentAction = 'start' | 'stop' | 'restart' + +/** How long one `af run`/`af stop` may take (run waits ≤30s for readiness). */ +const CLI_TIMEOUT_MS = 90_000 + +const MISSING_CLI_MESSAGE = + 'The AgentField CLI (af) was not found on PATH. Install it first: https://agentfield.ai/docs' + +/** Run one af verb to completion, capturing the last meaningful output line. */ +function runCli(args: string[], timeoutMs = CLI_TIMEOUT_MS): Promise { + return new Promise((resolve) => { + let lastLine = '' + let settled = false + const done = (result: AgentActionResult) => { + if (!settled) { + settled = true + resolve(result) + } + } + + const child = spawn(getCliCommand(), args, { windowsHide: true }) + const timer = setTimeout(() => { + child.kill() + done({ ok: false, message: `af ${args.join(' ')} timed out` }) + }, timeoutMs) + + const collect = (chunk: Buffer) => { + const lines = sanitizeInstallOutput(chunk.toString('utf8')) + if (lines.length > 0) lastLine = lines[lines.length - 1] + } + child.stdout.on('data', collect) + child.stderr.on('data', collect) + child.on('error', (err: NodeJS.ErrnoException) => { + clearTimeout(timer) + done({ + ok: false, + message: err.code === 'ENOENT' ? MISSING_CLI_MESSAGE : `Failed to run af: ${err.message}` + }) + }) + child.on('close', (code) => { + clearTimeout(timer) + done( + code === 0 + ? { ok: true, message: lastLine } + : { ok: false, message: lastLine || `af ${args.join(' ')} exited with code ${code}` } + ) + }) + }) +} + +/** + * Start / stop / restart an installed agent by registry name. The name is + * validated against ~/.agentfield/installed.yaml — the renderer only ever + * supplies names, and unknown ones are refused rather than handed to a shell. + */ +export async function runAgentAction( + action: AgentAction, + name: string +): Promise { + const registry = await readInstalledAgents() + if (!registry.agents.some((agent) => agent.name === name)) { + return { ok: false, message: `"${name}" is not an installed agent` } + } + + switch (action) { + case 'start': + return runCli(['run', name]) + case 'stop': + return runCli(['stop', name]) + case 'restart': { + // `af stop` exits cleanly when the agent is already stopped, so a + // restart of a wedged ("unknown") agent degrades to a plain start. + const stopped = await runCli(['stop', name]) + if (!stopped.ok) return stopped + return runCli(['run', name]) + } + } +} + +/** + * Spawn `af server` detached — it outlives the app, matching the "agents on + * autopilot" model — and wait until /health reports an AgentField control + * plane. Output goes to ~/.agentfield/logs/control-plane.log (same file the + * macOS launchd agent uses). + */ +export async function startControlPlane( + waitMs = 30_000 +): Promise { + let log: number + try { + const logsDir = join(getAgentFieldHome(), 'logs') + mkdirSync(logsDir, { recursive: true }) + log = openSync(join(logsDir, 'control-plane.log'), 'a') + } catch (err) { + return { ok: false, message: `could not open control-plane log: ${String(err)}` } + } + + try { + const child = spawn(getCliCommand(), ['server'], { + windowsHide: true, + detached: true, + stdio: ['ignore', log, log] + }) + const spawnError = new Promise((resolve) => { + child.on('error', (err: NodeJS.ErrnoException) => { + resolve({ + ok: false, + message: err.code === 'ENOENT' ? MISSING_CLI_MESSAGE : String(err.message) + }) + }) + }) + child.unref() + + const deadline = Date.now() + waitMs + while (Date.now() < deadline) { + const raced = await Promise.race([ + spawnError, + new Promise((resolve) => setTimeout(() => resolve(null), 1_000)) + ]) + if (raced) return raced + const status = await checkControlPlane() + if (status.healthy) return { ok: true, message: 'control plane running' } + // A foreign service answering the port will never become healthy. + if (status.reachable && !status.recognized) { + return { ok: false, message: status.error ?? 'port in use by another app' } + } + } + return { ok: false, message: 'control plane did not become healthy in time' } + } finally { + closeSync(log) + } +} diff --git a/desktop/src/main/autostart.test.ts b/desktop/src/main/autostart.test.ts new file mode 100644 index 000000000..936b532b7 --- /dev/null +++ b/desktop/src/main/autostart.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from 'vitest' +import type { AgentFieldSnapshot, DesktopSettings, SnapshotAgent } from '../shared/types' +import { autostartAgentPlan, shouldStartControlPlane } from './autostart' + +function agent(name: string, badge: SnapshotAgent['badge']): SnapshotAgent { + return { + name, + version: '0.1.0', + description: '', + status: badge === 'running' ? 'running' : 'stopped', + path: null, + port: null, + pid: null, + badge + } +} + +function settings(overrides: Partial): DesktopSettings { + return { + openAtLogin: false, + autostartControlPlane: true, + autostartAgents: [], + installSkills: true, + ...overrides + } +} + +function cp(overrides: Partial): AgentFieldSnapshot['controlPlane'] { + return { + baseUrl: 'http://localhost:8080', + reachable: false, + recognized: false, + healthy: false, + ...overrides + } +} + +describe('autostartAgentPlan', () => { + const installed = [agent('a', 'stopped'), agent('b', 'running'), agent('c', 'unknown')] + + it('starts stopped agents and skips running ones', () => { + expect(autostartAgentPlan(['a', 'b'], installed)).toEqual([{ name: 'a', action: 'start' }]) + }) + + it('restarts unknown agents (stale registry after reboot/crash)', () => { + expect(autostartAgentPlan(['c'], installed)).toEqual([{ name: 'c', action: 'restart' }]) + }) + + it('skips selections that are no longer installed', () => { + expect(autostartAgentPlan(['ghost'], installed)).toEqual([]) + }) + + it('preserves selection order', () => { + expect(autostartAgentPlan(['c', 'a'], installed).map((s) => s.name)).toEqual(['c', 'a']) + }) +}) + +describe('shouldStartControlPlane', () => { + it('starts only when enabled and nothing answers', () => { + expect(shouldStartControlPlane(settings({}), cp({}))).toBe(true) + }) + + it('never starts when the setting is off', () => { + expect(shouldStartControlPlane(settings({ autostartControlPlane: false }), cp({}))).toBe(false) + }) + + it('leaves a live control plane alone, even an unhealthy one', () => { + expect( + shouldStartControlPlane( + settings({}), + cp({ reachable: true, recognized: true, healthy: true }) + ) + ).toBe(false) + expect( + shouldStartControlPlane(settings({}), cp({ reachable: true, recognized: true })) + ).toBe(false) + }) + + it('does not fight a foreign service for the port', () => { + expect(shouldStartControlPlane(settings({}), cp({ reachable: true }))).toBe(false) + }) +}) diff --git a/desktop/src/main/autostart.ts b/desktop/src/main/autostart.ts new file mode 100644 index 000000000..89642d2b9 --- /dev/null +++ b/desktop/src/main/autostart.ts @@ -0,0 +1,76 @@ +// Boot orchestration for the "it's just there" story: when the app launches +// (typically hidden, at login), bring the control plane up if nothing is +// listening, then start the agents the user selected — so everything is +// already answering by the time Claude/Codex/anything queries it. +// +// Planning is pure (unit-tested); execution shells out via agents.ts. + +import type { AgentFieldSnapshot, DesktopSettings, SnapshotAgent } from '../shared/types' +import { getSnapshot } from './agentfield' +import { runAgentAction, startControlPlane } from './agents' + +export interface AutostartStep { + name: string + action: 'start' | 'restart' +} + +/** + * Decide what to do for each selected agent, from the snapshot's badge + * (registry × control-plane view): + * - running -> nothing to do + * - stopped -> start + * - unknown -> restart: the registry claims running but the control plane + * can't see it — typical after a reboot or crash, where the registry entry + * is stale (Windows never reconciles it live). Stop-then-run clears it. + * Names no longer installed are skipped. + */ +export function autostartAgentPlan( + selected: readonly string[], + agents: readonly SnapshotAgent[] +): AutostartStep[] { + const byName = new Map(agents.map((agent) => [agent.name, agent])) + const steps: AutostartStep[] = [] + for (const name of selected) { + const agent = byName.get(name) + if (!agent || agent.badge === 'running') continue + steps.push({ name, action: agent.badge === 'unknown' ? 'restart' : 'start' }) + } + return steps +} + +/** + * Start the control plane only when nothing answers at all: an unhealthy but + * recognized control plane is already running (not ours to double-start), and + * a foreign service owns the port — starting would just fail to bind. + */ +export function shouldStartControlPlane( + settings: DesktopSettings, + cp: AgentFieldSnapshot['controlPlane'] +): boolean { + return settings.autostartControlPlane && !cp.reachable +} + +/** + * Execute the boot sequence. Agents are started even when the control plane + * could not be brought up — SDK agents serve standalone and attach when the + * control plane appears, which still beats staying down. + */ +export async function runAutostart( + settings: DesktopSettings, + log: (message: string) => void +): Promise { + let snapshot = await getSnapshot() + + if (shouldStartControlPlane(settings, snapshot.controlPlane)) { + log('autostart: starting control plane') + const result = await startControlPlane() + log(`autostart: control plane — ${result.message}`) + if (result.ok) snapshot = await getSnapshot() + } + + for (const step of autostartAgentPlan(settings.autostartAgents, snapshot.registry.agents)) { + log(`autostart: ${step.action} ${step.name}`) + const result = await runAgentAction(step.action, step.name) + log(`autostart: ${step.name} — ${result.ok ? 'up' : result.message}`) + } +} diff --git a/desktop/src/main/cli.test.ts b/desktop/src/main/cli.test.ts new file mode 100644 index 000000000..4e4b38d4f --- /dev/null +++ b/desktop/src/main/cli.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it } from 'vitest' +import { type ProbedCandidate, cliCandidates, compareVersions, parseAfVersion, selectCli } from './cli' + +function probed(overrides: Partial): ProbedCandidate { + return { command: 'af', source: 'path', responds: true, version: '0.1.107', ...overrides } +} + +describe('parseAfVersion', () => { + it('reads the Version line of `af version` output', () => { + const output = 'AgentField Control Plane\n Version: v0.1.107\n Commit: abc123\n' + expect(parseAfVersion(output)).toBe('0.1.107') + }) + + it('accepts versions without the v prefix', () => { + expect(parseAfVersion('Version: 1.2.3')).toBe('1.2.3') + }) + + it('returns null for dev builds and garbage', () => { + expect(parseAfVersion('AgentField Control Plane\n Version: dev\n')).toBeNull() + expect(parseAfVersion('command not found')).toBeNull() + expect(parseAfVersion('')).toBeNull() + }) +}) + +describe('compareVersions', () => { + it('orders numerically per segment', () => { + expect(compareVersions('0.1.107', '0.1.107')).toBe(0) + expect(compareVersions('0.1.99', '0.1.107')).toBeLessThan(0) + expect(compareVersions('0.2.0', '0.1.999')).toBeGreaterThan(0) + }) + + it('treats missing segments as zero', () => { + expect(compareVersions('0.1', '0.1.0')).toBe(0) + expect(compareVersions('1', '0.9.9')).toBeGreaterThan(0) + }) +}) + +describe('selectCli', () => { + const MIN = '0.1.107' + + it('prefers the managed copy when it qualifies', () => { + const { chosen, outdated } = selectCli( + [ + probed({ command: 'C:\\home\\.agentfield\\bin\\af.exe', source: 'managed' }), + probed({ command: 'af', source: 'path' }), + probed({ command: 'bundled/af.exe', source: 'bundled', version: '0.1.108' }) + ], + MIN + ) + expect(chosen?.source).toBe('managed') + expect(outdated).toBeNull() + }) + + it('skips non-responding candidates', () => { + const { chosen } = selectCli( + [ + probed({ source: 'managed', responds: false, version: null }), + probed({ source: 'path', responds: false, version: null }), + probed({ source: 'bundled', version: '0.1.108' }) + ], + MIN + ) + expect(chosen?.source).toBe('bundled') + }) + + it('falls through an outdated install to the bundled copy and reports it', () => { + const { chosen, outdated } = selectCli( + [ + probed({ source: 'managed', version: '0.1.90' }), + probed({ source: 'path', responds: false, version: null }), + probed({ source: 'bundled', version: '0.1.108' }) + ], + MIN + ) + expect(chosen?.source).toBe('bundled') + expect(outdated?.source).toBe('managed') + expect(outdated?.version).toBe('0.1.90') + }) + + it('trusts dev builds (unparseable version) as usable', () => { + const { chosen, outdated } = selectCli( + [probed({ source: 'path', version: null }), probed({ source: 'bundled', version: '0.1.108' })], + MIN + ) + expect(chosen?.source).toBe('path') + expect(outdated).toBeNull() + }) + + it('reports nothing usable when everything is dead or old with no bundle', () => { + const { chosen, outdated } = selectCli( + [ + probed({ source: 'managed', version: '0.1.1' }), + probed({ source: 'path', responds: false, version: null }) + ], + MIN + ) + expect(chosen).toBeNull() + expect(outdated?.version).toBe('0.1.1') + }) +}) + +describe('cliCandidates', () => { + it('orders managed before PATH before bundled', () => { + const sources = cliCandidates('/tmp/bundle/af').map((c) => c.source) + expect(sources).toEqual(['managed', 'managed', 'path', 'bundled']) + }) + + it('omits the bundled candidate when the app has none', () => { + const sources = cliCandidates(null).map((c) => c.source) + expect(sources).toEqual(['managed', 'managed', 'path']) + }) +}) diff --git a/desktop/src/main/cli.ts b/desktop/src/main/cli.ts new file mode 100644 index 000000000..d9387c54d --- /dev/null +++ b/desktop/src/main/cli.ts @@ -0,0 +1,243 @@ +// Which `af` does the app use? Non-technical users install ONLY the desktop +// app — it carries a bundled af CLI so everything works out of the box. Users +// who installed AgentField themselves (curl, dev builds) keep their copy, as +// long as it meets the version this app's features need. +// +// Resolution order (first usable wins): +// 1. managed — ~/.agentfield/bin (where the curl installer puts it, and +// where this app provisions its bundled copy: the SHARED +// location both installers converge on) +// 2. PATH — a developer's own `af` +// 3. bundled — resources/bin inside the app package +// +// A copy that answers with a semver older than MIN_AF_VERSION is skipped (the +// UI then offers "Update AgentField", which installs the bundled copy into +// the managed location — never over a newer one). Dev builds answer +// "Version: dev" and are trusted as-is. +// +// No electron imports: the bundled path is injected by main, so probing, +// selection, and provisioning stay unit-testable. + +import { spawn } from 'node:child_process' +import { promises as fs } from 'node:fs' +import { join } from 'node:path' +import type { AgentActionResult, CliStatus } from '../shared/types' +import { getAgentFieldHome } from './agentfield' + +/** Oldest af this app can drive (needs `af run/stop `, `af skill`). */ +export const MIN_AF_VERSION = '0.1.107' + +const PROBE_TIMEOUT_MS = 5_000 + +export type CliSource = 'managed' | 'path' | 'bundled' + +export interface CliCandidate { + command: string + source: CliSource +} + +export interface ProbedCandidate extends CliCandidate { + /** The command exists and ` version` exited 0. */ + responds: boolean + /** Parsed semver like "0.1.107", or null (dev builds, unparseable output). */ + version: string | null +} + +function exeName(base: string): string { + return process.platform === 'win32' ? `${base}.exe` : base +} + +export function managedBinDir(): string { + return join(getAgentFieldHome(), 'bin') +} + +/** Candidate list in resolution priority order. bundledPath may not exist. */ +export function cliCandidates(bundledPath: string | null): CliCandidate[] { + const candidates: CliCandidate[] = [ + { command: join(managedBinDir(), exeName('af')), source: 'managed' }, + { command: join(managedBinDir(), exeName('agentfield')), source: 'managed' }, + { command: 'af', source: 'path' } + ] + if (bundledPath) candidates.push({ command: bundledPath, source: 'bundled' }) + return candidates +} + +/** Pull the semver out of `af version` output (" Version: v0.1.107"). */ +export function parseAfVersion(output: string): string | null { + const match = /Version:\s*v?([0-9]+(?:\.[0-9]+)+)/.exec(output) + return match ? match[1] : null +} + +/** Numeric dotted-version compare: negative when a < b. */ +export function compareVersions(a: string, b: string): number { + const pa = a.split('.').map(Number) + const pb = b.split('.').map(Number) + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const diff = (pa[i] ?? 0) - (pb[i] ?? 0) + if (diff !== 0) return diff + } + return 0 +} + +/** + * Pick the CLI to use. Returns the first responding candidate that is not + * too old ("dev" versions are trusted), plus the best-ranked copy that WAS + * too old — that one drives the "Update AgentField" banner. + */ +export function selectCli( + probed: readonly ProbedCandidate[], + minVersion: string = MIN_AF_VERSION +): { chosen: ProbedCandidate | null; outdated: ProbedCandidate | null } { + let outdated: ProbedCandidate | null = null + for (const candidate of probed) { + if (!candidate.responds) continue + const tooOld = + candidate.version !== null && compareVersions(candidate.version, minVersion) < 0 + if (tooOld) { + if (!outdated) outdated = candidate + continue + } + return { chosen: candidate, outdated } + } + return { chosen: null, outdated } +} + +/** Run ` version` and parse the answer. Never rejects. */ +export function probeCli(candidate: CliCandidate): Promise { + return new Promise((resolve) => { + let output = '' + let settled = false + const done = (responds: boolean) => { + if (settled) return + settled = true + resolve({ ...candidate, responds, version: responds ? parseAfVersion(output) : null }) + } + + const child = spawn(candidate.command, ['version'], { windowsHide: true }) + const timer = setTimeout(() => { + child.kill() + done(false) + }, PROBE_TIMEOUT_MS) + child.stdout.on('data', (chunk: Buffer) => { + output += chunk.toString('utf8') + }) + child.on('error', () => { + clearTimeout(timer) + done(false) + }) + child.on('close', (code) => { + clearTimeout(timer) + done(code === 0) + }) + }) +} + +// ---- Active command ---------------------------------------------------------- +// agents.ts / installer.ts spawn whatever this points at. It starts as bare +// 'af' (PATH) so nothing breaks before initializeCli() has run. + +let activeCommand = 'af' + +export function getCliCommand(): string { + return activeCommand +} + +function buildStatus( + chosen: ProbedCandidate | null, + outdated: ProbedCandidate | null, + bundled: ProbedCandidate | null +): CliStatus { + return { + command: chosen?.command ?? null, + source: chosen?.source ?? null, + version: chosen?.version ?? null, + minVersion: MIN_AF_VERSION, + outdated: + outdated && outdated.version + ? { source: outdated.source, version: outdated.version } + : null, + bundledAvailable: bundled?.responds ?? false, + bundledVersion: bundled?.version ?? null + } +} + +/** + * Probe everything, pick the CLI, and — when nothing usable exists outside + * the app package (fresh machine, or every ambient copy too old) — provision + * the bundled copy into ~/.agentfield/bin so terminals and coding agents get + * a stable af too, then re-resolve. + */ +export async function initializeCli(bundledPath: string | null): Promise { + const probeAll = async () => Promise.all(cliCandidates(bundledPath).map(probeCli)) + + let probed = await probeAll() + let { chosen, outdated } = selectCli(probed) + const bundled = probed.find((c) => c.source === 'bundled') ?? null + + if ((chosen === null || chosen.source === 'bundled') && bundled?.responds) { + const installed = await installBundledCli(bundledPath as string) + if (installed.ok) { + probed = await probeAll() + ;({ chosen, outdated } = selectCli(probed)) + } + } + + if (chosen) activeCommand = chosen.command + return buildStatus(chosen, outdated, bundled) +} + +/** Re-resolve without side effects (used after an explicit update). */ +export async function refreshCliStatus(bundledPath: string | null): Promise { + const probed = await Promise.all(cliCandidates(bundledPath).map(probeCli)) + const { chosen, outdated } = selectCli(probed) + if (chosen) activeCommand = chosen.command + return buildStatus(chosen, outdated, probed.find((c) => c.source === 'bundled') ?? null) +} + +/** + * Install the bundled af into the managed location (both names the curl + * installer uses: `agentfield` plus the `af` alias). Copes with a running + * binary on Windows by renaming it aside first — renaming a running exe is + * allowed, overwriting it is not. + */ +export async function installBundledCli(bundledPath: string): Promise { + const binDir = managedBinDir() + try { + await fs.mkdir(binDir, { recursive: true }) + for (const base of ['agentfield', 'af']) { + const target = join(binDir, exeName(base)) + const staged = `${target}.new` + await fs.copyFile(bundledPath, staged) + if (process.platform !== 'win32') await fs.chmod(staged, 0o755) + try { + await fs.rename(target, `${target}.old`) + } catch { + // target absent — first install + } + await fs.rename(staged, target) + await fs.rm(`${target}.old`, { force: true }).catch(() => {}) + } + } catch (err) { + return { ok: false, message: `could not install the AgentField CLI: ${String(err)}` } + } + + if (process.platform === 'win32') registerWindowsUserPath(binDir) + return { ok: true, message: `AgentField CLI installed to ${binDir}` } +} + +/** + * Best-effort: put ~/.agentfield/bin on the user PATH so `af` also works in + * terminals (and for coding agents running shell commands). User-scope only, + * idempotent, and never blocks the caller. macOS/Linux PATH setup remains the + * curl installer's job — editing shell profiles from a GUI app is too rude. + */ +function registerWindowsUserPath(binDir: string): void { + const script = + `$p = [Environment]::GetEnvironmentVariable('Path', 'User'); ` + + `if (($p -split ';') -notcontains '${binDir}') { ` + + `[Environment]::SetEnvironmentVariable('Path', "$p;${binDir}", 'User') }` + spawn('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], { + windowsHide: true, + stdio: 'ignore' + }).on('error', () => {}) +} diff --git a/desktop/src/main/index.ts b/desktop/src/main/index.ts new file mode 100644 index 000000000..6abd7e78f --- /dev/null +++ b/desktop/src/main/index.ts @@ -0,0 +1,340 @@ +import { join, resolve } from 'node:path' +import { BrowserWindow, Menu, app, ipcMain, shell } from 'electron' +import { CATALOG } from '../shared/catalog' +import { DEEP_LINK_SCHEME, type View, deepLinkFromArgv, parseDeepLink } from '../shared/deeplink' +import type { DesktopSettings } from '../shared/types' +import { spawn } from 'node:child_process' +import { getSnapshot } from './agentfield' +import { type AgentAction, runAgentAction } from './agents' +import { runAutostart } from './autostart' +import { getCliCommand, initializeCli, installBundledCli, refreshCliStatus } from './cli' +import { installAgent } from './installer' +import { getEnvReports, revokeAgentSecret, setAgentSecret } from './secrets' +import { loadSettings, mergeSettings, saveSettings } from './settings' +import { setupTray } from './tray' +import appIcon from '../../resources/icon.png?asset' + +const isMac = process.platform === 'darwin' + +let mainWindow: BrowserWindow | null = null +/** Deep-link view waiting for a renderer that can show it. */ +let pendingView: View | null = null +/** + * True once the renderer subscribed to navigation (it announces itself via + * agentfield:renderer-ready on mount). A push before that would be dropped — + * did-finish-load fires before React mounts, so readiness is the renderer's + * call, not the page loader's. + */ +let rendererReady = false +/** True once the user chose Quit — lets close-to-tray tell hide from exit. */ +let quitting = false +/** True when a tray exists (Windows/Linux) — enables close-to-tray. */ +let trayActive = false + +// Mac-first chrome: no default File/Edit/View menu bar. On macOS an app menu +// must still exist (it owns Cmd+Q/Cmd+W/Cmd+C…), so build the minimal one; +// on Windows/Linux remove the bar entirely. +function installAppMenu(): void { + if (!isMac) { + Menu.setApplicationMenu(null) + return + } + Menu.setApplicationMenu( + Menu.buildFromTemplate([ + { + label: app.name, + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideOthers' }, + { type: 'separator' }, + { role: 'quit' } + ] + }, + // Keeps standard clipboard shortcuts working in text fields. + { role: 'editMenu' }, + { role: 'windowMenu' } + ]) + ) +} + +function createWindow(): void { + const win = new BrowserWindow({ + width: 980, + height: 700, + minWidth: 720, + minHeight: 480, + title: 'AgentField', + backgroundColor: '#00000000', + // Windows/Linux window + taskbar icon; macOS uses the bundle's icns. + icon: isMac ? undefined : appIcon, + // Seamless titlebar: traffic lights float over the content on macOS, + // native window controls overlay on Windows. The renderer reserves a + // draggable strip at the top (see styles.css .titlebar). + titleBarStyle: isMac ? 'hiddenInset' : 'hidden', + trafficLightPosition: isMac ? { x: 18, y: 18 } : undefined, + titleBarOverlay: isMac + ? undefined + : { color: '#00000000', symbolColor: '#8b95a3', height: 48 }, + vibrancy: isMac ? 'sidebar' : undefined, + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + contextIsolation: true, + nodeIntegration: false, + sandbox: true + } + }) + mainWindow = win + + // External links (e.g. docs) open in the default browser, never in-app. + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith('https://')) void shell.openExternal(url) + return { action: 'deny' } + }) + + // With a tray, closing the window hides it (the app keeps watching from + // the tray, Docker-Desktop style); Quit lives in the tray menu. + win.on('close', (event) => { + if (trayActive && !quitting) { + event.preventDefault() + win.hide() + } + }) + win.on('closed', () => { + if (mainWindow === win) { + mainWindow = null + rendererReady = false + } + }) + // A reload restarts the renderer; it re-announces readiness on mount. + win.webContents.on('did-start-loading', () => { + rendererReady = false + }) + + // electron-vite convention: dev server URL in dev, built file in production. + const devUrl = process.env['ELECTRON_RENDERER_URL'] + if (devUrl) { + void win.loadURL(devUrl) + } else { + void win.loadFile(join(__dirname, '../renderer/index.html')) + } +} + +function showMainWindow(): void { + if (!mainWindow || mainWindow.isDestroyed()) { + createWindow() + return + } + if (mainWindow.isMinimized()) mainWindow.restore() + mainWindow.show() + mainWindow.focus() +} + +function flushPendingView(): void { + // Not ready yet -> keep pendingView; the renderer collects it when it + // announces readiness (agentfield:renderer-ready returns-and-clears it). + if (!mainWindow || !pendingView || !rendererReady) return + mainWindow.webContents.send('agentfield:navigate', pendingView) + pendingView = null +} + +/** Bring the app forward and, when a deep link named a view, switch to it. */ +function navigate(view: View | null): void { + if (view) pendingView = view + showMainWindow() + flushPendingView() +} + +// Register the agentfield:// scheme (see shared/deeplink.ts). Packaged apps +// register their own executable; in dev the handler must point Electron at +// this app's entry explicitly. +function registerDeepLinks(): void { + if (process.defaultApp) { + if (process.argv.length >= 2) { + app.setAsDefaultProtocolClient(DEEP_LINK_SCHEME, process.execPath, [ + resolve(process.argv[1]) + ]) + } + } else { + app.setAsDefaultProtocolClient(DEEP_LINK_SCHEME) + } +} + +let installInFlight = false +let settings: DesktopSettings + +function settingsFile(): string { + return join(app.getPath('userData'), 'settings.json') +} + +// The af CLI shipped inside the app package (see build.extraResources). In +// dev, scripts/bundle-cli.mjs drops it into desktop/vendor/ instead. +function bundledCliPath(): string { + const name = process.platform === 'win32' ? 'af.exe' : 'af' + return app.isPackaged + ? join(process.resourcesPath, 'bin', name) + : join(app.getAppPath(), 'vendor', name) +} + +// Keep the AgentField skills present in detected coding agents (Claude Code, +// Codex, Gemini, …): the builder skill (agentfield) and the consumer skill +// (agentfield-use — how to discover and call installed agents). One install +// per skill, sequential so concurrent runs never race on skillkit's state +// file. Idempotent — skillkit tracks versions in ~/.agentfield/skills/ +// .state.json — and pure best-effort: an older CLI without agentfield-use in +// its catalog fails that one invocation and nothing else. +function syncSkills(names = ['agentfield', 'agentfield-use']): void { + const [head, ...rest] = names + if (!head) return + spawn(getCliCommand(), ['skill', 'install', head, '--non-interactive'], { + windowsHide: true, + stdio: 'ignore' + }) + .on('error', () => {}) + .on('close', () => syncSkills(rest)) +} + +// Register (or clear) the OS login item. Dev builds skip it — registering +// electron.exe as a login item would be wrong and confusing. +function applyLoginItem(next: DesktopSettings): void { + if (!app.isPackaged) return + app.setLoginItemSettings({ + openAtLogin: next.openAtLogin, + // Started at login the app stays out of the way: tray only, no window. + args: ['--hidden'] + }) +} + +function main(): void { + registerDeepLinks() + + // Windows/Linux: a relaunch (including one carrying an agentfield:// URL in + // argv) lands here in the first instance instead of opening a second app. + app.on('second-instance', (_event, argv) => { + navigate(deepLinkFromArgv(argv)) + }) + // macOS delivers deep links as open-url events. + app.on('open-url', (event, url) => { + event.preventDefault() + navigate(parseDeepLink(url)) + }) + app.on('before-quit', () => { + quitting = true + }) + + app.whenReady().then(async () => { + installAppMenu() + settings = await loadSettings(settingsFile()) + applyLoginItem(settings) + + // Resolve which af to drive (managed → PATH → bundled); on a machine + // with no AgentField at all this provisions the bundled CLI, so a + // desktop-app-only install still gets a working `af`. + await initializeCli(bundledCliPath()) + if (settings.installSkills) syncSkills() + + ipcMain.handle('agentfield:snapshot', () => getSnapshot()) + ipcMain.handle('agentfield:catalog', () => CATALOG) + ipcMain.handle('agentfield:install', async (event, name: unknown) => { + if (typeof name !== 'string') { + return { ok: false, message: 'invalid install request' } + } + if (installInFlight) { + return { ok: false, message: 'an install is already in progress' } + } + installInFlight = true + try { + return await installAgent(name, (line) => { + if (!event.sender.isDestroyed()) { + event.sender.send('agentfield:install-progress', line) + } + }) + } finally { + installInFlight = false + } + }) + ipcMain.handle('agentfield:agent-action', (_event, action: unknown, name: unknown) => { + if ( + typeof name !== 'string' || + (action !== 'start' && action !== 'stop' && action !== 'restart') + ) { + return { ok: false, message: 'invalid agent action' } + } + return runAgentAction(action as AgentAction, name) + }) + ipcMain.handle('agentfield:env-reports', () => getEnvReports()) + ipcMain.handle( + 'agentfield:secret-set', + (_event, agent: unknown, key: unknown, value: unknown) => { + if (typeof agent !== 'string' || typeof key !== 'string' || typeof value !== 'string') { + return { ok: false, message: 'invalid secret request' } + } + return setAgentSecret(agent, key, value) + } + ) + ipcMain.handle('agentfield:secret-revoke', (_event, agent: unknown, key: unknown) => { + if (typeof agent !== 'string' || typeof key !== 'string') { + return { ok: false, message: 'invalid secret request' } + } + return revokeAgentSecret(agent, key) + }) + // The renderer calls this once its navigation listener is live; the + // return value is the deep-link view (if any) that arrived before then. + ipcMain.handle('agentfield:renderer-ready', () => { + rendererReady = true + const view = pendingView + pendingView = null + return view + }) + ipcMain.handle('agentfield:cli-status', () => refreshCliStatus(bundledCliPath())) + ipcMain.handle('agentfield:cli-update', async () => { + const result = await installBundledCli(bundledCliPath()) + if (!result.ok) console.error(result.message) + return refreshCliStatus(bundledCliPath()) + }) + ipcMain.handle('agentfield:settings-get', () => settings) + ipcMain.handle('agentfield:settings-set', async (_event, patch: unknown) => { + settings = mergeSettings(settings, patch) + applyLoginItem(settings) + await saveSettings(settingsFile(), settings) + return settings + }) + + // macOS has its own menu-bar companion (af-tray) — no in-app tray there. + if (!isMac) { + trayActive = setupTray({ showWindow: showMainWindow, quit: () => app.quit() }) + } + + // A cold start via deep link (Windows) carries the URL in this argv. + const initial = deepLinkFromArgv(process.argv) + if (initial) pendingView = initial + + // Login-item launches pass --hidden: stay in the tray, no window. Without + // a tray to live in, fall back to showing the window as usual. + if (!process.argv.includes('--hidden') || !trayActive) { + createWindow() + } + + // Bring the control plane and the selected agents up in the background. + runAutostart(settings, (message) => console.log(message)).catch((err) => + console.error('autostart failed:', err) + ) + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) + }) + + app.on('window-all-closed', () => { + // macOS convention keeps apps alive without windows; with a tray the app + // stays resident too. Only tray-less Windows/Linux quits on last close. + if (!isMac && !trayActive) app.quit() + }) +} + +if (app.requestSingleInstanceLock()) { + main() +} else { + app.quit() +} diff --git a/desktop/src/main/installer.ts b/desktop/src/main/installer.ts new file mode 100644 index 000000000..413c8ac8c --- /dev/null +++ b/desktop/src/main/installer.ts @@ -0,0 +1,91 @@ +// Install seam: runs `af install ` for vetted catalog entries. +// The af CLI is the single contract shared by agents, this app, and +// developers — the app never reimplements install logic, it shells out. +// Deliberately does NOT import from 'electron' so it stays unit-testable. + +import { spawn } from 'node:child_process' +import { catalogEntry } from '../shared/catalog' +import type { InstallResult } from '../shared/types' +import { getCliCommand } from './cli' + +// CSI sequences (colors, cursor movement, erase-line spinner frames) and OSC +// sequences (terminal titles), per ECMA-48. Written with \u escapes so no +// invisible control characters live in this source file. +const ANSI_PATTERN = new RegExp( + '\\u001b\\[[0-9;?]*[A-Za-z]|\\u001b\\][^\\u0007\\u001b]*(?:\\u0007|\\u001b\\\\)?', + 'g' +) + +/** + * Normalize a chunk of `af install` output into displayable lines: strip + * ANSI color/spinner escapes, split on newlines and carriage returns + * (spinner frames), drop empties. + */ +export function sanitizeInstallOutput(chunk: string): string[] { + return chunk + .replace(ANSI_PATTERN, '') + .split(/[\r\n]+/) + .map((line) => line.trim()) + .filter((line) => line.length > 0) +} + +/** + * Build the argv for installing a catalog entry. Returns null for names not + * in the curated catalog — the renderer only ever sends names, and anything + * unknown is refused rather than passed to a shell. + */ +export function installCommand(name: string): { command: string; args: string[] } | null { + const entry = catalogEntry(name) + if (!entry) return null + // spawn() without a shell; the command is whatever CLI resolution picked + // (managed copy, PATH `af`, or the app's bundled binary — see main/cli.ts). + return { command: getCliCommand(), args: ['install', entry.source] } +} + +/** + * Run `af install` for the named catalog entry, forwarding sanitized output + * lines to onLine as they arrive. Resolves (never rejects) with the outcome. + */ +export function installAgent( + name: string, + onLine: (line: string) => void +): Promise { + const cmd = installCommand(name) + if (!cmd) { + return Promise.resolve({ ok: false, message: `"${name}" is not in the install catalog` }) + } + + return new Promise((resolve) => { + let lastLine = '' + const forward = (chunk: Buffer) => { + for (const line of sanitizeInstallOutput(chunk.toString('utf8'))) { + // Spinner frames repeat the same text many times a second; only + // forward changes so the IPC channel stays quiet. + if (line !== lastLine) { + lastLine = line + onLine(line) + } + } + } + + const child = spawn(cmd.command, cmd.args, { windowsHide: true }) + child.stdout.on('data', forward) + child.stderr.on('data', forward) + child.on('error', (err: NodeJS.ErrnoException) => { + resolve({ + ok: false, + message: + err.code === 'ENOENT' + ? 'The AgentField CLI (af) was not found on PATH. Install it first: https://agentfield.ai/docs' + : `Failed to run af install: ${err.message}` + }) + }) + child.on('close', (code) => { + resolve( + code === 0 + ? { ok: true, message: `${name} installed` } + : { ok: false, message: lastLine || `af install exited with code ${code}` } + ) + }) + }) +} diff --git a/desktop/src/main/secrets.test.ts b/desktop/src/main/secrets.test.ts new file mode 100644 index 000000000..7eb644dd0 --- /dev/null +++ b/desktop/src/main/secrets.test.ts @@ -0,0 +1,189 @@ +import yaml from 'js-yaml' +import { describe, expect, it } from 'vitest' +import { + buildEnvReport, + findSpecVar, + parseSecretsTable, + parseUserEnvironment, + specIsEmpty +} from './secrets' + +// Abridged from SWE-AF's real agentfield-package.yaml — the exact shapes +// ParsePackageMetadata reads (control-plane/internal/packages/installer.go). +const SWE_MANIFEST = ` +config_version: v1 +name: swe-planner +version: 0.1.0 +user_environment: + require_one_of: + - id: llm_provider + description: an LLM provider key + options: + - name: ANTHROPIC_API_KEY + description: Anthropic API key (Claude) + type: secret + scope: global + - name: OPENROUTER_API_KEY + description: OpenRouter API key + type: secret + scope: global + required: + - name: GH_TOKEN + description: GitHub token (repo scope) + type: secret + scope: global + optional: + - name: SWE_DEFAULT_MODEL + description: Override the model id + - name: AGENTFIELD_SERVER + description: Control-plane URL + default: http://localhost:8080 +` + +// Captured from a real piped `af secrets ls` (lipgloss rounded borders, +// no ANSI when stdout is not a TTY). +const LS_OUTPUT = `Stored secrets (2) +╭───────────────────┬─────────────┬──────────╮ +│ KEY │ SCOPE │ VALUE │ +├───────────────────┼─────────────┼──────────┤ +│ GH_TOKEN │ global │ •••••••• │ +│ ANTHROPIC_API_KEY │ swe-planner │ •••••••• │ +╰───────────────────┴─────────────┴──────────╯` + +const LS_EMPTY = `No secrets stored +╭──────────────────────╮ +│ Add one with: │ +│ af secrets set KEY │ +╰──────────────────────╯` + +function sweSpec() { + return parseUserEnvironment(yaml.load(SWE_MANIFEST)) +} + +describe('parseUserEnvironment', () => { + it('reads required, require_one_of, and optional sections', () => { + const spec = sweSpec() + expect(spec.required.map((v) => v.name)).toEqual(['GH_TOKEN']) + expect(spec.groups).toHaveLength(1) + expect(spec.groups[0].id).toBe('llm_provider') + expect(spec.groups[0].options.map((v) => v.name)).toEqual([ + 'ANTHROPIC_API_KEY', + 'OPENROUTER_API_KEY' + ]) + expect(spec.optional.map((v) => v.name)).toEqual(['SWE_DEFAULT_MODEL', 'AGENTFIELD_SERVER']) + }) + + it('maps type/scope/default per var', () => { + const spec = sweSpec() + const gh = findSpecVar(spec, 'GH_TOKEN') + expect(gh).toMatchObject({ secret: true, scope: 'global', default: '' }) + const server = findSpecVar(spec, 'AGENTFIELD_SERVER') + expect(server).toMatchObject({ secret: false, default: 'http://localhost:8080' }) + }) + + it('yields an empty spec for manifests without user_environment', () => { + expect(specIsEmpty(parseUserEnvironment(yaml.load('name: bare-agent')))).toBe(true) + expect(specIsEmpty(parseUserEnvironment(null))).toBe(true) + expect(specIsEmpty(parseUserEnvironment({ user_environment: 'garbage' }))).toBe(true) + }) + + it('drops malformed vars and empty groups instead of throwing', () => { + const spec = parseUserEnvironment({ + user_environment: { + required: [{ name: 'GOOD' }, { description: 'no name' }, 42], + require_one_of: [{ id: 'empty', options: [] }, { id: 'ok', options: [{ name: 'A' }] }] + } + }) + expect(spec.required.map((v) => v.name)).toEqual(['GOOD']) + expect(spec.groups.map((g) => g.id)).toEqual(['ok']) + }) +}) + +describe('parseSecretsTable', () => { + it('extracts key/scope rows from the bordered table', () => { + expect(parseSecretsTable(LS_OUTPUT)).toEqual([ + { key: 'GH_TOKEN', scope: 'global' }, + { key: 'ANTHROPIC_API_KEY', scope: 'swe-planner' } + ]) + }) + + it('reads nothing from the empty-store panel', () => { + expect(parseSecretsTable(LS_EMPTY)).toEqual([]) + }) + + it('ignores plain text output', () => { + expect(parseSecretsTable('some error\nno table here')).toEqual([]) + }) +}) + +describe('buildEnvReport', () => { + it('is unsatisfied when required and group vars are all missing', () => { + const report = buildEnvReport('swe-planner', sweSpec(), [], {}) + expect(report.satisfied).toBe(false) + const byName = Object.fromEntries(report.vars.map((v) => [v.name, v])) + expect(byName.GH_TOKEN.status).toBe('missing') + expect(byName.ANTHROPIC_API_KEY.status).toBe('missing') + // Optional var with a manifest default resolves without any input. + expect(byName.AGENTFIELD_SERVER.status).toBe('default') + }) + + it('is satisfied once the required var and one group option resolve', () => { + const refs = parseSecretsTable(LS_OUTPUT) + const report = buildEnvReport('swe-planner', sweSpec(), refs, {}) + expect(report.satisfied).toBe(true) + const byName = Object.fromEntries(report.vars.map((v) => [v.name, v])) + expect(byName.GH_TOKEN).toMatchObject({ status: 'stored', storedScopes: ['global'] }) + expect(byName.ANTHROPIC_API_KEY).toMatchObject({ + status: 'stored', + storedScopes: ['swe-planner'] + }) + expect(byName.OPENROUTER_API_KEY.status).toBe('missing') + }) + + it('resolves from the process environment first', () => { + const report = buildEnvReport('swe-planner', sweSpec(), [], { + GH_TOKEN: 'ghp_x', + OPENROUTER_API_KEY: 'sk-or-x' + }) + expect(report.satisfied).toBe(true) + const byName = Object.fromEntries(report.vars.map((v) => [v.name, v])) + expect(byName.GH_TOKEN.status).toBe('env') + expect(byName.OPENROUTER_API_KEY.status).toBe('env') + }) + + it('ignores another node’s scoped secrets', () => { + const refs = [{ key: 'GH_TOKEN', scope: 'other-agent' }] + const report = buildEnvReport('swe-planner', sweSpec(), refs, {}) + const gh = report.vars.find((v) => v.name === 'GH_TOKEN') + expect(gh?.status).toBe('missing') + expect(gh?.storedScopes).toEqual([]) + }) + + it('requires every group to be satisfied independently', () => { + const spec = parseUserEnvironment({ + user_environment: { + require_one_of: [ + { id: 'a', options: [{ name: 'A1' }] }, + { id: 'b', options: [{ name: 'B1' }] } + ] + } + }) + const partial = buildEnvReport('x', spec, [{ key: 'A1', scope: 'global' }], {}) + expect(partial.satisfied).toBe(false) + const both = buildEnvReport( + 'x', + spec, + [ + { key: 'A1', scope: 'global' }, + { key: 'B1', scope: 'global' } + ], + {} + ) + expect(both.satisfied).toBe(true) + }) + + it('an empty env value does not count as resolved', () => { + const report = buildEnvReport('swe-planner', sweSpec(), [], { GH_TOKEN: '' }) + expect(report.vars.find((v) => v.name === 'GH_TOKEN')?.status).toBe('missing') + }) +}) diff --git a/desktop/src/main/secrets.ts b/desktop/src/main/secrets.ts new file mode 100644 index 000000000..474732350 --- /dev/null +++ b/desktop/src/main/secrets.ts @@ -0,0 +1,386 @@ +// Agent secrets seam: reads each installed node's user_environment spec from +// its agentfield-package.yaml and drives the af CLI's encrypted secret store +// (`af secrets set / ls / rm`) — the exact store `af run` decrypts into the +// agent's process environment. The app never touches the .enc files itself, +// and secret VALUES never cross the IPC boundary: the renderer only ever +// sees per-variable status flags, and values travel renderer → main → af's +// stdin (never argv, which other processes could observe). +// +// Resolution mirrors the CLI's EnvResolver (control-plane/internal/packages/ +// env_resolver.go): process env → secret store (node scope, then global) → +// manifest default → missing. `af run` from this app inherits our process +// env, so checking process.env here is faithful to what the spawned agent +// will see. +// +// No electron imports — unit-testable under plain vitest. + +import { spawn } from 'node:child_process' +import { promises as fs } from 'node:fs' +import path from 'node:path' +import yaml from 'js-yaml' +import type { + AgentActionResult, + AgentEnvReport, + AgentEnvVar, + EnvVarStatus +} from '../shared/types' +import { getAgentFieldHome, readInstalledAgents } from './agentfield' +import { getCliCommand } from './cli' +import { sanitizeInstallOutput } from './installer' + +const SECRETS_TIMEOUT_MS = 15_000 + +/** The store's shared scope name (control-plane/internal/packages/secrets.go). */ +const GLOBAL_SCOPE = 'global' + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function str(value: unknown): string { + return typeof value === 'string' ? value : '' +} + +/** One variable from a manifest's user_environment section. */ +export interface EnvSpecVar { + name: string + description: string + /** type: secret — hidden input, masked display. */ + secret: boolean + /** Store scope `af run` reads and `af secrets set` writes: global unless scope: node. */ + scope: 'global' | 'node' + /** Manifest default — counts as resolved, matching the EnvResolver. */ + default: string + /** Optional Go/RE2 regex the value must match (validated on prompt in the CLI). */ + validation: string +} + +/** Parsed user_environment block of an agentfield-package.yaml. */ +export interface EnvSpec { + required: EnvSpecVar[] + /** require_one_of groups: any one resolving option satisfies the group. */ + groups: { id: string; description: string; options: EnvSpecVar[] }[] + optional: EnvSpecVar[] +} + +function toSpecVar(raw: unknown): EnvSpecVar | null { + if (!isRecord(raw) || typeof raw.name !== 'string' || raw.name === '') return null + return { + name: raw.name, + description: str(raw.description), + secret: raw.type === 'secret', + scope: raw.scope === 'node' ? 'node' : 'global', + default: str(raw.default), + validation: str(raw.validation) + } +} + +function toSpecVars(raw: unknown): EnvSpecVar[] { + if (!Array.isArray(raw)) return [] + return raw.map(toSpecVar).filter((v): v is EnvSpecVar => v !== null) +} + +/** + * Extract the user_environment section from a yaml-loaded manifest document. + * Field shapes follow UserEnvironmentConfig in control-plane/internal/packages/ + * installer.go. Absent or malformed sections yield an empty spec — an agent + * without declared variables simply has nothing to configure. + */ +export function parseUserEnvironment(doc: unknown): EnvSpec { + const env = isRecord(doc) && isRecord(doc.user_environment) ? doc.user_environment : {} + const groups = Array.isArray(env.require_one_of) + ? env.require_one_of + .filter(isRecord) + .map((group) => ({ + id: str(group.id), + description: str(group.description), + options: toSpecVars(group.options) + })) + .filter((group) => group.options.length > 0) + : [] + return { + required: toSpecVars(env.required), + groups, + optional: toSpecVars(env.optional) + } +} + +export function specIsEmpty(spec: EnvSpec): boolean { + return spec.required.length === 0 && spec.groups.length === 0 && spec.optional.length === 0 +} + +/** Find a declared variable by name anywhere in the spec. */ +export function findSpecVar(spec: EnvSpec, name: string): EnvSpecVar | null { + for (const v of spec.required) if (v.name === name) return v + for (const group of spec.groups) for (const v of group.options) if (v.name === name) return v + for (const v of spec.optional) if (v.name === name) return v + return null +} + +/** One row of `af secrets ls`: a stored key and its scope (global or a node name). */ +export interface SecretRef { + key: string + scope: string +} + +/** + * Parse the `af secrets ls` table (internal/ui.Table — lipgloss rounded + * borders, no ANSI when piped). Data rows look like: + * │ OPENAI_API_KEY │ global │ •••••••• │ + * Everything that isn't a ≥2-cell row (borders, the header row, the empty + * "No secrets stored" panel) is ignored. + */ +export function parseSecretsTable(output: string): SecretRef[] { + const refs: SecretRef[] = [] + for (const line of output.split(/\r?\n/)) { + if (!line.includes('│')) continue + const cells = line + .split('│') + .map((cell) => cell.trim()) + .filter((cell) => cell.length > 0) + if (cells.length < 2) continue + if (cells[0] === 'KEY') continue + refs.push({ key: cells[0], scope: cells[1] }) + } + return refs +} + +/** Store scopes relevant to this agent that currently hold the key. */ +function storedScopesFor(refs: SecretRef[], agent: string, key: string): string[] { + return refs + .filter((ref) => ref.key === key && (ref.scope === GLOBAL_SCOPE || ref.scope === agent)) + .map((ref) => ref.scope) +} + +function statusFor( + v: EnvSpecVar, + storedScopes: string[], + env: Record +): EnvVarStatus { + const fromEnv = env[v.name] + if (typeof fromEnv === 'string' && fromEnv !== '') return 'env' + if (storedScopes.length > 0) return 'stored' + if (v.default !== '') return 'default' + return 'missing' +} + +function toReportVar( + v: EnvSpecVar, + required: boolean, + group: { id: string; description: string } | null, + agent: string, + refs: SecretRef[], + env: Record +): AgentEnvVar { + const storedScopes = storedScopesFor(refs, agent, v.name) + return { + name: v.name, + description: v.description, + secret: v.secret, + scope: v.scope, + required, + group: group?.id || undefined, + groupDescription: group?.description || undefined, + status: statusFor(v, storedScopes, env), + storedScopes + } +} + +/** + * Assemble the renderer-facing report for one agent. `satisfied` mirrors what + * the CLI's EnvResolver will conclude headlessly: every required variable and + * every require_one_of group resolves from env / store / default — so `af run` + * will not fail with "missing required environment variables". + */ +export function buildEnvReport( + agent: string, + spec: EnvSpec, + refs: SecretRef[], + env: Record = process.env +): AgentEnvReport { + const vars: AgentEnvVar[] = [] + for (const v of spec.required) vars.push(toReportVar(v, true, null, agent, refs, env)) + for (const group of spec.groups) { + for (const v of group.options) vars.push(toReportVar(v, true, group, agent, refs, env)) + } + for (const v of spec.optional) vars.push(toReportVar(v, false, null, agent, refs, env)) + + const requiredOk = vars.every((v) => v.group || !v.required || v.status !== 'missing') + const groupsOk = spec.groups.every((group) => + vars.some((v) => v.group === group.id && v.status !== 'missing') + ) + return { agent, vars, satisfied: requiredOk && groupsOk } +} + +/** Run one `af secrets …` verb, capturing all sanitized output. */ +function runSecretsCli( + args: string[], + input?: string +): Promise<{ ok: boolean; output: string }> { + return new Promise((resolve) => { + const lines: string[] = [] + let settled = false + const done = (ok: boolean) => { + if (!settled) { + settled = true + resolve({ ok, output: lines.join('\n') }) + } + } + + const child = spawn(getCliCommand(), ['secrets', ...args], { windowsHide: true }) + const timer = setTimeout(() => { + child.kill() + lines.push('af secrets timed out') + done(false) + }, SECRETS_TIMEOUT_MS) + + const collect = (chunk: Buffer) => { + lines.push(...sanitizeInstallOutput(chunk.toString('utf8'))) + } + child.stdout.on('data', collect) + child.stderr.on('data', collect) + child.on('error', (err: NodeJS.ErrnoException) => { + clearTimeout(timer) + lines.push( + err.code === 'ENOENT' ? 'The AgentField CLI (af) was not found' : String(err.message) + ) + done(false) + }) + child.on('close', (code) => { + clearTimeout(timer) + done(code === 0) + }) + if (input !== undefined) { + // `af secrets set KEY` reads the value from stdin when it is not a TTY + // (readHiddenValue in internal/cli/secrets.go) — the value never + // appears in a command line. + child.stdin.write(`${input}\n`) + } + child.stdin.end() + }) +} + +/** Read and parse /agentfield-package.yaml; null when unreadable. */ +export async function readEnvSpec(packageDir: string): Promise { + try { + const text = await fs.readFile(path.join(packageDir, 'agentfield-package.yaml'), 'utf8') + return parseUserEnvironment(yaml.load(text)) + } catch { + return null + } +} + +/** + * Build env reports for every installed agent that declares environment + * variables. One `af secrets ls` serves all agents. A store read failure + * degrades to "store looks empty" with the error attached — statuses from + * process env and manifest defaults are still real. + */ +export async function getEnvReports( + homeDir: string = getAgentFieldHome() +): Promise { + const registry = await readInstalledAgents(homeDir) + if (registry.agents.length === 0) return [] + + const specs: { agent: string; spec: EnvSpec }[] = [] + for (const agent of registry.agents) { + const dir = agent.path ?? path.join(homeDir, 'packages', agent.name) + const spec = await readEnvSpec(dir) + if (spec && !specIsEmpty(spec)) specs.push({ agent: agent.name, spec }) + } + if (specs.length === 0) return [] + + const ls = await runSecretsCli(['ls']) + const refs = ls.ok ? parseSecretsTable(ls.output) : [] + return specs.map(({ agent, spec }) => { + const report = buildEnvReport(agent, spec, refs) + if (!ls.ok) report.error = `could not read the secret store: ${ls.output}` + return report + }) +} + +/** Locate an agent's declared variable; refuse anything the manifest doesn't name. */ +async function resolveDeclaredVar( + agent: string, + key: string, + homeDir: string +): Promise<{ spec: EnvSpecVar } | { error: string }> { + const registry = await readInstalledAgents(homeDir) + const entry = registry.agents.find((a) => a.name === agent) + if (!entry) return { error: `"${agent}" is not an installed agent` } + const spec = await readEnvSpec(entry.path ?? path.join(homeDir, 'packages', agent)) + if (!spec) return { error: `could not read ${agent}'s manifest` } + const specVar = findSpecVar(spec, key) + if (!specVar) return { error: `${agent} does not declare ${key}` } + return { spec: specVar } +} + +/** + * Store a value for one of an agent's declared variables, in the scope the + * manifest names (global by default — shared across nodes, exactly like the + * CLI's own prompt-and-store). The renderer only ever supplies names the + * manifest declares; anything else is refused. + */ +export async function setAgentSecret( + agent: string, + key: string, + value: string, + homeDir: string = getAgentFieldHome() +): Promise { + const trimmed = value.trim() + if (trimmed === '') return { ok: false, message: 'value must not be empty' } + + const resolved = await resolveDeclaredVar(agent, key, homeDir) + if ('error' in resolved) return { ok: false, message: resolved.error } + + if (resolved.spec.validation) { + try { + if (!new RegExp(resolved.spec.validation).test(trimmed)) { + return { + ok: false, + message: `value does not match the required format (${resolved.spec.validation})` + } + } + } catch { + // Go RE2 pattern that JS cannot compile — let the CLI-side validation + // (which runs on prompt) be the judge rather than rejecting here. + } + } + + const args = ['set', key] + if (resolved.spec.scope === 'node') args.push('--node', agent) + const result = await runSecretsCli(args, trimmed) + return result.ok + ? { ok: true, message: `${key} stored` } + : { ok: false, message: result.output || `failed to store ${key}` } +} + +/** + * Remove a stored value from every scope relevant to this agent that holds + * it (its node scope and/or global). Revoking a global key affects all + * agents sharing it — that is the store's sharing model, and the UI labels + * the scope so the choice is informed. + */ +export async function revokeAgentSecret( + agent: string, + key: string, + homeDir: string = getAgentFieldHome() +): Promise { + const resolved = await resolveDeclaredVar(agent, key, homeDir) + if ('error' in resolved) return { ok: false, message: resolved.error } + + const ls = await runSecretsCli(['ls']) + if (!ls.ok) return { ok: false, message: `could not read the secret store: ${ls.output}` } + const scopes = storedScopesFor(parseSecretsTable(ls.output), agent, key) + if (scopes.length === 0) return { ok: true, message: `${key} is not stored` } + + for (const scope of scopes) { + const args = ['rm', key] + if (scope !== GLOBAL_SCOPE) args.push('--node', scope) + const result = await runSecretsCli(args) + if (!result.ok) { + return { ok: false, message: result.output || `failed to remove ${key}` } + } + } + return { ok: true, message: `${key} revoked` } +} diff --git a/desktop/src/main/settings.test.ts b/desktop/src/main/settings.test.ts new file mode 100644 index 000000000..daf4161e7 --- /dev/null +++ b/desktop/src/main/settings.test.ts @@ -0,0 +1,74 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterAll, describe, expect, it } from 'vitest' +import { DEFAULT_SETTINGS, loadSettings, mergeSettings, normalizeSettings, saveSettings } from './settings' + +const dir = mkdtempSync(join(tmpdir(), 'af-desktop-settings-')) +afterAll(() => rmSync(dir, { recursive: true, force: true })) + +describe('normalizeSettings', () => { + it('accepts a valid shape as-is', () => { + const s = { + openAtLogin: true, + autostartControlPlane: false, + autostartAgents: ['a', 'b'], + installSkills: false + } + expect(normalizeSettings(s)).toEqual(s) + }) + + it('falls back to defaults for garbage', () => { + expect(normalizeSettings(null)).toEqual(DEFAULT_SETTINGS) + expect(normalizeSettings('nope')).toEqual(DEFAULT_SETTINGS) + expect(normalizeSettings({ openAtLogin: 'yes', autostartAgents: 42 })).toEqual( + DEFAULT_SETTINGS + ) + }) + + it('drops non-string agent names and dedupes', () => { + expect( + normalizeSettings({ autostartAgents: ['a', 7, 'a', null, 'b'] }).autostartAgents + ).toEqual(['a', 'b']) + }) +}) + +describe('mergeSettings', () => { + it('applies a partial patch over the base', () => { + const merged = mergeSettings(DEFAULT_SETTINGS, { openAtLogin: true }) + expect(merged.openAtLogin).toBe(true) + expect(merged.autostartControlPlane).toBe(DEFAULT_SETTINGS.autostartControlPlane) + }) + + it('sanitizes hostile patches (renderer input is untrusted)', () => { + const merged = mergeSettings(DEFAULT_SETTINGS, { + autostartAgents: ['ok', { evil: true }], + openAtLogin: 'true' + }) + expect(merged.autostartAgents).toEqual(['ok']) + expect(merged.openAtLogin).toBe(false) + }) +}) + +describe('load/save round trip', () => { + it('persists and reloads settings', async () => { + const file = join(dir, 'nested', 'settings.json') + const s = { + openAtLogin: true, + autostartControlPlane: true, + autostartAgents: ['swe-planner'], + installSkills: true + } + await saveSettings(file, s) + expect(await loadSettings(file)).toEqual(s) + }) + + it('missing or corrupt file yields defaults', async () => { + expect(await loadSettings(join(dir, 'nope.json'))).toEqual(DEFAULT_SETTINGS) + const bad = join(dir, 'bad.json') + await saveSettings(bad, DEFAULT_SETTINGS) + const fs = await import('node:fs') + fs.writeFileSync(bad, '{not json') + expect(await loadSettings(bad)).toEqual(DEFAULT_SETTINGS) + }) +}) diff --git a/desktop/src/main/settings.ts b/desktop/src/main/settings.ts new file mode 100644 index 000000000..ac2adf50a --- /dev/null +++ b/desktop/src/main/settings.ts @@ -0,0 +1,56 @@ +// Persisted app settings. Plain JSON in the app's user-data directory — +// no electron imports here so normalization and IO stay unit-testable; the +// login-item side effect lives in index.ts where `app` is available. + +import { promises as fs } from 'node:fs' +import { dirname } from 'node:path' +import type { DesktopSettings } from '../shared/types' + +export const DEFAULT_SETTINGS: DesktopSettings = { + openAtLogin: false, + autostartControlPlane: true, + autostartAgents: [], + installSkills: true +} + +/** + * Coerce whatever was on disk (old versions, hand edits, corruption) into a + * valid DesktopSettings. Unknown keys are dropped, wrong types fall back to + * defaults, agent names are deduped strings. + */ +export function normalizeSettings(raw: unknown): DesktopSettings { + const obj = typeof raw === 'object' && raw !== null ? (raw as Record) : {} + const agents = Array.isArray(obj.autostartAgents) + ? [...new Set(obj.autostartAgents.filter((n): n is string => typeof n === 'string'))] + : DEFAULT_SETTINGS.autostartAgents + return { + openAtLogin: + typeof obj.openAtLogin === 'boolean' ? obj.openAtLogin : DEFAULT_SETTINGS.openAtLogin, + autostartControlPlane: + typeof obj.autostartControlPlane === 'boolean' + ? obj.autostartControlPlane + : DEFAULT_SETTINGS.autostartControlPlane, + autostartAgents: agents, + installSkills: + typeof obj.installSkills === 'boolean' ? obj.installSkills : DEFAULT_SETTINGS.installSkills + } +} + +/** Merge a partial update (renderer-supplied, so also unvalidated) into base. */ +export function mergeSettings(base: DesktopSettings, patch: unknown): DesktopSettings { + const p = typeof patch === 'object' && patch !== null ? (patch as Record) : {} + return normalizeSettings({ ...base, ...p }) +} + +export async function loadSettings(file: string): Promise { + try { + return normalizeSettings(JSON.parse(await fs.readFile(file, 'utf8'))) + } catch { + return { ...DEFAULT_SETTINGS } + } +} + +export async function saveSettings(file: string, settings: DesktopSettings): Promise { + await fs.mkdir(dirname(file), { recursive: true }) + await fs.writeFile(file, JSON.stringify(settings, null, 2) + '\n', 'utf8') +} diff --git a/desktop/src/main/tray-model.test.ts b/desktop/src/main/tray-model.test.ts new file mode 100644 index 000000000..1bced5630 --- /dev/null +++ b/desktop/src/main/tray-model.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest' +import type { ControlPlaneStatus } from '../shared/types' +import { trayIconBase, trayState, trayStatusLabel, trayTooltip } from './tray-model' + +function cp(overrides: Partial): ControlPlaneStatus { + return { reachable: false, recognized: false, healthy: false, ...overrides } +} + +describe('trayState', () => { + it('healthy control plane is running', () => { + expect(trayState(cp({ reachable: true, recognized: true, healthy: true }))).toBe('running') + }) + + it('recognized but not healthy is unhealthy', () => { + expect(trayState(cp({ reachable: true, recognized: true }))).toBe('unhealthy') + }) + + it('reachable but unrecognized (another app on the port) is foreign', () => { + expect(trayState(cp({ reachable: true }))).toBe('foreign') + }) + + it('unreachable is stopped', () => { + expect(trayState(cp({}))).toBe('stopped') + }) +}) + +describe('tray labels', () => { + it('running names the host', () => { + expect(trayStatusLabel('running', 'localhost:8080')).toBe( + 'Control plane running · localhost:8080' + ) + }) + + it('foreign warns about the squatting app', () => { + expect(trayStatusLabel('foreign', 'localhost:8080')).toBe( + 'Port in use by another app (localhost:8080)' + ) + }) + + it('stopped and unhealthy are plain statements', () => { + expect(trayStatusLabel('stopped', 'localhost:8080')).toBe('Control plane not running') + expect(trayStatusLabel('unhealthy', 'localhost:8080')).toBe('Control plane unhealthy') + }) + + it('tooltip is the brand plus the status', () => { + expect(trayTooltip('running', 'localhost:8080')).toBe( + 'AgentField — Control plane running · localhost:8080' + ) + }) +}) + +describe('trayIconBase', () => { + it('only running earns the active (gold-dot) glyph', () => { + expect(trayIconBase('running', true)).toBe('tray-active-light') + expect(trayIconBase('unhealthy', true)).toBe('tray-inactive-light') + expect(trayIconBase('foreign', true)).toBe('tray-inactive-light') + expect(trayIconBase('stopped', true)).toBe('tray-inactive-light') + }) + + it('light glyphs for dark taskbars, dark glyphs for light taskbars', () => { + expect(trayIconBase('running', false)).toBe('tray-active-dark') + expect(trayIconBase('stopped', false)).toBe('tray-inactive-dark') + }) +}) diff --git a/desktop/src/main/tray-model.ts b/desktop/src/main/tray-model.ts new file mode 100644 index 000000000..0031dd5a2 --- /dev/null +++ b/desktop/src/main/tray-model.ts @@ -0,0 +1,47 @@ +// Pure presentation logic for the Windows/Linux tray icon. No Electron +// imports, so it is unit-tested directly (see tray-model.test.ts); the tray +// glue that consumes it lives in tray.ts. +// +// macOS gets no tray from the desktop app on purpose: the menu-bar companion +// there is `af-tray`, installed with AgentField itself (control-plane/cmd/af-tray). + +import type { ControlPlaneStatus } from '../shared/types' + +export type TrayState = 'running' | 'unhealthy' | 'foreign' | 'stopped' + +/** Collapse a health probe into the four states the tray can express. */ +export function trayState(cp: ControlPlaneStatus): TrayState { + if (cp.healthy) return 'running' + if (cp.reachable && cp.recognized) return 'unhealthy' + if (cp.reachable) return 'foreign' + return 'stopped' +} + +/** One-line status, used as the disabled menu row. `host` is e.g. "localhost:8080". */ +export function trayStatusLabel(state: TrayState, host: string): string { + switch (state) { + case 'running': + return `Control plane running · ${host}` + case 'unhealthy': + return 'Control plane unhealthy' + case 'foreign': + return `Port in use by another app (${host})` + case 'stopped': + return 'Control plane not running' + } +} + +export function trayTooltip(state: TrayState, host: string): string { + return `AgentField — ${trayStatusLabel(state, host)}` +} + +/** + * Which glyph file the tray wears (relative to resources/tray/, without the + * -.png suffix). The gold dot doubles as the status light: gold while + * the control plane is running, gray otherwise. `darkTaskbar` follows the + * OS system-UI theme — light glyphs for dark taskbars and vice versa. + */ +export function trayIconBase(state: TrayState, darkTaskbar: boolean): string { + const activity = state === 'running' ? 'active' : 'inactive' + return `tray-${activity}-${darkTaskbar ? 'light' : 'dark'}` +} diff --git a/desktop/src/main/tray.ts b/desktop/src/main/tray.ts new file mode 100644 index 000000000..1f70ac653 --- /dev/null +++ b/desktop/src/main/tray.ts @@ -0,0 +1,108 @@ +// Windows/Linux tray (taskbar status) icon. +// +// On macOS the menu-bar companion ships with AgentField itself (`af-tray`, +// installed by the curl installer), so the desktop app adds no tray there. +// On Windows there is no curl-installed tray — the desktop app carries it: +// a status glyph (the brand dot goes gold while the control plane runs), +// a small menu, and close-to-tray so the app keeps watching in the background. + +import { readFileSync } from 'node:fs' +import { Menu, Tray, nativeImage, nativeTheme, shell } from 'electron' +import { DEFAULT_BASE_URL, checkControlPlane } from './agentfield' +import { type TrayState, trayIconBase, trayState, trayStatusLabel, trayTooltip } from './tray-model' +import trayActiveLight16 from '../../resources/tray/tray-active-light-16.png?asset' +import trayActiveLight24 from '../../resources/tray/tray-active-light-24.png?asset' +import trayActiveLight32 from '../../resources/tray/tray-active-light-32.png?asset' +import trayActiveDark16 from '../../resources/tray/tray-active-dark-16.png?asset' +import trayActiveDark24 from '../../resources/tray/tray-active-dark-24.png?asset' +import trayActiveDark32 from '../../resources/tray/tray-active-dark-32.png?asset' +import trayInactiveLight16 from '../../resources/tray/tray-inactive-light-16.png?asset' +import trayInactiveLight24 from '../../resources/tray/tray-inactive-light-24.png?asset' +import trayInactiveLight32 from '../../resources/tray/tray-inactive-light-32.png?asset' +import trayInactiveDark16 from '../../resources/tray/tray-inactive-dark-16.png?asset' +import trayInactiveDark24 from '../../resources/tray/tray-inactive-dark-24.png?asset' +import trayInactiveDark32 from '../../resources/tray/tray-inactive-dark-32.png?asset' + +const POLL_MS = 5000 + +/** 1x / 1.5x / 2x raster per glyph (see scripts/make-icons.mjs). */ +const ICONS: Record = { + 'tray-active-light': [trayActiveLight16, trayActiveLight24, trayActiveLight32], + 'tray-active-dark': [trayActiveDark16, trayActiveDark24, trayActiveDark32], + 'tray-inactive-light': [trayInactiveLight16, trayInactiveLight24, trayInactiveLight32], + 'tray-inactive-dark': [trayInactiveDark16, trayInactiveDark24, trayInactiveDark32] +} + +function trayImage(base: string): Electron.NativeImage { + const [x1, x15, x2] = ICONS[base] + const image = nativeImage.createEmpty() + image.addRepresentation({ scaleFactor: 1, buffer: readFileSync(x1) }) + image.addRepresentation({ scaleFactor: 1.5, buffer: readFileSync(x15) }) + image.addRepresentation({ scaleFactor: 2, buffer: readFileSync(x2) }) + return image +} + +/** The taskbar follows the OS "system UI" theme, not the app theme. */ +function darkTaskbar(): boolean { + return nativeTheme.shouldUseDarkColors +} + +/** What the tray needs from the app shell, kept narrow for clarity. */ +export interface TrayHost { + showWindow(): void + quit(): void +} + +/** + * Create the tray and start polling the control plane. Returns false when the + * platform has no working status area (e.g. some Linux desktops) — the caller + * then keeps classic quit-on-close behavior instead of close-to-tray. + */ +export function setupTray(host: TrayHost): boolean { + const hostLabel = new URL(DEFAULT_BASE_URL).host + let state: TrayState = 'stopped' + + let tray: Tray + try { + tray = new Tray(trayImage(trayIconBase(state, darkTaskbar()))) + } catch (err) { + // e.g. a Linux desktop without a status area — or a packaging bug that + // lost the glyphs; degrade to quit-on-close but say why. + console.error('tray unavailable:', err) + return false + } + + const apply = (next: TrayState): void => { + state = next + tray.setImage(trayImage(trayIconBase(state, darkTaskbar()))) + tray.setToolTip(trayTooltip(state, hostLabel)) + tray.setContextMenu( + Menu.buildFromTemplate([ + { label: 'Open AgentField', click: () => host.showWindow() }, + { + label: 'Open web UI', + enabled: state === 'running', + click: () => void shell.openExternal(`${DEFAULT_BASE_URL}/ui/`) + }, + { type: 'separator' }, + { label: trayStatusLabel(state, hostLabel), enabled: false }, + { type: 'separator' }, + { label: 'Quit AgentField', click: () => host.quit() } + ]) + ) + } + apply(state) + + // Re-render only on actual change — replacing the icon/menu every poll + // would churn native tray APIs (and can dismiss an open menu on Windows). + const refresh = async (): Promise => { + const next = trayState(await checkControlPlane()) + if (next !== state) apply(next) + } + void refresh() + setInterval(() => void refresh(), POLL_MS) + + tray.on('click', () => host.showWindow()) + nativeTheme.on('updated', () => apply(state)) + return true +} diff --git a/desktop/src/preload/index.ts b/desktop/src/preload/index.ts new file mode 100644 index 000000000..6067ead90 --- /dev/null +++ b/desktop/src/preload/index.ts @@ -0,0 +1,32 @@ +import { contextBridge, ipcRenderer } from 'electron' +import type { AgentFieldApi } from '../shared/types' + +// Sandboxed preload: only contextBridge/ipcRenderer are used, no Node APIs. +const api: AgentFieldApi = { + getSnapshot: () => ipcRenderer.invoke('agentfield:snapshot'), + getCatalog: () => ipcRenderer.invoke('agentfield:catalog'), + install: (name) => ipcRenderer.invoke('agentfield:install', name), + onInstallProgress: (listener) => { + const wrapped = (_event: Electron.IpcRendererEvent, line: string) => listener(line) + ipcRenderer.on('agentfield:install-progress', wrapped) + return () => ipcRenderer.removeListener('agentfield:install-progress', wrapped) + }, + agentAction: (action, name) => ipcRenderer.invoke('agentfield:agent-action', action, name), + getEnvReports: () => ipcRenderer.invoke('agentfield:env-reports'), + setAgentSecret: (agent, key, value) => + ipcRenderer.invoke('agentfield:secret-set', agent, key, value), + revokeAgentSecret: (agent, key) => ipcRenderer.invoke('agentfield:secret-revoke', agent, key), + getSettings: () => ipcRenderer.invoke('agentfield:settings-get'), + setSettings: (patch) => ipcRenderer.invoke('agentfield:settings-set', patch), + getCliStatus: () => ipcRenderer.invoke('agentfield:cli-status'), + updateCli: () => ipcRenderer.invoke('agentfield:cli-update'), + onNavigate: (listener) => { + const wrapped = (_event: Electron.IpcRendererEvent, view: string) => listener(view) + ipcRenderer.on('agentfield:navigate', wrapped) + return () => ipcRenderer.removeListener('agentfield:navigate', wrapped) + }, + announceReady: () => ipcRenderer.invoke('agentfield:renderer-ready'), + platform: process.platform +} + +contextBridge.exposeInMainWorld('agentfield', api) diff --git a/desktop/src/renderer/index.html b/desktop/src/renderer/index.html new file mode 100644 index 000000000..05b69c004 --- /dev/null +++ b/desktop/src/renderer/index.html @@ -0,0 +1,16 @@ + + + + + + + AgentField + + +
+ + + diff --git a/desktop/src/renderer/src/App.tsx b/desktop/src/renderer/src/App.tsx new file mode 100644 index 000000000..8836a917b --- /dev/null +++ b/desktop/src/renderer/src/App.tsx @@ -0,0 +1,119 @@ +import { useCallback, useEffect, useState } from 'react' +import { type View, isView } from '../../shared/deeplink' +import type { AgentFieldSnapshot } from '../../shared/types' +import { Sidebar } from './components/Sidebar' +import { DashboardView } from './components/DashboardView' +import { AgentsPanel } from './components/AgentsPanel' +import { ActivityPanel } from './components/ActivityPanel' +import { InstallPanel } from './components/InstallPanel' +import { SettingsPanel } from './components/SettingsPanel' + +const POLL_INTERVAL_MS = 5000 + +export type CpTone = 'green' | 'yellow' | 'red' | 'gray' + +export function controlPlaneStatus(snapshot: AgentFieldSnapshot | null): { + tone: CpTone + label: string + detail?: string +} { + const cp = snapshot?.controlPlane + if (!cp) return { tone: 'gray', label: 'Checking…' } + if (cp.healthy) return { tone: 'green', label: 'Running' } + if (cp.reachable && cp.recognized) { + return { tone: 'yellow', label: 'Unhealthy', detail: cp.error } + } + if (cp.reachable) { + return { tone: 'yellow', label: 'Port in use', detail: cp.error } + } + return { + tone: 'red', + label: 'Not running', + detail: 'Start the control plane with `af server`, then this app picks it up automatically.' + } +} + +const VIEW_TITLES: Record = { + dashboard: 'Dashboard', + agents: 'Agents', + activity: 'Activity', + install: 'Install', + settings: 'Settings' +} + +export default function App() { + const [snapshot, setSnapshot] = useState(null) + const [ipcError, setIpcError] = useState(null) + const [view, setView] = useState('dashboard') + + useEffect(() => { + // Lets styles.css inset window chrome for macOS traffic lights vs the + // Windows caption-button overlay. + document.body.dataset.platform = window.agentfield.platform + }, []) + + useEffect(() => { + // agentfield:// deep links land here via the main process. Deep + // links from before this listener existed (a link that cold-started the + // app) are collected by announceReady once the subscription is live. + const unsubscribe = window.agentfield.onNavigate((v) => { + if (isView(v)) setView(v) + }) + void window.agentfield.announceReady().then((v) => { + if (v !== null && isView(v)) setView(v) + }) + return unsubscribe + }, []) + + const refresh = useCallback(async () => { + try { + const next = await window.agentfield.getSnapshot() + setSnapshot(next) + setIpcError(null) + } catch (err) { + setIpcError(err instanceof Error ? err.message : String(err)) + } + }, []) + + useEffect(() => { + void refresh() + const timer = setInterval(() => void refresh(), POLL_INTERVAL_MS) + return () => clearInterval(timer) + }, [refresh]) + + const cp = controlPlaneStatus(snapshot) + const installedNames = snapshot?.registry.agents.map((a) => a.name) ?? [] + + return ( +
+ + +
+
+

{VIEW_TITLES[view]}

+
+
+ {ipcError &&
{ipcError}
} + {cp.detail &&
{cp.detail}
} + + {view === 'dashboard' && ( + + )} + {view === 'agents' && ( + void refresh()} /> + )} + {view === 'activity' && ( + + )} + {view === 'install' && ( + void refresh()} /> + )} + {view === 'settings' && } +
+
+
+ ) +} diff --git a/desktop/src/renderer/src/components/ActivityPanel.tsx b/desktop/src/renderer/src/components/ActivityPanel.tsx new file mode 100644 index 000000000..fe01712dc --- /dev/null +++ b/desktop/src/renderer/src/components/ActivityPanel.tsx @@ -0,0 +1,84 @@ +import type { ReactElement } from 'react' +import type { ExecutionsResult, ExecutionSummary } from '../../../shared/types' + +interface ActivityPanelProps { + executions: ExecutionsResult | null + controlPlaneUp: boolean +} + +function formatDuration(ms: number | null): string { + if (ms === null) return '' + if (ms < 1000) return `${ms}ms` + if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s` + return `${Math.round(ms / 60_000)}m` +} + +function formatStarted(iso: string): string { + const t = Date.parse(iso) + if (Number.isNaN(t)) return '' + return new Date(t).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) +} + +const STATUS_GLYPH: Record = { + succeeded: '✓', + failed: '✕', + cancelled: '⊘', + timeout: '⏱' +} + +export function ActivityPanel({ executions, controlPlaneUp }: ActivityPanelProps): ReactElement { + return ( +
+ +
+ ) +} + +function ActivityBody({ executions, controlPlaneUp }: ActivityPanelProps) { + if (!controlPlaneUp || executions === null) { + return ( +
+ Activity appears here once the control plane is running. +
+ ) + } + if (executions.running.length === 0 && executions.recent.length === 0) { + return
No executions yet.
+ } + return ( +
    + {executions.running.map((run) => ( + + ))} + {executions.recent.map((run) => ( + + ))} +
+ ) +} + +export function ExecutionRow({ run, live = false }: { run: ExecutionSummary; live?: boolean }) { + return ( +
  • + {live ? ( +
  • + ) +} diff --git a/desktop/src/renderer/src/components/AgentsPanel.tsx b/desktop/src/renderer/src/components/AgentsPanel.tsx new file mode 100644 index 000000000..62c9d223e --- /dev/null +++ b/desktop/src/renderer/src/components/AgentsPanel.tsx @@ -0,0 +1,165 @@ +import { useCallback, useEffect, useState } from 'react' +import type { ReactElement } from 'react' +import type { AgentEnvReport, AgentFieldSnapshot } from '../../../shared/types' +import { EnvEditor } from './EnvEditor' + +type AgentAction = 'start' | 'stop' | 'restart' + +interface AgentsPanelProps { + registry: AgentFieldSnapshot['registry'] | null + /** Called after a lifecycle action so the snapshot refreshes promptly. */ + onChanged: () => void +} + +const BADGE_LABEL: Record = { + running: 'Running', + stopped: 'Stopped', + unknown: 'Unknown' +} + +const BUSY_LABEL: Record = { + start: 'Starting…', + stop: 'Stopping…', + restart: 'Restarting…' +} + +export function AgentsPanel({ registry, onChanged }: AgentsPanelProps): ReactElement { + return ( +
    + +
    + ) +} + +function AgentsBody({ registry, onChanged }: AgentsPanelProps) { + const [busy, setBusy] = useState<{ name: string; action: AgentAction } | null>(null) + const [failure, setFailure] = useState<{ name: string; message: string } | null>(null) + const [envReports, setEnvReports] = useState>({}) + const [expanded, setExpanded] = useState(null) + + // Env/secret statuses come from the af CLI + manifests — refreshed on + // mount and after any change, not on the snapshot poll (each refresh + // shells out to `af secrets ls`). + const loadEnv = useCallback(() => { + window.agentfield + .getEnvReports() + .then((reports) => { + const byAgent: Record = {} + for (const report of reports) byAgent[report.agent] = report + setEnvReports(byAgent) + }) + .catch(() => {}) + }, []) + useEffect(loadEnv, [loadEnv]) + + if (!registry) { + return
    Loading…
    + } + if (registry.error) { + return
    {registry.error}
    + } + if (!registry.exists || registry.agents.length === 0) { + return ( +
    +

    No agents installed yet.

    +

    Head to Install to add your first one.

    +
    + ) + } + + const run = async (action: AgentAction, name: string) => { + // Starting an agent with unresolved required keys is a guaranteed + // "missing required environment variables" failure — open the editor + // instead of letting it happen. + const report = envReports[name] + if (action !== 'stop' && report && !report.satisfied) { + setExpanded(name) + setFailure({ name, message: 'This agent needs keys before it can start — add them below.' }) + return + } + setBusy({ name, action }) + setFailure(null) + const result = await window.agentfield.agentAction(action, name) + setBusy(null) + if (!result.ok) setFailure({ name, message: result.message }) + onChanged() + loadEnv() + } + + const onEnvChanged = () => { + loadEnv() + setFailure(null) + } + + return ( +
      + {registry.agents.map((agent) => { + const isBusy = busy?.name === agent.name + const running = agent.badge === 'running' + const report = envReports[agent.name] + const isExpanded = expanded === agent.name + return ( +
    • +
      +
      + {isExpanded && report && } +
    • + ) + })} +
    + ) +} diff --git a/desktop/src/renderer/src/components/DashboardView.tsx b/desktop/src/renderer/src/components/DashboardView.tsx new file mode 100644 index 000000000..4c1cdde18 --- /dev/null +++ b/desktop/src/renderer/src/components/DashboardView.tsx @@ -0,0 +1,95 @@ +import type { ReactElement } from 'react' +import type { AgentFieldSnapshot } from '../../../shared/types' +import type { View } from './Sidebar' +import { ExecutionRow } from './ActivityPanel' + +interface DashboardViewProps { + snapshot: AgentFieldSnapshot | null + onNavigate: (view: View) => void +} + +function Tile({ + label, + value, + context +}: { + label: string + value: string + context?: string +}) { + return ( +
    + {label} + {value} + {context && {context}} +
    + ) +} + +function todayDelta(today: number, yesterday: number): string { + if (yesterday === 0) return today > 0 ? 'none yesterday' : '' + const diff = today - yesterday + if (diff === 0) return 'same as yesterday' + return `${diff > 0 ? '+' : ''}${diff} vs yesterday` +} + +export function DashboardView({ snapshot, onNavigate }: DashboardViewProps): ReactElement { + const metrics = snapshot?.metrics ?? null + const executions = snapshot?.executions ?? null + const runningNow = executions?.running.length ?? 0 + const off = metrics === null + + return ( + <> +
    + + + + +
    + +
    +
    +

    Recent activity

    + +
    +
    + {executions === null ? ( +
    + Activity appears here once the control plane is running. +
    + ) : executions.running.length === 0 && executions.recent.length === 0 ? ( +
    No executions yet.
    + ) : ( +
      + {executions.running.map((run) => ( + + ))} + {executions.recent.slice(0, 3).map((run) => ( + + ))} +
    + )} +
    +
    + + ) +} diff --git a/desktop/src/renderer/src/components/EnvEditor.tsx b/desktop/src/renderer/src/components/EnvEditor.tsx new file mode 100644 index 000000000..80103296b --- /dev/null +++ b/desktop/src/renderer/src/components/EnvEditor.tsx @@ -0,0 +1,162 @@ +import { useState } from 'react' +import type { ReactElement } from 'react' +import type { AgentEnvReport, AgentEnvVar, EnvVarStatus } from '../../../shared/types' + +interface EnvEditorProps { + report: AgentEnvReport + /** Called after a successful set/revoke so statuses refresh. */ + onChanged: () => void +} + +const STATUS_LABEL: Record = { + env: 'From environment', + stored: 'Stored', + default: 'Default', + missing: 'Missing' +} + +/** + * Inline editor for one agent's declared environment variables / API keys. + * Values are write-only: they go straight into the af CLI's encrypted secret + * store and are never read back — only the resolution status is shown. + */ +export function EnvEditor({ report, onChanged }: EnvEditorProps): ReactElement { + const required = report.vars.filter((v) => v.required && !v.group) + const groups = new Map() + for (const v of report.vars) { + if (!v.group) continue + const list = groups.get(v.group) ?? [] + list.push(v) + groups.set(v.group, list) + } + const optional = report.vars.filter((v) => !v.required) + + return ( +
    + {report.error &&
    {report.error}
    } + {required.length > 0 && ( + + )} + {[...groups.entries()].map(([id, vars]) => ( + + ))} + {optional.length > 0 && ( + + )} +
    + ) +} + +interface EnvSectionProps { + agent: string + title: string + vars: AgentEnvVar[] + onChanged: () => void +} + +function EnvSection({ agent, title, vars, onChanged }: EnvSectionProps) { + return ( +
    +
    {title}
    + {vars.map((v) => ( + + ))} +
    + ) +} + +function EnvRow({ + agent, + envVar, + onChanged +}: { + agent: string + envVar: AgentEnvVar + onChanged: () => void +}) { + const [draft, setDraft] = useState('') + const [busy, setBusy] = useState(false) + const [error, setError] = useState(null) + + const save = async () => { + setBusy(true) + setError(null) + const result = await window.agentfield.setAgentSecret(agent, envVar.name, draft) + setBusy(false) + if (!result.ok) { + setError(result.message) + return + } + setDraft('') + onChanged() + } + + const revoke = async () => { + setBusy(true) + setError(null) + const result = await window.agentfield.revokeAgentSecret(agent, envVar.name) + setBusy(false) + if (!result.ok) { + setError(result.message) + return + } + onChanged() + } + + const stored = envVar.storedScopes.length > 0 + // A stored global key is shared by every agent that names it — say so + // before a revoke silently breaks a sibling. + const revokeTitle = envVar.storedScopes.includes('global') + ? 'Remove from the shared (global) secret store' + : 'Remove from this agent’s secret store' + + return ( +
    +
    +
    + {envVar.name} + {STATUS_LABEL[envVar.status]} + {stored && envVar.scope === 'global' && shared} +
    + {envVar.description && {envVar.description}} + {error && {error}} +
    +
    + setDraft(event.target.value)} + onKeyDown={(event) => { + if (event.key === 'Enter' && draft.trim() !== '') void save() + }} + /> + + {stored && ( + + )} +
    +
    + ) +} diff --git a/desktop/src/renderer/src/components/InstallPanel.tsx b/desktop/src/renderer/src/components/InstallPanel.tsx new file mode 100644 index 000000000..c170eab0d --- /dev/null +++ b/desktop/src/renderer/src/components/InstallPanel.tsx @@ -0,0 +1,82 @@ +import { useEffect, useState } from 'react' +import type { CatalogEntry } from '../../../shared/types' + +interface InstallPanelProps { + installedNames: string[] + onInstalled: () => void +} + +type InstallPhase = + | { state: 'idle' } + | { state: 'installing'; name: string; progress: string } + | { state: 'done'; name: string; ok: boolean; message: string } + +export function InstallPanel({ installedNames, onInstalled }: InstallPanelProps) { + const [catalog, setCatalog] = useState([]) + const [phase, setPhase] = useState({ state: 'idle' }) + + useEffect(() => { + void window.agentfield.getCatalog().then(setCatalog) + }, []) + + useEffect(() => { + return window.agentfield.onInstallProgress((line) => { + setPhase((prev) => + prev.state === 'installing' ? { ...prev, progress: line } : prev + ) + }) + }, []) + + const install = async (name: string) => { + setPhase({ state: 'installing', name, progress: 'Starting…' }) + const result = await window.agentfield.install(name) + setPhase({ state: 'done', name, ok: result.ok, message: result.message }) + if (result.ok) onInstalled() + } + + const installing = phase.state === 'installing' + + return ( + <> +

    + Curated agent nodes, installed with one click. More arrive as the catalog grows. +

    +
    + {catalog.length === 0 &&
    Loading catalog…
    } +
      + {catalog.map((entry) => { + const isInstalled = installedNames.includes(entry.name) + const busy = installing && phase.name === entry.name + return ( +
    • +
      + {entry.name} + {entry.description} + {busy && phase.state === 'installing' && ( + {phase.progress} + )} + {phase.state === 'done' && phase.name === entry.name && !phase.ok && ( + {phase.message} + )} +
      +
      + {isInstalled ? ( + Installed ✓ + ) : ( + + )} +
      +
    • + ) + })} +
    +
    + + ) +} diff --git a/desktop/src/renderer/src/components/SettingsPanel.tsx b/desktop/src/renderer/src/components/SettingsPanel.tsx new file mode 100644 index 000000000..30a689e5d --- /dev/null +++ b/desktop/src/renderer/src/components/SettingsPanel.tsx @@ -0,0 +1,205 @@ +import { useEffect, useState } from 'react' +import type { CliStatus, DesktopSettings, InstalledAgent } from '../../../shared/types' + +interface SettingsPanelProps { + agents: InstalledAgent[] +} + +/** + * The "set it and forget it" surface: launch at login, keep the control + * plane up, and pick which agents come up with it — so everything is already + * answering by the time Claude (or anything else) queries it. + */ +export function SettingsPanel({ agents }: SettingsPanelProps) { + const [settings, setSettings] = useState(null) + + useEffect(() => { + void window.agentfield.getSettings().then(setSettings) + }, []) + + const update = (patch: Partial) => { + // Optimistic: flip the control immediately, reconcile with what main + // actually persisted (it normalizes and applies login-item effects). + setSettings((prev) => (prev ? { ...prev, ...patch } : prev)) + void window.agentfield.setSettings(patch).then(setSettings) + } + + if (!settings) { + return ( +
    +
    Loading…
    +
    + ) + } + + const toggleAgent = (name: string, on: boolean) => { + const next = on + ? [...settings.autostartAgents, name] + : settings.autostartAgents.filter((n) => n !== name) + update({ autostartAgents: next }) + } + + return ( + <> +

    + Set everything up once — the app keeps your agents ready for whatever queries them. +

    + +
    +
      + update({ openAtLogin: on })} + /> + update({ autostartControlPlane: on })} + /> + update({ installSkills: on })} + /> +
    +
    + +

    AgentField CLI

    + + +

    Auto-start agents

    +
    + {agents.length === 0 ? ( +
    + Install an agent first — then pick which ones start with the app. +
    + ) : ( +
      + {agents.map((agent) => ( + toggleAgent(agent.name, on)} + /> + ))} +
    + )} +
    + + ) +} + +/** Which af the app drives, and the one-click path to a good version. */ +function CliCard() { + const [status, setStatus] = useState(null) + const [busy, setBusy] = useState(false) + + useEffect(() => { + void window.agentfield.getCliStatus().then(setStatus) + }, []) + + if (!status) { + return ( +
    +
    Checking…
    +
    + ) + } + + const runUpdate = async () => { + setBusy(true) + setStatus(await window.agentfield.updateCli()) + setBusy(false) + } + + const SOURCE_LABEL: Record = { + managed: 'installed in ~/.agentfield', + path: 'from your PATH', + bundled: 'bundled with the app' + } + + const versionLabel = status.version ? `v${status.version}` : status.command ? 'dev build' : '—' + const updateAvailable = + status.bundledAvailable && + status.bundledVersion !== null && + status.version !== null && + status.bundledVersion !== status.version + + let issue: string | null = null + let buttonLabel: string | null = null + if (!status.command) { + issue = 'No AgentField CLI found.' + if (status.bundledAvailable) buttonLabel = 'Install AgentField CLI' + } else if (status.outdated) { + issue = `Your installed AgentField (v${status.outdated.version}) is older than this app needs (v${status.minVersion}) — the app is using its bundled copy meanwhile.` + buttonLabel = 'Update AgentField' + } else if (updateAvailable) { + buttonLabel = `Update to v${status.bundledVersion}` + } + + return ( +
    +
      +
    • +
      + + {versionLabel} + {status.source && ( + · {SOURCE_LABEL[status.source] ?? status.source} + )} + + {issue && {issue}} +
      + {buttonLabel && ( +
      + +
      + )} +
    • +
    +
    + ) +} + +function ToggleRow({ + title, + sub, + checked, + onChange +}: { + title: string + sub?: string + checked: boolean + onChange: (on: boolean) => void +}) { + return ( +
  • +
    + {title} + {sub && {sub}} +
    +
    + +
    +
  • + ) +} diff --git a/desktop/src/renderer/src/components/Sidebar.tsx b/desktop/src/renderer/src/components/Sidebar.tsx new file mode 100644 index 000000000..613871045 --- /dev/null +++ b/desktop/src/renderer/src/components/Sidebar.tsx @@ -0,0 +1,75 @@ +import type { ReactElement } from 'react' +import type { View } from '../../../shared/deeplink' +import type { CpTone } from '../App' + +// Re-exported so view components keep one import site; the canonical list +// lives in shared/deeplink.ts, where agentfield:// URLs resolve to views. +export type { View } + +interface SidebarProps { + view: View + onSelect: (view: View) => void + cpTone: CpTone + cpLabel: string +} + +function Icon({ d }: { d: string }) { + return ( + + ) +} + +// Simple line icons (24px grid, stroked), kept inline so the CSP stays strict. +const ICONS: Record = { + dashboard: 'M3 3h7v9H3zM14 3h7v5h-7zM14 12h7v9h-7zM3 16h7v5H3z', + agents: 'M5 7h14v10H5zM9 21h6M12 17v4M9 3v4M15 3v4', + activity: 'M3 12h4l3 -8l4 16l3 -8h4', + install: 'M12 3v12M7 10l5 5l5 -5M4 21h16', + settings: 'M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M1 14h6M9 8h6M17 16h6' +} + +const NAV: Array<{ id: View; label: string }> = [ + { id: 'dashboard', label: 'Dashboard' }, + { id: 'agents', label: 'Agents' }, + { id: 'activity', label: 'Activity' }, + { id: 'install', label: 'Install' }, + { id: 'settings', label: 'Settings' } +] + +export function Sidebar({ view, onSelect, cpTone, cpLabel }: SidebarProps): ReactElement { + return ( + + ) +} diff --git a/desktop/src/renderer/src/env.d.ts b/desktop/src/renderer/src/env.d.ts new file mode 100644 index 000000000..c74e20608 --- /dev/null +++ b/desktop/src/renderer/src/env.d.ts @@ -0,0 +1,6 @@ +declare module '*.css' + +interface Window { + /** Exposed by src/preload/index.ts via contextBridge. */ + agentfield: import('../../shared/types').AgentFieldApi +} diff --git a/desktop/src/renderer/src/main.tsx b/desktop/src/renderer/src/main.tsx new file mode 100644 index 000000000..241cf437d --- /dev/null +++ b/desktop/src/renderer/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './styles.css' + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + +) diff --git a/desktop/src/renderer/src/styles.css b/desktop/src/renderer/src/styles.css new file mode 100644 index 000000000..0e7627076 --- /dev/null +++ b/desktop/src/renderer/src/styles.css @@ -0,0 +1,698 @@ +/* AgentField — Mac-first desktop styles. + Sidebar + content layout, system font stack, light/dark from the OS, + hairline borders, a seamless draggable titlebar. Plain CSS, no framework. */ + +:root { + --bg: #f5f5f7; + --sidebar-bg: rgba(0, 0, 0, 0.035); + --panel: rgba(255, 255, 255, 0.72); + --hairline: rgba(0, 0, 0, 0.09); + --text: #1d1d1f; + --text-secondary: #6e6e73; + --text-tertiary: #98989d; + --green: #28a745; + --yellow: #b58a00; + --red: #e0342b; + --gray: #98989d; + --accent: #0071e3; + --accent-pressed: #0060c0; + --pill-bg: rgba(0, 0, 0, 0.05); + --nav-active-bg: rgba(0, 0, 0, 0.07); +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #1e1e20; + --sidebar-bg: rgba(255, 255, 255, 0.03); + --panel: rgba(255, 255, 255, 0.055); + --hairline: rgba(255, 255, 255, 0.1); + --text: #f5f5f7; + --text-secondary: #a1a1a6; + --text-tertiary: #6e6e73; + --green: #30d158; + --yellow: #ffd60a; + --red: #ff453a; + --gray: #8e8e93; + --accent: #0a84ff; + --accent-pressed: #0060c0; + --pill-bg: rgba(255, 255, 255, 0.08); + --nav-active-bg: rgba(255, 255, 255, 0.09); + } +} + +* { + box-sizing: border-box; +} + +html, +body, +#root { + height: 100%; +} + +body { + margin: 0; + font-family: + -apple-system, + BlinkMacSystemFont, + 'SF Pro Text', + 'Segoe UI Variable', + 'Segoe UI', + system-ui, + sans-serif; + background: var(--bg); + color: var(--text); + font-size: 13px; + line-height: 1.45; + -webkit-font-smoothing: antialiased; + user-select: none; +} + +/* On macOS the window has sidebar vibrancy: let it show through. */ +body[data-platform='darwin'] { + background: transparent; +} + +body[data-platform='darwin'] .sidebar { + background: transparent; +} + +.app { + display: flex; + height: 100%; +} + +/* --- sidebar --------------------------------------------------------------- */ + +.sidebar { + flex: none; + width: 200px; + display: flex; + flex-direction: column; + background: var(--sidebar-bg); + border-right: 1px solid var(--hairline); + /* The whole rail drags the window; interactive children opt out. */ + -webkit-app-region: drag; + padding: 14px 10px; +} + +/* Clear the macOS traffic lights. */ +body[data-platform='darwin'] .sidebar { + padding-top: 48px; +} + +.sidebar-brand { + font-size: 13px; + font-weight: 700; + letter-spacing: -0.01em; + padding: 4px 10px 14px; +} + +.sidebar-nav { + display: flex; + flex-direction: column; + gap: 2px; +} + +.nav-item { + -webkit-app-region: no-drag; + display: flex; + align-items: center; + gap: 9px; + width: 100%; + border: none; + background: transparent; + color: var(--text-secondary); + font-family: inherit; + font-size: 13px; + font-weight: 500; + text-align: left; + padding: 7px 10px; + border-radius: 8px; + cursor: pointer; +} + +.nav-item svg { + flex: none; + opacity: 0.75; +} + +.nav-item:hover { + background: var(--pill-bg); + color: var(--text); +} + +.nav-item.active { + background: var(--nav-active-bg); + color: var(--text); +} + +.sidebar-foot { + margin-top: auto; + padding: 10px 6px 2px; +} + +/* --- main column ------------------------------------------------------------ */ + +.main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; +} + +.view-header { + -webkit-app-region: drag; + flex: none; + display: flex; + align-items: center; + height: 52px; + padding: 0 28px; +} + +/* Clear the Windows caption-button overlay. */ +body[data-platform='win32'] .view-header { + padding-right: 150px; +} + +.view-header h1 { + margin: 0; + font-size: 17px; + font-weight: 700; + letter-spacing: -0.02em; +} + +.view-body { + flex: 1; + overflow-y: auto; + padding: 6px 28px 32px; + display: flex; + flex-direction: column; + gap: 18px; + max-width: 820px; + width: 100%; +} + +.view-lede { + margin: -6px 0 0; + color: var(--text-secondary); + font-size: 12.5px; +} + +/* --- dashboard tiles --------------------------------------------------------- */ + +.tile-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 12px; +} + +.tile { + background: var(--panel); + border: 1px solid var(--hairline); + border-radius: 12px; + padding: 14px 16px 12px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.tile-label { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--text-tertiary); +} + +.tile-value { + font-size: 26px; + font-weight: 700; + letter-spacing: -0.02em; + font-variant-numeric: tabular-nums; + line-height: 1.2; +} + +.tile-context { + font-size: 11.5px; + color: var(--text-secondary); +} + +.subhead { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 8px; +} + +.section-title { + margin: 0 0 0 4px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-tertiary); +} + +.link-button { + border: none; + background: none; + color: var(--accent); + font-family: inherit; + font-size: 12px; + font-weight: 500; + cursor: pointer; + padding: 0; +} + +/* --- panels & rows ------------------------------------------------------------ */ + +.panel { + background: var(--panel); + border: 1px solid var(--hairline); + border-radius: 12px; + overflow: hidden; +} + +.row-list { + list-style: none; + margin: 0; + padding: 0; +} + +.row { + display: flex; + align-items: center; + gap: 12px; + padding: 11px 16px; +} + +.row + .row { + border-top: 1px solid var(--hairline); +} + +.row-dot { + flex: none; + width: 9px; + height: 9px; + border-radius: 50%; +} + +.row-dot.running { + background: var(--green); +} +.row-dot.stopped { + background: var(--gray); +} +.row-dot.unknown { + background: var(--yellow); +} + +.row-dot.pulse { + animation: pulse 1.6s ease-in-out infinite; +} + +@keyframes pulse { + 0%, + 100% { + box-shadow: 0 0 0 0 rgba(48, 209, 88, 0.45); + } + 50% { + box-shadow: 0 0 0 5px rgba(48, 209, 88, 0); + } +} + +.row-main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; +} + +.row-title { + font-weight: 500; + letter-spacing: -0.01em; +} + +.row-sub { + font-size: 12px; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.row-progress { + font-size: 11.5px; + color: var(--text-tertiary); + margin-top: 3px; + font-variant-numeric: tabular-nums; +} + +.row-side { + flex: none; + display: flex; + align-items: center; + gap: 10px; +} + +.row-meta { + font-size: 12px; + color: var(--text-tertiary); + font-variant-numeric: tabular-nums; +} + +.row-past .row-title { + font-weight: 400; +} + +.run-glyph { + flex: none; + width: 9px; + text-align: center; + font-size: 11px; + color: var(--text-tertiary); +} + +.run-glyph.succeeded { + color: var(--green); +} +.run-glyph.failed, +.run-glyph.timeout { + color: var(--red); +} + +/* --- status pill ---------------------------------------------------------------- */ + +.status-pill { + -webkit-app-region: no-drag; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 10px; + border-radius: 999px; + background: var(--pill-bg); + font-size: 11.5px; + font-weight: 500; + color: var(--text-secondary); +} + +.status-pill .status-dot { + width: 7px; + height: 7px; + border-radius: 50%; +} + +.status-pill.green .status-dot { + background: var(--green); +} +.status-pill.yellow .status-dot { + background: var(--yellow); +} +.status-pill.red .status-dot { + background: var(--red); +} +.status-pill.gray .status-dot { + background: var(--gray); +} + +/* --- badges / buttons -------------------------------------------------------- */ + +.badge { + display: inline-block; + padding: 2px 9px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; +} + +.badge.running { + background: color-mix(in srgb, var(--green) 14%, transparent); + color: var(--green); +} + +.badge.stopped { + background: var(--pill-bg); + color: var(--text-secondary); +} + +.badge.unknown { + background: color-mix(in srgb, var(--yellow) 14%, transparent); + color: var(--yellow); +} + +.badge.warn { + background: color-mix(in srgb, var(--yellow) 14%, transparent); + color: var(--yellow); +} + +/* --- agent rows with an expandable keys editor -------------------------------- */ + +.row-item + .row-item { + border-top: 1px solid var(--hairline); +} + +.env-editor { + padding: 4px 16px 14px 37px; +} + +.env-section-title { + margin: 12px 0 2px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-tertiary); +} + +.env-row { + display: flex; + align-items: center; + gap: 12px; + padding: 7px 0; +} + +.env-row + .env-row { + border-top: 1px solid var(--hairline); +} + +.env-row-main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; +} + +.env-row-head { + display: flex; + align-items: center; + gap: 8px; +} + +.env-name { + font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; + font-size: 12px; + font-weight: 600; +} + +.env-scope { + font-size: 10.5px; + color: var(--text-tertiary); +} + +.chip { + display: inline-block; + padding: 1px 7px; + border-radius: 999px; + font-size: 10.5px; + font-weight: 600; +} + +.chip.env, +.chip.stored { + background: color-mix(in srgb, var(--green) 14%, transparent); + color: var(--green); +} + +.chip.default { + background: var(--pill-bg); + color: var(--text-secondary); +} + +.chip.missing { + background: color-mix(in srgb, var(--red) 12%, transparent); + color: var(--red); +} + +.env-row-controls { + flex: none; + display: flex; + align-items: center; + gap: 6px; +} + +.env-input { + -webkit-app-region: no-drag; + width: 180px; + background: var(--pill-bg); + border: 1px solid var(--hairline); + border-radius: 7px; + padding: 4px 9px; + font-size: 12px; + font-family: inherit; + color: var(--text); + outline: none; +} + +.env-input:focus { + border-color: var(--accent); +} + +.install-button { + -webkit-app-region: no-drag; + background: var(--accent); + color: #fff; + border: none; + border-radius: 999px; + padding: 4px 14px; + font-size: 12px; + font-weight: 600; + font-family: inherit; + cursor: pointer; +} + +.install-button:hover:not(:disabled) { + background: var(--accent-pressed); +} + +.install-button:disabled { + opacity: 0.5; + cursor: default; +} + +.installed-check { + font-size: 12px; + font-weight: 500; + color: var(--green); +} + +/* --- agent lifecycle controls ------------------------------------------------ */ + +.row-actions { + display: flex; + gap: 6px; +} + +.action-button { + -webkit-app-region: no-drag; + background: var(--pill-bg); + color: var(--text-primary); + border: none; + border-radius: 999px; + padding: 4px 12px; + font-size: 12px; + font-weight: 600; + font-family: inherit; + cursor: pointer; +} + +.action-button:hover:not(:disabled) { + background: color-mix(in srgb, var(--text-primary) 14%, var(--pill-bg)); +} + +.action-button.primary { + background: var(--accent); + color: #fff; +} + +.action-button.primary:hover:not(:disabled) { + background: var(--accent-pressed); +} + +.action-button:disabled { + opacity: 0.5; + cursor: default; +} + +/* --- settings ----------------------------------------------------------------- */ + +.section-title { + margin: 18px 0 0; + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); +} + +.switch { + -webkit-app-region: no-drag; + position: relative; + width: 36px; + height: 21px; + border: none; + border-radius: 999px; + background: var(--pill-bg); + cursor: pointer; + transition: background 0.15s ease; + flex: none; +} + +.switch.on { + background: var(--green); +} + +.switch-thumb { + position: absolute; + top: 2px; + left: 2px; + width: 17px; + height: 17px; + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); + transition: transform 0.15s ease; +} + +.switch.on .switch-thumb { + transform: translateX(15px); +} + +/* --- misc ------------------------------------------------------------------ */ + +.callout { + border-radius: 10px; + padding: 10px 14px; + font-size: 12.5px; + background: var(--pill-bg); + color: var(--text-secondary); +} + +.callout.error { + background: color-mix(in srgb, var(--red) 10%, transparent); + color: var(--red); +} + +.error-text { + color: var(--red); +} + +.empty { + padding: 26px 16px; + text-align: center; + color: var(--text-secondary); +} + +.empty p { + margin: 2px 0; +} + +.secondary { + color: var(--text-tertiary); +} + +code { + font-family: ui-monospace, 'SF Mono', 'Cascadia Mono', monospace; + font-size: 12px; + background: var(--pill-bg); + padding: 1px 6px; + border-radius: 4px; +} diff --git a/desktop/src/shared/catalog.ts b/desktop/src/shared/catalog.ts new file mode 100644 index 000000000..461011dd8 --- /dev/null +++ b/desktop/src/shared/catalog.ts @@ -0,0 +1,49 @@ +import type { CatalogEntry } from './types' + +// Curated list of installable agent nodes, shown in the app's Install view. +// +// This is deliberately a hard-coded list maintained by hand: entries are +// vetted, and the app refuses to install any source that is not in it (the +// renderer only ever passes a catalog *name* over IPC, never a raw source). +// When the marketplace/registry search lands, this file is the seam to +// replace with a remote catalog fetch. +// +// What qualifies: an Agent-Field org repo is installable iff it has an +// `agentfield-package.yaml` manifest at its root — that manifest is what +// `af install` requires. When adding an entry, `name` MUST equal the +// manifest's `name:` (the registry key after install — how the app detects +// installed state), which is often NOT the repo name (SWE-AF → swe-planner). +export const CATALOG: CatalogEntry[] = [ + { + name: 'swe-planner', + description: + 'Autonomous software-engineering fleet: plan, code, test, and ship production-grade PRs', + source: 'https://github.com/Agent-Field/SWE-AF', + language: 'python' + }, + { + name: 'pr-af', + description: 'Turns a plain task description into a draft pull request on GitHub', + source: 'https://github.com/Agent-Field/pr-af', + language: 'python' + }, + { + name: 'sec-af', + description: + 'Code security auditor: scans repositories and proves exploitability with verdicts and traces', + source: 'https://github.com/Agent-Field/sec-af', + language: 'python' + }, + { + name: 'cloudsecurity-af', + description: + 'Cloud security posture: read-only attack-path scans across AWS, GCP, and Azure', + source: 'https://github.com/Agent-Field/cloudsecurity-af', + language: 'python' + } +] + +/** Look up a catalog entry by name. Returns undefined for unknown names. */ +export function catalogEntry(name: string): CatalogEntry | undefined { + return CATALOG.find((entry) => entry.name === name) +} diff --git a/desktop/src/shared/deeplink.test.ts b/desktop/src/shared/deeplink.test.ts new file mode 100644 index 000000000..3df2fb04e --- /dev/null +++ b/desktop/src/shared/deeplink.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest' +import { deepLinkFromArgv, isView, parseDeepLink } from './deeplink' + +describe('parseDeepLink', () => { + it('maps each view URL to its view', () => { + expect(parseDeepLink('agentfield://dashboard')).toBe('dashboard') + expect(parseDeepLink('agentfield://agents')).toBe('agents') + expect(parseDeepLink('agentfield://activity')).toBe('activity') + expect(parseDeepLink('agentfield://install')).toBe('install') + }) + + it('is case-insensitive and tolerates trailing slashes and subpaths', () => { + expect(parseDeepLink('agentfield://Agents')).toBe('agents') + expect(parseDeepLink('agentfield://agents/')).toBe('agents') + expect(parseDeepLink('agentfield://agents/some-agent')).toBe('agents') + }) + + it('accepts the no-slash (opaque path) spelling', () => { + expect(parseDeepLink('agentfield:agents')).toBe('agents') + }) + + it('falls back to dashboard for a bare or unknown target', () => { + expect(parseDeepLink('agentfield://')).toBe('dashboard') + expect(parseDeepLink('agentfield://marketplace')).toBe('dashboard') + }) + + it('returns null for other schemes and non-URLs', () => { + expect(parseDeepLink('https://agentfield.ai')).toBeNull() + expect(parseDeepLink('http://localhost:8080/ui/agents')).toBeNull() + expect(parseDeepLink('C:\\Program Files\\AgentField\\AgentField.exe')).toBeNull() + expect(parseDeepLink('--allow-file-access-from-files')).toBeNull() + expect(parseDeepLink('')).toBeNull() + }) +}) + +describe('deepLinkFromArgv', () => { + it('finds the deep link among ordinary process args', () => { + const argv = ['C:\\AgentField\\AgentField.exe', '--allow-file-access', 'agentfield://activity'] + expect(deepLinkFromArgv(argv)).toBe('activity') + }) + + it('returns null when no arg is a deep link', () => { + expect(deepLinkFromArgv(['electron.exe', '.', '--inspect=9229'])).toBeNull() + expect(deepLinkFromArgv([])).toBeNull() + }) +}) + +describe('isView', () => { + it('accepts exactly the app views', () => { + for (const v of ['dashboard', 'agents', 'activity', 'install', 'settings']) { + expect(isView(v)).toBe(true) + } + expect(isView('marketplace')).toBe(false) + expect(isView('')).toBe(false) + }) +}) diff --git a/desktop/src/shared/deeplink.ts b/desktop/src/shared/deeplink.ts new file mode 100644 index 000000000..cad03e521 --- /dev/null +++ b/desktop/src/shared/deeplink.ts @@ -0,0 +1,49 @@ +// agentfield:// deep links. +// +// The desktop app registers itself as the `agentfield:` protocol handler +// (see main/index.ts and the electron-builder `protocols` config), so other +// surfaces — the macOS menu-bar tray (`af-tray`), docs, the web UI — can open +// the app on a specific view with e.g. `open agentfield://agents`. + +export const DEEP_LINK_SCHEME = 'agentfield' + +/** The app's views, also the vocabulary of deep-link targets. */ +export const VIEWS = ['dashboard', 'agents', 'activity', 'install', 'settings'] as const +export type View = (typeof VIEWS)[number] + +export function isView(value: string): value is View { + return (VIEWS as readonly string[]).includes(value) +} + +/** + * Parse an agentfield:// URL into the view it addresses. + * + * Returns null for anything that is not an agentfield: URL at all. A bare + * `agentfield://` and any unknown view fall back to 'dashboard', so links + * minted by newer (or older) senders still open the app instead of dying. + */ +export function parseDeepLink(url: string): View | null { + let parsed: URL + try { + parsed = new URL(url) + } catch { + return null + } + if (parsed.protocol !== `${DEEP_LINK_SCHEME}:`) return null + // agentfield://agents parses the view as the host; agentfield:agents (no + // slashes) parses it as an opaque pathname. Accept both spellings. + const target = (parsed.host || parsed.pathname).replace(/^\/+/, '').split('/')[0].toLowerCase() + return isView(target) ? target : 'dashboard' +} + +/** + * Find the deep link in a second-instance argv (how Windows/Linux hand the + * URL to an already-running app). Non-URL args parse to null and are skipped. + */ +export function deepLinkFromArgv(argv: readonly string[]): View | null { + for (const arg of argv) { + const view = parseDeepLink(arg) + if (view) return view + } + return null +} diff --git a/desktop/src/shared/types.ts b/desktop/src/shared/types.ts new file mode 100644 index 000000000..7d3886e90 --- /dev/null +++ b/desktop/src/shared/types.ts @@ -0,0 +1,231 @@ +// Shared types crossing the main / preload / renderer IPC boundary. +// Import these type-only from every layer — this file must stay runtime-free. + +/** Result of probing GET {baseUrl}/health on the control plane. */ +export interface ControlPlaneStatus { + /** An HTTP response came back (any status code, including 503). */ + reachable: boolean + /** + * The response body looks like an AgentField control plane health payload + * (status: "healthy" | "unhealthy"). False when some unrelated service is + * squatting on the port — its nodes view must not be trusted. + */ + recognized: boolean + /** The health endpoint answered 200 with a body reporting "healthy". */ + healthy: boolean + /** Raw JSON body of the health response, when one was parseable. */ + raw?: unknown + /** Network/timeout error when unreachable, or why the payload was rejected. */ + error?: string +} + +/** One entry parsed from ~/.agentfield/installed.yaml. */ +export interface InstalledAgent { + name: string + version: string + description: string + /** Optional on newer registry entries (python/go); absent on older ones. */ + language?: string + /** Raw registry status string (e.g. "running", "stopped"). */ + status: string + /** Install dir (~/.agentfield/packages/) — where the manifest lives. */ + path: string | null + port: number | null + pid: number | null +} + +/** Registry read result. Missing file/dir is a graceful empty state, not an error. */ +export interface RegistryResult { + exists: boolean + agents: InstalledAgent[] + /** Set when the registry file exists but could not be parsed. */ + error?: string +} + +/** Status badge shown in the UI, derived from registry + control-plane view. */ +export type AgentBadge = 'running' | 'stopped' | 'unknown' + +export interface SnapshotAgent extends InstalledAgent { + badge: AgentBadge +} + +/** One workflow run parsed from GET /api/ui/v2/workflow-runs. */ +export interface ExecutionSummary { + runId: string + /** e.g. "running", "succeeded", "failed" */ + status: string + /** Human-facing name (the root reasoner, e.g. "demo_echo"). */ + displayName: string + agentId: string + startedAt: string + durationMs: number | null + /** True once the run reached a terminal state. */ + terminal: boolean +} + +/** Executions view: in-flight runs plus a short tail of finished ones. */ +export interface ExecutionsResult { + running: ExecutionSummary[] + recent: ExecutionSummary[] +} + +/** One installable node in the curated catalog (see shared/catalog.ts). */ +export interface CatalogEntry { + /** Node name, matches the registry key after install. */ + name: string + description: string + /** `af install` source: a git URL or af://registry/ reference. */ + source: string + language?: string +} + +/** Terminal states of an install kicked off from the app. */ +export interface InstallResult { + ok: boolean + message: string +} + +/** Outcome of a start/stop/restart issued from the app. */ +export interface AgentActionResult { + ok: boolean + message: string +} + +/** + * How one declared variable resolves for `af run`, mirroring the CLI's + * EnvResolver order: process env → encrypted secret store → manifest default. + */ +export type EnvVarStatus = 'env' | 'stored' | 'default' | 'missing' + +/** One variable an agent's manifest declares under user_environment. */ +export interface AgentEnvVar { + name: string + description: string + /** Manifest type: secret — render a password input, mask everywhere. */ + secret: boolean + /** Store scope a set writes to: shared "global" (default) or per-node. */ + scope: 'global' | 'node' + /** Must resolve for `af run` to succeed (required list or a group member). */ + required: boolean + /** require_one_of group id — any one member resolving satisfies the group. */ + group?: string + groupDescription?: string + status: EnvVarStatus + /** Secret-store scopes currently holding this key ("global" or the node name). */ + storedScopes: string[] +} + +/** + * Everything the renderer needs to show and edit one agent's keys. Values + * themselves never cross the IPC boundary — only these status flags do. + */ +export interface AgentEnvReport { + agent: string + vars: AgentEnvVar[] + /** Every required variable and group resolves — `af run` won't fail on env. */ + satisfied: boolean + /** Set when the secret store could not be read (statuses degrade gracefully). */ + error?: string +} + +/** Which af CLI the app resolved and whether an installed copy needs updating. */ +export interface CliStatus { + /** Spawnable command (absolute path or bare "af"), null when none usable. */ + command: string | null + /** Where the resolved CLI came from. */ + source: 'managed' | 'path' | 'bundled' | null + /** Its version, or null for dev/unparseable builds (trusted as-is). */ + version: string | null + /** Oldest version this app can drive. */ + minVersion: string + /** An installed copy that is too old — drives the "Update AgentField" banner. */ + outdated: { source: string; version: string } | null + /** The app package carries a CLI it can (re)install. */ + bundledAvailable: boolean + bundledVersion: string | null +} + +/** + * Persisted app settings (settings.json in the app's user-data dir). + * The goal: the app is "just there" — it boots at login, brings the control + * plane up, starts the agents you selected, and everything is queryable the + * moment Claude/Codex/anything asks. + */ +export interface DesktopSettings { + /** Launch the app when you log in (starts hidden, in the tray). */ + openAtLogin: boolean + /** Start the control plane on app launch when nothing is listening. */ + autostartControlPlane: boolean + /** Installed agent names to start once the control plane is healthy. */ + autostartAgents: string[] + /** + * Keep the AgentField skills (agentfield: building agents; agentfield-use: + * calling installed ones) installed in detected coding agents (Claude + * Code, Codex, …) via `af skill install` — so they know how to use this. + */ + installSkills: boolean +} + +/** Headline numbers from GET /api/ui/v1/dashboard/summary. */ +export interface DashboardMetrics { + agentsRunning: number + agentsTotal: number + executionsToday: number + executionsYesterday: number + /** Percentage 0-100, or null when the server reports none. */ + successRate: number | null +} + +/** The single payload shipped over IPC to the renderer. */ +export interface AgentFieldSnapshot { + controlPlane: ControlPlaneStatus & { baseUrl: string } + registry: { + exists: boolean + agents: SnapshotAgent[] + error?: string + } + /** null when the control plane view is unavailable. */ + executions: ExecutionsResult | null + /** null when the control plane view is unavailable. */ + metrics: DashboardMetrics | null + /** ISO timestamp of when this snapshot was assembled. */ + fetchedAt: string +} + +/** Surface exposed on window.agentfield by the preload script. */ +export interface AgentFieldApi { + getSnapshot(): Promise + getCatalog(): Promise + /** Install a catalog entry by name. Resolves when `af install` exits. */ + install(name: string): Promise + /** Start / stop / restart an installed agent by its registry name. */ + agentAction(action: 'start' | 'stop' | 'restart', name: string): Promise + /** Env/secret status for every installed agent that declares variables. */ + getEnvReports(): Promise + /** Store a declared variable's value in af's encrypted secret store. */ + setAgentSecret(agent: string, key: string, value: string): Promise + /** Remove a stored value from every scope relevant to this agent. */ + revokeAgentSecret(agent: string, key: string): Promise + getSettings(): Promise + /** Merge a partial update into the settings; returns the result. */ + setSettings(patch: Partial): Promise + /** Which af CLI the app is using (managed / PATH / bundled) and its version. */ + getCliStatus(): Promise + /** Install/refresh the bundled CLI into ~/.agentfield/bin; returns new status. */ + updateCli(): Promise + /** Subscribe to install output lines; returns an unsubscribe function. */ + onInstallProgress(listener: (line: string) => void): () => void + /** + * Subscribe to deep-link navigation (agentfield://). The view arrives + * as a plain string over IPC; validate with isView() before trusting it. + */ + onNavigate(listener: (view: string) => void): () => void + /** + * Tell the main process the navigation listener is live. Returns the view + * of a deep link that arrived before then (e.g. the link that cold-started + * a hidden app), or null. Call once, after subscribing with onNavigate. + */ + announceReady(): Promise + /** "darwin" | "win32" | "linux" — for platform-specific chrome (traffic-light inset). */ + platform: string +} diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json new file mode 100644 index 000000000..6d570caf5 --- /dev/null +++ b/desktop/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "types": ["node", "electron-vite/node"], + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "isolatedModules": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true + }, + "include": ["electron.vite.config.ts", "src"] +} diff --git a/desktop/vendor/.gitkeep b/desktop/vendor/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/install.sh b/scripts/install.sh index 808b031b9..961c4df9e 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -545,13 +545,17 @@ verify_installation() { fi } -# Install the agentfield skill into coding-agent integrations (Claude Code, +# Install the agentfield skills into coding-agent integrations (Claude Code, # Codex, Gemini, OpenCode, Aider, Windsurf, Cursor). Delegated to the # freshly-installed `af` binary so the install logic stays in one place. +# Two skills ship with the binary: agentfield (building agents) and +# agentfield-use (calling installed agents) — `af skill install` handles one +# skill per invocation, so loop. # Honors $SKILL_MODE: all (default) | all-targets | interactive | none. install_skill() { local install_dir="$1" local af_bin="$install_dir/agentfield" + local skill if [[ ! -x "$af_bin" ]]; then print_warning "af binary not executable, skipping skill install" @@ -568,17 +572,24 @@ install_skill() { ;; all) printf "\n" - print_info "Installing skill into all detected coding agents..." - "$af_bin" skill install --all || print_warning "Skill install reported errors" + print_info "Installing skills into all detected coding agents..." + for skill in agentfield agentfield-use; do + "$af_bin" skill install "$skill" --all || print_warning "Skill install ($skill) reported errors" + done ;; all-targets) printf "\n" - print_info "Installing skill into all registered coding agents (even undetected)..." - "$af_bin" skill install --all-targets || print_warning "Skill install reported errors" + print_info "Installing skills into all registered coding agents (even undetected)..." + for skill in agentfield agentfield-use; do + "$af_bin" skill install "$skill" --all-targets || print_warning "Skill install ($skill) reported errors" + done ;; interactive|*) printf "\n" "$af_bin" skill install || print_warning "Skill install reported errors" + printf "\n" + print_info "Also installing the agentfield-use skill (calling installed agents)..." + "$af_bin" skill install agentfield-use --non-interactive || print_warning "Skill install (agentfield-use) reported errors" ;; esac } diff --git a/scripts/sync-embedded-skills.sh b/scripts/sync-embedded-skills.sh index c8cd3d692..8acb7deb9 100755 --- a/scripts/sync-embedded-skills.sh +++ b/scripts/sync-embedded-skills.sh @@ -25,6 +25,7 @@ EMBED_DIR="${REPO_ROOT}/control-plane/internal/skillkit/skill_data" # Skills to mirror. Add new skills here when they're added to the catalog. SKILLS=( "agentfield" + "agentfield-use" ) CHECK_ONLY=0 diff --git a/sdk/python/agentfield/harness/_cli.py b/sdk/python/agentfield/harness/_cli.py index 63e4be44e..b9aceab81 100644 --- a/sdk/python/agentfield/harness/_cli.py +++ b/sdk/python/agentfield/harness/_cli.py @@ -6,6 +6,7 @@ import json import os import re +import shutil import signal from typing import Any, Dict, List, Optional, Tuple @@ -20,6 +21,24 @@ def strip_ansi(text: str) -> str: return _ANSI_RE.sub("", text) +def resolve_cli_command(name: str) -> str: + """Resolve a bare CLI name to a spawnable path on Windows. + + ``create_subprocess_exec`` uses CreateProcess, which does no PATHEXT + resolution — npm-installed CLIs (opencode, codex, gemini) exist on + Windows PATH only as ``.cmd``/``.ps1`` shims, so spawning the bare name + raises FileNotFoundError even though the shell finds it. ``shutil.which`` + honors PATHEXT; the resolved full path (CreateProcess runs ``.cmd`` files + via cmd.exe when given the real path) spawns fine. Names that already + carry a path separator, and anything on POSIX, pass through untouched. + """ + if os.name != "nt": + return name + if os.sep in name or "/" in name: + return name + return shutil.which(name) or name + + def _resolve_idle_seconds(idle_seconds: Optional[float]) -> Optional[float]: """Resolve the no-progress watchdog window. @@ -55,6 +74,22 @@ async def _drain( last_activity[0] = asyncio.get_event_loop().time() +async def _feed_stdin(proc: asyncio.subprocess.Process, data: bytes) -> None: + """Write data to the child's stdin and close it, tolerating early exits.""" + if proc.stdin is None: + return + try: + proc.stdin.write(data) + await proc.stdin.drain() + except (BrokenPipeError, ConnectionResetError, OSError): + pass + finally: + try: + proc.stdin.close() + except OSError: + pass + + async def run_cli( cmd: List[str], *, @@ -62,6 +97,7 @@ async def run_cli( cwd: Optional[str] = None, timeout: Optional[float] = None, idle_seconds: Optional[float] = None, + input_text: Optional[str] = None, ) -> Tuple[str, str, int]: """Run a CLI command async. Returns (stdout, stderr, returncode). @@ -70,6 +106,11 @@ async def run_cli( ``AGENTFIELD_HARNESS_IDLE_SECONDS``, default 120s; <= 0 disables), the process group is killed and ``TimeoutError`` is raised. ``timeout`` remains the outer wall-clock bound. + + ``input_text`` is written to the child's stdin (UTF-8) and stdin is closed; + without it stdin is /dev/null. Providers use this to hand over prompts too + large for a command line — on Windows an npm ``.cmd`` shim runs via + cmd.exe, which caps the command line at ~8k characters. """ merged_env = {**os.environ} if env: @@ -79,8 +120,11 @@ async def run_cli( idle = _resolve_idle_seconds(idle_seconds) proc = await asyncio.create_subprocess_exec( - *cmd, - stdin=asyncio.subprocess.DEVNULL, + resolve_cli_command(cmd[0]), + *cmd[1:], + stdin=asyncio.subprocess.PIPE + if input_text is not None + else asyncio.subprocess.DEVNULL, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=merged_env, @@ -92,15 +136,20 @@ async def run_cli( stderr_chunks: List[bytes] = [] last_activity = [asyncio.get_event_loop().time()] - # Drain both pipes concurrently to avoid a pipe-buffer deadlock. - drain = asyncio.gather( + # Pump all pipes concurrently to avoid a pipe-buffer deadlock (a large + # stdin feed must not block stdout/stderr reads, and vice versa). + pumps = [ _drain(proc.stdout, stdout_chunks, last_activity), _drain(proc.stderr, stderr_chunks, last_activity), - ) + ] + if input_text is not None: + pumps.append(_feed_stdin(proc, input_text.encode("utf-8"))) + drain = asyncio.gather(*pumps) def _kill_group() -> None: pid = proc.pid - if isinstance(pid, int) and pid > 0: + # killpg only exists on POSIX; on Windows fall through to proc.kill(). + if hasattr(os, "killpg") and isinstance(pid, int) and pid > 0: try: os.killpg(pid, signal.SIGKILL) return @@ -150,9 +199,7 @@ def _kill_group() -> None: await proc.wait() if idle_timed_out: - raise TimeoutError( - f"CLI command made no progress for {idle}s: {' '.join(cmd)}" - ) + raise TimeoutError(f"CLI command made no progress for {idle}s: {' '.join(cmd)}") if timed_out: raise TimeoutError(f"CLI command timed out after {timeout}s: {' '.join(cmd)}") diff --git a/sdk/python/agentfield/harness/providers/opencode.py b/sdk/python/agentfield/harness/providers/opencode.py index 754fac187..b122d3825 100644 --- a/sdk/python/agentfield/harness/providers/opencode.py +++ b/sdk/python/agentfield/harness/providers/opencode.py @@ -35,6 +35,18 @@ ) +def _prompt_via_stdin() -> bool: + """Whether to hand the prompt to opencode over stdin instead of argv. + + On Windows the CLI on PATH is usually an npm .cmd shim that runs via + cmd.exe, whose ~8k command-line cap real prompts blow straight through + ("The command line is too long."). opencode reads the prompt from stdin + when the positional arg is absent, so feed it that way there. POSIX keeps + the battle-tested positional-arg path. + """ + return os.name == "nt" + + def _count_turns_from_events(events: list[dict[str, object]]) -> int: """Count opencode turns from JSON events. @@ -184,8 +196,11 @@ async def _execute_impl(self, prompt: str, options: dict[str, object]) -> RawRes f"---\n\nUSER REQUEST:\n{prompt}" ) - # Prompt is a positional arg to `opencode run` (not -p) - cmd.append(effective_prompt) + # Prompt is a positional arg to `opencode run` (not -p) on POSIX; on + # Windows it goes over stdin instead (see _prompt_via_stdin). + prompt_via_stdin = _prompt_via_stdin() + if not prompt_via_stdin: + cmd.append(effective_prompt) env: Dict[str, str] = {} env_value = options.get("env") @@ -239,7 +254,11 @@ async def _execute_impl(self, prompt: str, options: dict[str, object]) -> RawRes try: try: stdout, stderr, returncode = await run_cli( - cmd, env=env, cwd=cwd, timeout=timeout_seconds + cmd, + env=env, + cwd=cwd, + timeout=timeout_seconds, + input_text=effective_prompt if prompt_via_stdin else None, ) except FileNotFoundError: return RawResult( diff --git a/sdk/python/tests/test_harness_cli.py b/sdk/python/tests/test_harness_cli.py index d9bee978f..d91866d24 100644 --- a/sdk/python/tests/test_harness_cli.py +++ b/sdk/python/tests/test_harness_cli.py @@ -177,3 +177,62 @@ def test_estimate_cli_cost_returns_none_when_litellm_raises(): ) assert cost is None + + +def test_resolve_cli_command_passthrough_on_posix(): + from agentfield.harness._cli import resolve_cli_command + + with patch("agentfield.harness._cli.os") as mock_os: + mock_os.name = "posix" + assert resolve_cli_command("opencode") == "opencode" + + +def test_resolve_cli_command_resolves_bare_names_on_windows(): + # On Windows CreateProcess does no PATHEXT resolution, so bare names of + # npm-installed CLIs (.cmd shims) must be resolved via shutil.which. + from agentfield.harness import _cli + + with ( + patch.object(_cli.os, "name", "nt"), + patch.object(_cli.os, "sep", "\\"), + patch.object(_cli.shutil, "which", return_value="C:\npm\opencode.CMD") as which, + ): + assert _cli.resolve_cli_command("opencode") == "C:\npm\opencode.CMD" + which.assert_called_once_with("opencode") + # Explicit paths pass through untouched — never re-resolved. + assert _cli.resolve_cli_command("C:\tools\opencode.exe") == ( + "C:\tools\opencode.exe" + ) + # Unresolvable names fall through so the spawn error names the input. + which.return_value = None + which.reset_mock() + assert _cli.resolve_cli_command("nonexistent") == "nonexistent" + + +@pytest.mark.asyncio +async def test_run_cli_feeds_input_text_via_stdin(): + process = MagicMock() + process.stdout = _stream_reader([b"OK"]) + process.stderr = _stream_reader([]) + process.stdin = MagicMock() + process.stdin.drain = AsyncMock() + process.returncode = 0 + process.wait = AsyncMock(return_value=0) + + create_process = AsyncMock(return_value=process) + + with patch("asyncio.create_subprocess_exec", create_process): + stdout, _, returncode = await run_cli( + ["opencode", "run"], + timeout=1, + input_text="a prompt too large for a cmd.exe command line", + ) + + assert stdout == "OK" + assert returncode == 0 + _, kwargs = create_process.call_args + assert kwargs["stdin"] is asyncio.subprocess.PIPE + process.stdin.write.assert_called_once_with( + b"a prompt too large for a cmd.exe command line" + ) + process.stdin.close.assert_called_once() diff --git a/sdk/python/tests/test_harness_provider_opencode.py b/sdk/python/tests/test_harness_provider_opencode.py index d9d7a9659..6f9c2ca38 100644 --- a/sdk/python/tests/test_harness_provider_opencode.py +++ b/sdk/python/tests/test_harness_provider_opencode.py @@ -12,14 +12,28 @@ from agentfield.types import HarnessConfig +@pytest.fixture(autouse=True) +def _argv_prompt_path(monkeypatch: pytest.MonkeyPatch): + """Pin the POSIX CLI shape (prompt as positional arg). + + The provider hands the prompt over stdin on Windows (cmd.exe's ~8k + command-line cap); these tests assert the argv shape, so the platform + decision is pinned to keep them deterministic on Windows dev machines. + The stdin path has its own test below. + """ + monkeypatch.setattr( + "agentfield.harness.providers.opencode._prompt_via_stdin", lambda: False + ) + + @pytest.mark.asyncio async def test_opencode_provider_constructs_command_and_maps_result( monkeypatch: pytest.MonkeyPatch, ): captured: dict[str, Any] = {} - async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None): - _ = timeout + async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None, input_text=None): + _ = timeout, input_text captured["cmd"] = cmd captured["env"] = env captured["cwd"] = cwd @@ -110,7 +124,7 @@ def test_factory_builds_opencode_provider_with_config_bin() -> None: async def test_opencode_passes_model_flag(monkeypatch: pytest.MonkeyPatch): captured: dict[str, Any] = {} - async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None): + async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None, input_text=None): _ = timeout captured["cmd"] = cmd captured["env"] = env @@ -138,7 +152,7 @@ async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None): async def test_opencode_cost_flows_through_metrics(monkeypatch: pytest.MonkeyPatch): """When model is provided, estimated cost populates metrics.total_cost_usd.""" - async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None): + async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None, input_text=None): _ = (env, cwd, timeout) return "result text\n", "", 0 @@ -169,7 +183,7 @@ async def test_opencode_cost_prefers_stream_cost_when_present( ] ) - async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None): + async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None, input_text=None): _ = (cmd, env, cwd, timeout) return stdout, "", 0 @@ -191,7 +205,7 @@ async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None): async def test_opencode_cost_none_without_model(monkeypatch: pytest.MonkeyPatch): """Without a model, cost estimation returns None (not 0).""" - async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None): + async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None, input_text=None): _ = (env, cwd, timeout) return "result text\n", "", 0 @@ -211,7 +225,9 @@ async def test_opencode_command_does_not_use_attach_pattern( """Verify the provider uses direct CLI pattern, NOT serve+attach workaround.""" captured_cmd: list[str] | None = None - async def capture_cmd(cmd: list[str], *, env=None, cwd=None, timeout=None): + async def capture_cmd( + cmd: list[str], *, env=None, cwd=None, timeout=None, input_text=None + ): nonlocal captured_cmd captured_cmd = cmd return "result", "", 0 @@ -237,7 +253,9 @@ async def test_opencode_uses_project_dir_when_no_cwd( """Verify project_dir is used as --dir argument when cwd is not provided.""" captured_cmd: list[str] | None = None - async def capture_cmd(cmd: list[str], *, env=None, cwd=None, timeout=None): + async def capture_cmd( + cmd: list[str], *, env=None, cwd=None, timeout=None, input_text=None + ): nonlocal captured_cmd captured_cmd = cmd return "result", "", 0 @@ -263,7 +281,9 @@ async def test_opencode_project_dir_takes_precedence_over_cwd( """ captured_cmd: list[str] | None = None - async def capture_cmd(cmd: list[str], *, env=None, cwd=None, timeout=None): + async def capture_cmd( + cmd: list[str], *, env=None, cwd=None, timeout=None, input_text=None + ): nonlocal captured_cmd captured_cmd = cmd return "result", "", 0 @@ -304,7 +324,7 @@ async def test_opencode_exit0_with_error_stderr_is_treated_as_failure( "Error: Model not found: minimax/minimax-m2.5.\n" ) - async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None): + async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None, input_text=None): return "", stderr_with_real_error, 0 monkeypatch.setattr("agentfield.harness.providers.opencode.run_cli", fake_run_cli) @@ -326,7 +346,7 @@ async def test_opencode_exit0_with_only_migration_stderr_is_success( ): """Migration prelude on stderr without an Error: line should NOT be a failure.""" - async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None): + async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None, input_text=None): return ( "actual model output\n", "Performing one time database migration, may take a few minutes...\n" @@ -355,7 +375,7 @@ async def test_opencode_exit0_with_json_error_event_is_treated_as_failure( ] ) - async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None): + async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None, input_text=None): _ = (cmd, env, cwd, timeout) return stdout, "", 0 @@ -379,7 +399,7 @@ async def test_opencode_exit_nonzero_uses_extracted_error_not_truncated_prelude( long_prelude = "Performing one time database migration line\n" * 30 stderr = long_prelude + "Error: AuthenticationError: bad key\n" - async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None): + async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None, input_text=None): return "", stderr, 1 monkeypatch.setattr("agentfield.harness.providers.opencode.run_cli", fake_run_cli) @@ -408,7 +428,9 @@ async def test_opencode_v14_cli_shape_no_deprecated_flags( """ captured_cmd: list[str] | None = None - async def capture_cmd(cmd: list[str], *, env=None, cwd=None, timeout=None): + async def capture_cmd( + cmd: list[str], *, env=None, cwd=None, timeout=None, input_text=None + ): nonlocal captured_cmd captured_cmd = cmd return "result", "", 0 @@ -455,7 +477,7 @@ async def test_opencode_num_turns_counts_step_start_events( ] ) - async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None): + async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None, input_text=None): _ = (cmd, env, cwd, timeout) return stdout, "", 0 @@ -483,7 +505,7 @@ async def test_opencode_extracts_result_from_text_events( ] ) - async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None): + async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None, input_text=None): _ = (cmd, env, cwd, timeout) return stdout, "", 0 @@ -510,7 +532,7 @@ async def test_opencode_accumulates_multiple_text_parts( ] ) - async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None): + async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None, input_text=None): _ = (cmd, env, cwd, timeout) return stdout, "", 0 @@ -522,3 +544,37 @@ async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None): assert raw.is_error is False assert raw.result == "first second" assert raw.metrics.num_turns == 1 + + +@pytest.mark.asyncio +async def test_opencode_windows_hands_prompt_over_stdin( + monkeypatch: pytest.MonkeyPatch, +): + """On Windows the prompt goes over stdin, never argv — npm .cmd shims run + via cmd.exe, whose ~8k command-line cap real prompts exceed.""" + captured: dict[str, Any] = {} + + async def fake_run_cli(cmd, *, env=None, cwd=None, timeout=None, input_text=None): + _ = env, cwd, timeout + captured["cmd"] = cmd + captured["input_text"] = input_text + return "ok\n", "", 0 + + monkeypatch.setattr("agentfield.harness.providers.opencode.run_cli", fake_run_cli) + monkeypatch.setattr( + "agentfield.harness.providers.opencode._prompt_via_stdin", lambda: True + ) + + provider = OpenCodeProvider() + raw = await provider.execute( + "a prompt far too large for a cmd.exe command line", + {"system_prompt": "be brief"}, + ) + + assert raw.is_error is False + # The prompt (with the system prompt folded in) went over stdin... + assert "a prompt far too large" in (captured["input_text"] or "") + assert "SYSTEM INSTRUCTIONS:" in (captured["input_text"] or "") + # ...and argv carries only the fixed flags, no positional prompt. + assert captured["cmd"][:4] == ["opencode", "run", "--format", "json"] + assert all("too large" not in part for part in captured["cmd"]) diff --git a/skills/agentfield-use/SKILL.md b/skills/agentfield-use/SKILL.md new file mode 100644 index 000000000..5defce2a6 --- /dev/null +++ b/skills/agentfield-use/SKILL.md @@ -0,0 +1,143 @@ +--- +name: agentfield-use +description: Discover and call agents already running on a local AgentField control plane. Use when the user asks to use, call, query, run, or delegate work to an installed AgentField agent (swe-planner, pr-af, sec-af, …), to list what agents or reasoners are available, or to check on an execution. Not for building new agents — that is the agentfield skill. +--- + +# Using AgentField agents + +A machine with AgentField has a **control plane** (default `http://localhost:8080`, +override via `AGENTFIELD_SERVER`) and **agent nodes** installed under +`~/.agentfield`. Each node exposes **reasoners** — typed functions you call over +HTTP. You never talk to an agent's own port: every call goes through the control +plane, which routes it, records the workflow, and returns the result. + +In local mode there is no auth. If the server has an API key configured, send it +as `X-API-Key: ` on every request. + +## The flow + +1. Health-check the control plane. +2. Discover what agents and reasoners exist. +3. Execute — async for anything nontrivial. +4. Poll (or stream) until the execution finishes. + +## 1. Is the control plane up? + +```bash +curl -s http://localhost:8080/health +``` + +Healthy: `200` with `{"status":"healthy", ...}`. Connection refused means no +control plane is running — the user can open the AgentField desktop app, or you +can start one in the background (`af server` blocks, so background it and poll +`/health` until healthy). + +## 2. Discover agents and reasoners + +```bash +curl -s "http://localhost:8080/api/v1/discovery/capabilities?include_input_schema=true" \ + | jq '.capabilities[] | {agent: .agent_id, health: .health_status, reasoners: [.reasoners[].id]}' +``` + +This is the durable discovery endpoint. Reasoner names are `.reasoners[].id` +(NOT `.name`), and `include_input_schema=true` adds each reasoner's JSON input +schema — read it before calling so your `input` matches. + +Two gotchas: + +- The response's `invocation_target` field uses a **colon** (`agent:reasoner`). + The execute URL uses a **dot**. Build the target yourself: `.`. +- Discovery only lists agents that are **running and registered**. Installed but + stopped agents live in the local registry — check with `af ls`, start with + `af run ` (it detaches; the agent keeps running after the CLI exits). + +## 3. Call a reasoner + +Input kwargs are ALWAYS nested under `"input"` — never raw at the top level. + +**Async — the default for real work.** Returns `202` immediately: + +```bash +curl -s -X POST http://localhost:8080/api/v1/execute/async/swe-planner.plan \ + -H 'Content-Type: application/json' \ + -d '{"input": {"task": "add rate limiting to the API"}}' +# -> {"execution_id":"...", "run_id":"...", "status":"queued", ...} +``` + +**Sync — only for calls that finish fast** (hard 90s timeout, response carries +`result` directly): + +```bash +curl -s -X POST http://localhost:8080/api/v1/execute/swe-planner.plan \ + -H 'Content-Type: application/json' \ + -d '{"input": {"task": "..."}}' +``` + +## 4. Get the result + +Poll the execution until `status` is terminal (`succeeded` / `failed`, also +`cancelled` / `timeout`): + +```bash +curl -s http://localhost:8080/api/v1/executions/ \ + | jq '{status, result, error}' +``` + +Long-running agents can take minutes — poll with backoff (2s → 5s → 10s) and +tell the user what is in flight. For live progress, stream Server-Sent Events +from `GET /api/v1/executions//events`. To check several at once: +`POST /api/v1/executions/batch-status` with `{"execution_ids": [...]}`. + +There is **no** `GET /api/v1/executions` list endpoint — do not invent one. +Cancel with `POST /api/v1/executions//cancel`. + +## Sessions and multi-call work + +- `X-Session-ID: ` on execute requests groups multi-turn work; the + control plane forwards it to the agent and scopes session memory by it. +- Reuse `X-Run-ID` across several execute calls to group them into one + workflow; each response also returns its `run_id`. + +Agents share state through control-plane memory if you need to pass artifacts +around: `POST /api/v1/memory/set` with `{"key": ..., "data": , "scope": +"global"}` and `POST /api/v1/memory/get` with `{"key": ...}` (non-global scopes +resolve from the `X-Workflow-ID` / `X-Session-ID` / `X-Actor-ID` headers). + +## When things fail + +| Symptom | Meaning | Fix | +|---|---|---| +| connection refused on :8080 | control plane not running | desktop app, or background `af server` and poll `/health` | +| agent missing from discovery | node installed but not running (or not installed) | `af ls`, then `af run ` — or `af install ` | +| `missing required environment variables: X` from `af run` | required key not configured | `af secrets set X` (value via stdin/arg; `--node ` for node-scoped) — or desktop app → Agents → Keys | +| HTTP 502 with `error_message` | the agent itself errored | read `af logs `, fix, retry | +| execution stuck in `queued`/`running` | agent wedged or overloaded | `af stop && af run `, then re-submit | + +## Local ops cheat sheet (af CLI) + +```bash +af ls # installed agents + status +af run # start (detached); af stop +af logs # agent logs +af secrets set KEY # store an API key (encrypted; prompts for value) +af secrets ls # what's configured (values never shown) +af install # install a new agent node +``` + +## Audit trail + +Every execution is recorded. When provenance matters (or the user asks "what +did the agents actually do"), fetch the verifiable-credential chain for a +workflow: `GET /api/v1/did/workflow//vc-chain` (available when DID/VC +is enabled), and verify offline with `af verify audit.json`. + +## Hard rules + +- Every call goes through the control plane — never POST to an agent's own port. +- Kwargs live under `"input"`. Empty input is `{"input": {}}`. +- Async + poll for anything that might exceed a few seconds; sync is for quick + lookups only. +- Don't guess endpoints. The surface above is the contract; if something is + missing, say so instead of inventing a route. +- Building or modifying an agent (new reasoners, scaffolds, deploys) is the + **agentfield** skill's job — switch to it for that.