diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..288d94b --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,165 @@ +name: Package & Publish + +on: + workflow_run: + workflows: ["Build & Test"] + types: [completed] + +jobs: + check: + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'success' + outputs: + is_tag: ${{ steps.check_tag.outputs.is_tag }} + steps: + - id: check_tag + run: | + # workflow_run.head_branch contains the tag name for tag-triggered runs + # Build & Test triggers on tags: ['v*'], so check if the triggering ref was a tag + if [[ "${{ github.event.workflow_run.event }}" == "push" ]] && \ + [[ "${{ github.event.workflow_run.head_branch }}" =~ ^v[0-9] ]]; then + echo "is_tag=true" >> $GITHUB_OUTPUT + else + echo "is_tag=false" >> $GITHUB_OUTPUT + fi + + electron-build: + needs: check + strategy: + matrix: + include: + - os: windows-latest + rid: win-x64 + electron-args: --win --x64 + artifact-pattern: "*.exe" + - os: macos-latest + rid: osx-x64 + electron-args: --mac --x64 + artifact-pattern: "*.dmg" + - os: ubuntu-latest + rid: linux-x64 + electron-args: --linux --x64 + artifact-pattern: "*.deb" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - uses: actions/download-artifact@v4 + with: + name: devbrain-${{ matrix.rid }} + path: packages/tray/resources/bin/ + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/download-artifact@v4 + with: + name: dashboard-dist + path: packages/tray/resources/wwwroot/ + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract .NET binaries (Unix) + if: runner.os != 'Windows' + run: | + cd packages/tray/resources/bin + for f in *.tar.gz; do [ -f "$f" ] && tar xzf "$f" && rm "$f"; done + chmod +x devbrain devbrain-daemon 2>/dev/null || true + + - name: Extract .NET binaries (Windows) + if: runner.os == 'Windows' + shell: bash + run: | + cd packages/tray/resources/bin + for f in *.zip; do [ -f "$f" ] && 7z x "$f" -y && rm "$f"; done + + - name: Install dependencies + run: cd packages/tray && npm ci + + - name: Build TypeScript + run: cd packages/tray && npm run build + + - name: Build Electron package + run: cd packages/tray && npx electron-builder ${{ matrix.electron-args }} --publish never + + - uses: actions/upload-artifact@v4 + with: + name: electron-${{ matrix.rid }} + path: packages/tray/release/${{ matrix.artifact-pattern }} + + publish-homebrew: + needs: [check, electron-build] + if: needs.check.outputs.is_tag == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Update Homebrew formula + uses: Justintime50/homebrew-releaser@v1 + with: + homebrew_owner: devbrain + homebrew_tap: homebrew-tap + formula_folder: Formula + github_token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + commit_owner: devbrain-bot + commit_email: bot@devbrain.dev + install: | + bin.install "devbrain" + bin.install "devbrain-daemon" + test: | + assert_match "devbrain", shell_output("#{bin}/devbrain --version") + + publish-winget: + needs: [check, electron-build] + if: needs.check.outputs.is_tag == 'true' + runs-on: windows-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: electron-win-x64 + path: installer/ + + - name: Submit to winget + uses: vedantmgoyal9/winget-releaser@v2 + with: + identifier: DevBrain.DevBrain + installers-regex: '\.exe$' + token: ${{ secrets.WINGET_TOKEN }} + + publish-apt: + needs: [check, electron-build] + if: needs.check.outputs.is_tag == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: electron-linux-x64 + path: deb-package/ + + - name: Setup GPG + run: | + GPG_KEY_FILE="$(mktemp)" + echo "${{ secrets.APT_GPG_PRIVATE_KEY }}" > "$GPG_KEY_FILE" + chmod 600 "$GPG_KEY_FILE" + gpg --batch --import "$GPG_KEY_FILE" + rm -f "$GPG_KEY_FILE" + + - name: Update APT repository + run: | + mkdir -p apt-repo/pool/main + cp deb-package/*.deb apt-repo/pool/main/ + cd apt-repo + dpkg-scanpackages pool/main /dev/null | gzip -9c > Packages.gz + apt-ftparchive release . > Release + gpg --batch --yes --armor --detach-sign -o Release.gpg Release + gpg --batch --yes --clearsign -o InRelease Release + + - name: Publish to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./apt-repo + destination_dir: apt diff --git a/.gitignore b/.gitignore index 2f13c26..bff9870 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ wwwroot/ .env.local *.pem docs/superpowers/ +packages/tray/dist/ +packages/tray/node_modules/ +packages/tray/resources/bin/ +packages/tray/resources/wwwroot/ diff --git a/CLAUDE.md b/CLAUDE.md index acd77e2..5ad06c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,6 +28,10 @@ tests/ dashboard/ # React + TypeScript SPA (Vite), 7 pages ``` +## Build & Toolchain + +When working on Rust projects on Windows, prefer MSVC toolchain in release mode. Debug mode causes Wasmtime stack overflows. Avoid GNU toolchain as it requires gcc/dlltool which are often missing. Always check toolchain before attempting builds. + ## Build & Test Commands ```bash @@ -38,6 +42,10 @@ dotnet run --project src/DevBrain.Api/ # Run daemon (localhost:37800) dotnet run --project src/DevBrain.Cli/ -- status # Run CLI ``` +## Development Workflow + +After implementing any feature, run a full build/compile check before committing. Do not batch multiple features without intermediate build verification. For Rust: `cargo check` after each module. For .NET: `dotnet build` after each project change. + ## Architecture Rules **Dependency direction is strictly enforced by project references:** @@ -135,6 +143,18 @@ refactor: extract graph traversal into helper - **LLM daily counter** resets at midnight UTC but has no persistence across daemon restarts. - **No ICaptureAdapter interface** in Core — adapter contract is defined inline in the Capture project. +## Code Quality + +For code reviews: always check for division-by-zero, SQL injection, race conditions, bounds validation, and correct API signatures before marking a feature complete. Do not wait for user to catch these in review. + +## Deployment + +When working with Azure deployments, always verify: 1) Environment variables are set before deploy, 2) Database migrations are idempotent, 3) DNS and health check endpoints are configured. Never assume previous deploy state is clean. + +## Windows Development + +When running background services or daemons on Windows, use UseShellExecute approach rather than stream draining for process management. Always kill processes on conflicting ports before restart. + ## Security Notes - Daemon binds to `127.0.0.1` ONLY — never `0.0.0.0` diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 4291247..294da4f 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -1,5 +1,6 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; import Navigation from './components/Navigation'; +import AlertBanner from './components/AlertBanner'; import Timeline from './pages/Timeline'; import Briefings from './pages/Briefings'; import DeadEnds from './pages/DeadEnds'; @@ -9,11 +10,17 @@ import SettingsPage from './pages/SettingsPage'; import Health from './pages/Health'; import Database from './pages/Database'; import Setup from './pages/Setup'; +import Alerts from './pages/Alerts'; +import Sessions from './pages/Sessions'; +import Replay from './pages/Replay'; +import BlastRadius from './pages/BlastRadius'; +import Growth from './pages/Growth'; export default function App() { return ( +
} /> @@ -25,6 +32,11 @@ export default function App() { } /> } /> } /> + } /> + } /> + } /> + } /> + } />
diff --git a/dashboard/src/api/client.ts b/dashboard/src/api/client.ts index b16d5ca..74bfe78 100644 --- a/dashboard/src/api/client.ts +++ b/dashboard/src/api/client.ts @@ -185,6 +185,95 @@ export interface DeadEnd { createdAt: string; } +// DejaVuAlert model +export interface DejaVuAlert { + id: string; + threadId: string; + matchedDeadEndId: string; + confidence: number; + message: string; + strategy: string; + dismissed: boolean; + createdAt: string; +} + +// BlastRadius model +export interface BlastRadiusEntry { + filePath: string; + riskScore: number; + chainLength: number; + reason: string; + linkedDecisionId?: string; +} + +export interface BlastRadius { + sourceFile: string; + affectedFiles: BlastRadiusEntry[]; + deadEndsAtRisk: string[]; + summary?: string; + generatedAt: string; +} + +// DecisionChain model +export interface DecisionStep { + observationId: string; + summary: string; + timestamp: string; + stepType: string; + filesInvolved: string[]; +} + +export interface DecisionChain { + id: string; + rootNodeId: string; + narrative: string; + steps: DecisionStep[]; + generatedAt: string; +} + +// SessionSummary model +export interface SessionSummary { + id: string; + sessionId: string; + narrative: string; + outcome: string; + duration: string; + observationCount: number; + filesTouched: number; + deadEndsHit: number; + phases: string[]; + createdAt: string; +} + +// Growth Tracker models +export interface DeveloperMetric { + id: string; + dimension: string; + value: number; + periodStart: string; + periodEnd: string; + createdAt: string; +} + +export interface GrowthMilestone { + id: string; + type: string; + description: string; + achievedAt: string; + observationId?: string; + createdAt: string; +} + +export interface GrowthReport { + id: string; + periodStart: string; + periodEnd: string; + metrics: DeveloperMetric[]; + milestones: GrowthMilestone[]; + narrative?: string; + generatedAt: string; +} + // Database explorer types export interface DbTableInfo { name: string; @@ -331,6 +420,45 @@ export const api = { return fetchJson(`/dead-ends${qs ? `?${qs}` : ''}`); }, + // Alerts + alerts: () => fetchJson('/alerts'), + + alertsAll: () => fetchJson('/alerts/all'), + + alertDismiss: async (id: string) => { + const res = await fetch(`${BASE_URL}/alerts/${encodeURIComponent(id)}/dismiss`, { + method: 'POST', + }); + if (!res.ok) throw new Error(`API error ${res.status}: ${res.statusText}`); + }, + + // Blast Radius + blastRadius: (path: string, hops = 3) => + fetchJson(`/blast-radius/${encodeURIComponent(path)}?hops=${hops}`), + + // Decision Replay + replayFile: (path: string) => + fetchJson(`/replay/file/${encodeURIComponent(path)}`), + + replayDecision: (nodeId: string) => + fetchJson(`/replay/decision/${encodeURIComponent(nodeId)}`), + + // Growth + growth: () => fetchJson('/growth'), + + growthHistory: (dimension: string, weeks = 12) => + fetchJson(`/growth/history?dimension=${encodeURIComponent(dimension)}&weeks=${weeks}`), + + growthMilestones: (limit = 50) => + fetchJson(`/growth/milestones?limit=${limit}`), + + // Sessions + sessions: (limit = 50) => + fetchJson(`/sessions?limit=${limit}`), + + sessionStory: (id: string) => + fetchJson(`/sessions/${encodeURIComponent(id)}/story`), + // Context fileContext: (path: string) => fetchJson(`/context/file/${encodeURIComponent(path)}`), diff --git a/dashboard/src/components/AlertBanner.tsx b/dashboard/src/components/AlertBanner.tsx new file mode 100644 index 0000000..befb991 --- /dev/null +++ b/dashboard/src/components/AlertBanner.tsx @@ -0,0 +1,61 @@ +import { useEffect, useState } from 'react'; +import { api } from '../api/client'; +import { useNavigate } from 'react-router-dom'; + +export default function AlertBanner() { + const [count, setCount] = useState(0); + const [message, setMessage] = useState(''); + const navigate = useNavigate(); + + useEffect(() => { + const check = () => { + api.alerts().then((alerts) => { + setCount(alerts.length); + if (alerts.length > 0) setMessage(alerts[0].message); + }).catch(() => {}); + }; + + check(); + const interval = setInterval(check, 10000); + return () => clearInterval(interval); + }, []); + + if (count === 0) return null; + + return ( +
navigate('/alerts')}> + ! + + {count} active alert{count !== 1 ? 's' : ''} + {message && ` — ${message.slice(0, 80)}${message.length > 80 ? '...' : ''}`} + +
+ ); +} + +const styles: Record = { + banner: { + background: '#7c2d12', + color: '#fbbf24', + padding: '0.5rem 1.5rem', + fontSize: '0.85rem', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + gap: '0.75rem', + }, + icon: { + background: '#fbbf24', + color: '#7c2d12', + borderRadius: '50%', + width: '1.2rem', + height: '1.2rem', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontWeight: 800, + fontSize: '0.75rem', + flexShrink: 0, + }, + text: { lineHeight: 1.3 }, +}; diff --git a/dashboard/src/components/Navigation.tsx b/dashboard/src/components/Navigation.tsx index e13dff1..c2587e2 100644 --- a/dashboard/src/components/Navigation.tsx +++ b/dashboard/src/components/Navigation.tsx @@ -4,6 +4,11 @@ const links = [ { to: '/', label: 'Timeline' }, { to: '/briefings', label: 'Briefings' }, { to: '/dead-ends', label: 'Dead Ends' }, + { to: '/alerts', label: 'Alerts' }, + { to: '/sessions', label: 'Sessions' }, + { to: '/replay', label: 'Replay' }, + { to: '/blast-radius', label: 'Blast Radius' }, + { to: '/growth', label: 'Growth' }, { to: '/threads', label: 'Threads' }, { to: '/search', label: 'Search' }, { to: '/settings', label: 'Settings' }, diff --git a/dashboard/src/pages/Alerts.tsx b/dashboard/src/pages/Alerts.tsx new file mode 100644 index 0000000..6312d60 --- /dev/null +++ b/dashboard/src/pages/Alerts.tsx @@ -0,0 +1,180 @@ +import { useEffect, useState, useCallback } from 'react'; +import { api, type DejaVuAlert } from '../api/client'; + +export default function Alerts() { + const [alerts, setAlerts] = useState([]); + const [showDismissed, setShowDismissed] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const loadAlerts = useCallback(() => { + setLoading(true); + const fetcher = showDismissed ? api.alertsAll : api.alerts; + fetcher() + .then((data) => { + setAlerts(data); + setLoading(false); + }) + .catch((e) => { + setError(String(e)); + setLoading(false); + }); + }, [showDismissed]); + + useEffect(() => { + loadAlerts(); + }, [loadAlerts]); + + const handleDismiss = async (id: string) => { + try { + await api.alertDismiss(id); + loadAlerts(); + } catch (e) { + setError(String(e)); + } + }; + + if (error) return
Error: {error}
; + if (loading) return
Loading alerts...
; + + const activeCount = alerts.filter((a) => !a.dismissed).length; + + return ( +
+

Deja Vu Alerts

+ +
+ + {activeCount} active alert{activeCount !== 1 ? 's' : ''} + + +
+ + {alerts.length === 0 && ( +

+ {showDismissed + ? 'No alerts recorded yet.' + : "No active alerts. You're in the clear!"} +

+ )} + +
+ {alerts.map((alert) => ( +
+
+
+ + {alert.dismissed ? 'Dismissed' : 'Active'} + + {alert.strategy} + + {Math.round(alert.confidence * 100)}% match + +
+ + {new Date(alert.createdAt).toLocaleString()} + +
+ +
{alert.message}
+ + {!alert.dismissed && ( + + )} +
+ ))} +
+
+ ); +} + +const styles: Record = { + container: { padding: '1.5rem', maxWidth: 800, margin: '0 auto' }, + controls: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: '1.5rem', + }, + count: { fontSize: '0.9rem', color: '#fbbf24', fontWeight: 600 }, + toggle: { fontSize: '0.85rem', color: '#9ca3af', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '0.4rem' }, + list: { display: 'flex', flexDirection: 'column', gap: '0.75rem' }, + card: { + background: '#1f2028', + borderRadius: 8, + padding: '1rem', + border: '1px solid #7c2d12', + }, + cardDismissed: { + opacity: 0.5, + borderColor: '#2e303a', + }, + cardHeader: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: '0.75rem', + flexWrap: 'wrap' as const, + gap: '0.5rem', + }, + badges: { display: 'flex', gap: '0.5rem', alignItems: 'center' }, + badge: { + fontSize: '0.7rem', + padding: '2px 8px', + borderRadius: 4, + fontWeight: 600, + textTransform: 'uppercase' as const, + }, + strategyBadge: { + fontSize: '0.7rem', + background: '#1e293b', + color: '#60a5fa', + padding: '2px 8px', + borderRadius: 4, + fontFamily: 'monospace', + }, + confidence: { fontSize: '0.8rem', color: '#9ca3af' }, + date: { fontSize: '0.8rem', color: '#6b7280' }, + message: { + color: '#f3f4f6', + fontSize: '0.9rem', + lineHeight: 1.5, + }, + dismissBtn: { + marginTop: '0.75rem', + padding: '0.35rem 1rem', + background: '#374151', + color: '#d1d5db', + border: '1px solid #4b5563', + borderRadius: 4, + cursor: 'pointer', + fontSize: '0.8rem', + }, + loading: { padding: '2rem', textAlign: 'center' as const, color: '#9ca3af' }, + error: { padding: '2rem', textAlign: 'center' as const, color: '#ef4444' }, + empty: { color: '#6b7280', textAlign: 'center' as const, padding: '2rem' }, +}; diff --git a/dashboard/src/pages/BlastRadius.tsx b/dashboard/src/pages/BlastRadius.tsx new file mode 100644 index 0000000..9ae2120 --- /dev/null +++ b/dashboard/src/pages/BlastRadius.tsx @@ -0,0 +1,193 @@ +import { useState } from 'react'; +import { api, type BlastRadius as BlastRadiusType } from '../api/client'; + +function riskColor(score: number): string { + if (score > 0.7) return '#ef4444'; + if (score > 0.3) return '#eab308'; + return '#22c55e'; +} + +export default function BlastRadius() { + const [query, setQuery] = useState(''); + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const search = async () => { + if (!query.trim()) return; + setLoading(true); + setError(null); + setResult(null); + + try { + const data = await api.blastRadius(query.trim()); + setResult(data); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setLoading(false); + } + }; + + return ( +
+

Blast Radius

+

+ Enter a file path to see what else might break if you change it — + based on decision dependencies, not just code imports. +

+ +
+ setQuery(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && search()} + style={styles.input} + /> + +
+ + {error &&
{error}
} + + {result && ( +
+ {result.deadEndsAtRisk.length > 0 && ( +
+ {result.deadEndsAtRisk.length} dead end(s) at risk of + re-triggering +
+ )} + + {result.affectedFiles.length === 0 ? ( +

+ No affected files found. Safe to change! +

+ ) : ( +
+
+ {result.affectedFiles.length} affected file(s) +
+ {result.affectedFiles.map((file) => ( +
+
+
+
+ + {(file.riskScore * 100).toFixed(0)}% + +
+ + chain: {file.chainLength} + +
+
{file.filePath}
+
{file.reason}
+
+ ))} +
+ )} +
+ )} +
+ ); +} + +const styles: Record = { + container: { padding: '1.5rem', maxWidth: 800, margin: '0 auto' }, + subtitle: { color: '#9ca3af', fontSize: '0.9rem', marginBottom: '1.5rem' }, + searchRow: { display: 'flex', gap: '0.5rem', marginBottom: '1.5rem' }, + input: { + flex: 1, + padding: '0.5rem 0.75rem', + background: '#1f2028', + border: '1px solid #2e303a', + borderRadius: 6, + color: '#f3f4f6', + fontSize: '0.9rem', + fontFamily: 'monospace', + outline: 'none', + }, + searchBtn: { + padding: '0.5rem 1.25rem', + background: '#2a2a4a', + color: '#a5b4fc', + border: '1px solid #3b3b6b', + borderRadius: 6, + cursor: 'pointer', + fontSize: '0.9rem', + }, + error: { color: '#ef4444', marginBottom: '1rem' }, + deadEndWarning: { + background: '#7c2d12', + color: '#fbbf24', + padding: '0.75rem 1rem', + borderRadius: 6, + marginBottom: '1rem', + fontSize: '0.9rem', + fontWeight: 600, + }, + safe: { + color: '#22c55e', + textAlign: 'center' as const, + padding: '2rem', + fontSize: '1.1rem', + }, + fileList: {}, + fileListHeader: { + color: '#9ca3af', + fontSize: '0.85rem', + marginBottom: '0.75rem', + }, + fileCard: { + background: '#1f2028', + borderRadius: 8, + padding: '0.75rem 1rem', + border: '1px solid #2e303a', + marginBottom: '0.5rem', + }, + fileHeader: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: '0.35rem', + }, + riskBadgeContainer: { + display: 'flex', + alignItems: 'center', + gap: '0.5rem', + flex: 1, + }, + riskBar: { + height: 6, + borderRadius: 3, + maxWidth: 100, + }, + riskScore: { fontSize: '0.8rem', fontWeight: 700 }, + chainLength: { + fontSize: '0.75rem', + color: '#6b7280', + fontFamily: 'monospace', + }, + filePath: { + color: '#a5b4fc', + fontFamily: 'monospace', + fontSize: '0.85rem', + marginBottom: '0.25rem', + }, + reason: { color: '#9ca3af', fontSize: '0.8rem' }, +}; diff --git a/dashboard/src/pages/Growth.tsx b/dashboard/src/pages/Growth.tsx new file mode 100644 index 0000000..a4c6de5 --- /dev/null +++ b/dashboard/src/pages/Growth.tsx @@ -0,0 +1,160 @@ +import { useEffect, useState } from 'react'; +import { api, type GrowthReport, type GrowthMilestone } from '../api/client'; + +const milestoneColors: Record = { + First: '#3b82f6', + Streak: '#eab308', + Improvement: '#22c55e', +}; + +export default function Growth() { + const [report, setReport] = useState(null); + const [milestones, setMilestones] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + Promise.all([ + api.growth().catch(() => null), + api.growthMilestones().catch(() => []), + ]) + .then(([reportData, milestonesData]) => { + if (reportData && 'id' in reportData) setReport(reportData); + setMilestones(milestonesData as GrowthMilestone[]); + setLoading(false); + }) + .catch((e) => { + setError(String(e)); + setLoading(false); + }); + }, []); + + if (error) return
Error: {error}
; + if (loading) return
Loading growth data...
; + + return ( +
+

Developer Growth

+ + {/* Narrative */} + {report?.narrative && ( +
+
This Week
+
{report.narrative}
+
+ )} + + {/* Metrics */} + {report?.metrics && report.metrics.length > 0 && ( +
+ {report.metrics.map((m) => ( +
+
{m.dimension.replace(/_/g, ' ')}
+
{m.value.toFixed(2)}
+
+ ))} +
+ )} + + {!report && ( +

+ No growth reports yet. The growth agent runs weekly (Monday 8 AM). +

+ )} + + {/* Milestones */} +

Milestones

+ {milestones.length === 0 ? ( +

No milestones yet.

+ ) : ( +
+ {milestones.map((m) => ( +
+ + {m.type} + + {m.description} + + {new Date(m.achievedAt).toLocaleDateString()} + +
+ ))} +
+ )} +
+ ); +} + +const styles: Record = { + container: { padding: '1.5rem', maxWidth: 800, margin: '0 auto' }, + narrativeCard: { + background: '#1f2028', + borderRadius: 8, + padding: '1.25rem', + border: '1px solid #22c55e33', + marginBottom: '1.5rem', + }, + narrativeLabel: { + fontSize: '0.75rem', + color: '#22c55e', + fontWeight: 700, + textTransform: 'uppercase' as const, + marginBottom: '0.5rem', + }, + narrative: { color: '#e5e7eb', fontSize: '0.95rem', lineHeight: 1.5 }, + metricsGrid: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', + gap: '0.75rem', + marginBottom: '2rem', + }, + metricCard: { + background: '#1f2028', + borderRadius: 8, + padding: '1rem', + border: '1px solid #2e303a', + textAlign: 'center' as const, + }, + metricDimension: { + fontSize: '0.75rem', + color: '#9ca3af', + textTransform: 'capitalize' as const, + marginBottom: '0.5rem', + }, + metricValue: { + fontSize: '1.5rem', + fontWeight: 700, + color: '#f3f4f6', + fontFamily: 'monospace', + }, + sectionTitle: { fontSize: '1.1rem', color: '#d1d5db', marginBottom: '1rem' }, + milestoneList: { display: 'flex', flexDirection: 'column', gap: '0.5rem' }, + milestoneCard: { + display: 'flex', + alignItems: 'center', + gap: '0.75rem', + padding: '0.5rem 0.75rem', + background: '#1f2028', + borderRadius: 6, + border: '1px solid #2e303a', + }, + milestoneBadge: { + fontSize: '0.65rem', + color: '#fff', + padding: '2px 8px', + borderRadius: 4, + fontWeight: 700, + textTransform: 'uppercase' as const, + flexShrink: 0, + }, + milestoneDesc: { flex: 1, color: '#e5e7eb', fontSize: '0.85rem' }, + milestoneDate: { fontSize: '0.75rem', color: '#6b7280', flexShrink: 0 }, + loading: { padding: '2rem', textAlign: 'center' as const, color: '#9ca3af' }, + error: { padding: '2rem', textAlign: 'center' as const, color: '#ef4444' }, + empty: { color: '#6b7280', textAlign: 'center' as const, padding: '1rem' }, +}; diff --git a/dashboard/src/pages/Replay.tsx b/dashboard/src/pages/Replay.tsx new file mode 100644 index 0000000..063b6bb --- /dev/null +++ b/dashboard/src/pages/Replay.tsx @@ -0,0 +1,201 @@ +import { useState } from 'react'; +import { api, type DecisionChain } from '../api/client'; + +const stepColors: Record = { + Decision: '#22c55e', + DeadEnd: '#ef4444', + Error: '#eab308', + Resolution: '#3b82f6', +}; + +export default function Replay() { + const [query, setQuery] = useState(''); + const [chain, setChain] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const search = async () => { + if (!query.trim()) return; + setLoading(true); + setError(null); + setChain(null); + + try { + const result = await api.replayFile(query.trim()); + setChain(result); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + setError( + msg.includes('404') || msg.includes('Not Found') + ? `No decision chain found for '${query}'` + : `Error: ${msg}` + ); + } finally { + setLoading(false); + } + }; + + return ( +
+

Decision Replay

+

+ Enter a file path to see why it exists — the full chain of decisions, + dead ends, and resolutions. +

+ +
+ setQuery(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && search()} + style={styles.input} + /> + +
+ + {error &&
{error}
} + + {chain && ( +
+
{chain.narrative}
+ +
+ {chain.steps.map((step, i) => ( +
+
+
+ {i < chain.steps.length - 1 && ( +
+ )} +
+
+
+ + {step.stepType} + + + {new Date(step.timestamp).toLocaleString()} + +
+
{step.summary}
+ {step.filesInvolved.length > 0 && ( +
+ {step.filesInvolved.map((f) => ( + + {f} + + ))} +
+ )} +
+
+ ))} +
+
+ )} +
+ ); +} + +const styles: Record = { + container: { padding: '1.5rem', maxWidth: 800, margin: '0 auto' }, + subtitle: { color: '#9ca3af', fontSize: '0.9rem', marginBottom: '1.5rem' }, + searchRow: { display: 'flex', gap: '0.5rem', marginBottom: '1.5rem' }, + input: { + flex: 1, + padding: '0.5rem 0.75rem', + background: '#1f2028', + border: '1px solid #2e303a', + borderRadius: 6, + color: '#f3f4f6', + fontSize: '0.9rem', + fontFamily: 'monospace', + outline: 'none', + }, + searchBtn: { + padding: '0.5rem 1.25rem', + background: '#2a2a4a', + color: '#a5b4fc', + border: '1px solid #3b3b6b', + borderRadius: 6, + cursor: 'pointer', + fontSize: '0.9rem', + }, + error: { color: '#ef4444', marginBottom: '1rem' }, + result: {}, + narrative: { + padding: '1rem', + background: '#161620', + borderRadius: 6, + color: '#d1d5db', + fontSize: '0.9rem', + lineHeight: 1.5, + marginBottom: '1.5rem', + }, + timeline: { display: 'flex', flexDirection: 'column' }, + step: { display: 'flex', gap: '1rem' }, + stepLine: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + width: 20, + flexShrink: 0, + }, + stepDot: { + width: 12, + height: 12, + borderRadius: '50%', + flexShrink: 0, + }, + stepConnector: { + width: 2, + flex: 1, + background: '#374151', + minHeight: 30, + }, + stepContent: { + flex: 1, + paddingBottom: '1.5rem', + }, + stepHeader: { + display: 'flex', + gap: '0.75rem', + alignItems: 'center', + marginBottom: '0.25rem', + }, + stepType: { + fontSize: '0.75rem', + fontWeight: 700, + textTransform: 'uppercase' as const, + }, + stepTime: { fontSize: '0.75rem', color: '#6b7280' }, + stepSummary: { color: '#f3f4f6', fontSize: '0.9rem', lineHeight: 1.4 }, + stepFiles: { + display: 'flex', + flexWrap: 'wrap' as const, + gap: '0.3rem', + marginTop: '0.4rem', + }, + fileTag: { + fontSize: '0.7rem', + background: '#2a2a4a', + color: '#a5b4fc', + padding: '2px 6px', + borderRadius: 3, + fontFamily: 'monospace', + }, +}; diff --git a/dashboard/src/pages/Sessions.tsx b/dashboard/src/pages/Sessions.tsx new file mode 100644 index 0000000..05ac1c3 --- /dev/null +++ b/dashboard/src/pages/Sessions.tsx @@ -0,0 +1,207 @@ +import { useEffect, useState } from 'react'; +import { api, type SessionSummary } from '../api/client'; + +const phaseColors: Record = { + Exploration: '#3b82f6', + Implementation: '#22c55e', + Debugging: '#ef4444', + Refactoring: '#eab308', +}; + +export default function Sessions() { + const [sessions, setSessions] = useState([]); + const [expanded, setExpanded] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + api + .sessions() + .then((data) => { + setSessions(data); + setLoading(false); + }) + .catch((e) => { + setError(String(e)); + setLoading(false); + }); + }, []); + + const copyMarkdown = (session: SessionSummary) => { + const md = `## Session Story\n\n${session.narrative}\n\n**Outcome:** ${session.outcome}\n\n_${session.observationCount} observations | ${session.filesTouched} files | ${session.deadEndsHit} dead ends_`; + navigator.clipboard.writeText(md); + }; + + if (error) return
Error: {error}
; + if (loading) return
Loading sessions...
; + + return ( +
+

Sessions

+ + {sessions.length === 0 && ( +

No session stories generated yet.

+ )} + +
+ {sessions.map((session) => ( +
+
+
+ {session.observationCount} obs + {session.filesTouched} files + {session.deadEndsHit > 0 && ( + + {session.deadEndsHit} dead ends + + )} +
+ + {new Date(session.createdAt).toLocaleDateString()} + +
+ + {/* Phase bar + labels */} + {session.phases.length > 0 && ( + <> +
+ {session.phases.map((phase, i) => ( +
+ ))} +
+
+ {session.phases.map((phase, i) => ( + + {phase} + + ))} +
+ + )} + +
{session.outcome}
+ +
+ + +
+ + {expanded === session.id && ( +
{session.narrative}
+ )} +
+ ))} +
+
+ ); +} + +const styles: Record = { + container: { padding: '1.5rem', maxWidth: 800, margin: '0 auto' }, + list: { display: 'flex', flexDirection: 'column', gap: '0.75rem' }, + card: { + background: '#1f2028', + borderRadius: 8, + padding: '1rem', + border: '1px solid #2e303a', + }, + cardHeader: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: '0.75rem', + }, + stats: { display: 'flex', gap: '0.75rem' }, + stat: { + fontSize: '0.8rem', + color: '#9ca3af', + background: '#1e293b', + padding: '2px 8px', + borderRadius: 4, + fontFamily: 'monospace', + }, + date: { fontSize: '0.8rem', color: '#6b7280' }, + phaseBar: { + display: 'flex', + height: 6, + borderRadius: 3, + overflow: 'hidden', + gap: 2, + marginBottom: '0.35rem', + }, + phaseSegment: { borderRadius: 2 }, + phaseLabels: { + display: 'flex', + gap: '0.5rem', + marginBottom: '0.75rem', + }, + phaseLabel: { + fontSize: '0.7rem', + fontWeight: 600, + textTransform: 'uppercase' as const, + }, + outcome: { + color: '#d1d5db', + fontSize: '0.9rem', + marginBottom: '0.75rem', + lineHeight: 1.4, + }, + actions: { display: 'flex', gap: '0.5rem' }, + expandBtn: { + padding: '0.35rem 1rem', + background: '#2a2a4a', + color: '#a5b4fc', + border: '1px solid #3b3b6b', + borderRadius: 4, + cursor: 'pointer', + fontSize: '0.8rem', + }, + copyBtn: { + padding: '0.35rem 1rem', + background: '#374151', + color: '#d1d5db', + border: '1px solid #4b5563', + borderRadius: 4, + cursor: 'pointer', + fontSize: '0.8rem', + }, + narrative: { + marginTop: '1rem', + padding: '1rem', + background: '#161620', + borderRadius: 6, + color: '#e5e7eb', + fontSize: '0.9rem', + lineHeight: 1.6, + whiteSpace: 'pre-wrap' as const, + }, + loading: { padding: '2rem', textAlign: 'center' as const, color: '#9ca3af' }, + error: { padding: '2rem', textAlign: 'center' as const, color: '#ef4444' }, + empty: { color: '#6b7280', textAlign: 'center' as const, padding: '2rem' }, +}; diff --git a/docs/superpowers/plans/2026-04-07-single-click-packaging.md b/docs/superpowers/plans/2026-04-07-single-click-packaging.md new file mode 100644 index 0000000..0305afe --- /dev/null +++ b/docs/superpowers/plans/2026-04-07-single-click-packaging.md @@ -0,0 +1,2114 @@ +# Single-Click Packaging Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship DevBrain as cross-platform packages (winget, brew, apt) with an Electron tray app that manages daemon lifecycle, Ollama bootstrapping, and auto-launch at login. + +**Architecture:** An Electron menubar app embeds the existing .NET daemon + CLI binaries, manages their lifecycle, and handles first-run setup (config creation, Ollama install). Package managers (winget, Homebrew, APT) distribute the Electron app as an NSIS installer, DMG, or .deb. CI/CD builds everything and publishes to package registries on tagged releases. + +**Tech Stack:** Electron 36+, TypeScript, electron-builder, Jest, GitHub Actions, Homebrew formula, winget manifest, Debian packaging. + +**Spec:** `docs/superpowers/specs/2026-04-07-single-click-packaging-design.md` + +--- + +## File Structure + +### New Files + +``` +package.json # Root npm workspaces config +packages/ + tray/ + package.json # Electron + electron-builder deps + tsconfig.json # TypeScript config (Node/ES2022) + jest.config.js # Jest config for ts-jest + electron-builder.yml # Platform build targets, autoLaunch, extraResources + src/ + main.ts # Electron entry — app lifecycle, tray creation, menu + notifications.ts # Show/update OS-native notifications + health.ts # Poll daemon health, emit state changes + daemon.ts # Spawn/stop/restart devbrain-daemon, PID management + bootstrap.ts # First-run: config, Ollama install, model pull + paths.ts # Resolve platform-specific paths (data dir, binaries, icons) + assets/ + icon.png # 256x256 tray icon (green state — base) + icon-yellow.png # Yellow state (starting/bootstrapping) + icon-red.png # Red state (stopped/error) + icon.ico # Windows .ico (multi-resolution) + icon.icns # macOS .icns + build/ + installer.nsh # NSIS script for PATH manipulation (Windows) + linux-after-install.sh # Post-install: symlinks + autostart (Linux .deb) + linux-after-remove.sh # Pre-remove: cleanup (Linux .deb) + __tests__/ + health.test.ts # Health state machine transitions + daemon.test.ts # Spawn/restart/crash logic + bootstrap.test.ts # Config creation, Ollama detection, idempotency + homebrew/ + devbrain.rb # Homebrew formula + winget/ + DevBrain.DevBrain.yaml # winget manifest (version template) + apt/ + debian/ + control # Package metadata + dependencies + postinst # Symlinks + autostart registration + prerm # Stop daemon + tray, remove autostart + rules # dpkg-buildpackage rules + devbrain.desktop # XDG autostart .desktop file +.github/ + workflows/ + package.yml # Electron build + package manager publish +``` + +### Modified Files + +``` +.gitignore # Add packages/tray/dist/, packages/tray/node_modules/ +src/DevBrain.Cli/Commands/StartCommand.cs # Add tray.lock check +src/DevBrain.Cli/Commands/StopCommand.cs # Add tray.lock check + stopped sentinel +``` + +--- + +## Task 1: Project Scaffolding — npm Workspaces + Electron Skeleton + +**Files:** +- Create: `package.json` (root) +- Create: `packages/tray/package.json` +- Create: `packages/tray/tsconfig.json` +- Modify: `.gitignore` + +- [ ] **Step 1: Create root package.json for npm workspaces** + +```json +{ + "name": "devbrain", + "private": true, + "workspaces": [ + "dashboard", + "packages/tray" + ] +} +``` + +Write to: `package.json` (project root) + +- [ ] **Step 2: Create packages/tray/package.json** + +```json +{ + "name": "devbrain-tray", + "version": "1.0.0", + "private": true, + "main": "dist/main.js", + "scripts": { + "build": "tsc", + "start": "npm run build && electron dist/main.js", + "test": "jest --config jest.config.js", + "pack": "npm run build && electron-builder --dir", + "dist": "npm run build && electron-builder" + }, + "dependencies": { + "electron-log": "^5.3.0" + }, + "devDependencies": { + "@types/node": "^24.12.2", + "electron": "^36.0.0", + "electron-builder": "^26.0.0", + "jest": "^30.0.0", + "@types/jest": "^30.0.0", + "ts-jest": "^29.3.0", + "typescript": "~6.0.2" + } +} +``` + +Write to: `packages/tray/package.json` + +- [ ] **Step 3: Create packages/tray/tsconfig.json** + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["__tests__", "dist", "node_modules"] +} +``` + +Write to: `packages/tray/tsconfig.json` + +- [ ] **Step 4: Update .gitignore** + +Add these lines to `.gitignore`: + +``` +packages/tray/dist/ +packages/tray/node_modules/ +packages/tray/resources/bin/ +packages/tray/resources/wwwroot/ +``` + +- [ ] **Step 5: Install dependencies** + +Run: `cd packages/tray && npm install` + +Expected: `node_modules/` created, no errors. + +- [ ] **Step 6: Verify workspace setup** + +Run from project root: `npm ls --workspaces` + +Expected: Lists `devbrain-dashboard` and `devbrain-tray` as workspaces. + +- [ ] **Step 7: Commit** + +```bash +git add package.json packages/tray/package.json packages/tray/tsconfig.json .gitignore +git commit -m "feat(packaging): scaffold Electron tray app with npm workspaces" +``` + +--- + +## Task 2: Platform Paths Module + +**Files:** +- Create: `packages/tray/src/paths.ts` + +This module resolves all platform-specific paths. Every other module imports from here — no hardcoded paths elsewhere. + +- [ ] **Step 1: Write paths.ts** + +```typescript +import * as path from "path"; +import * as os from "os"; +import { app } from "electron"; + +/** ~/.devbrain on all platforms */ +export function dataDir(): string { + return path.join(os.homedir(), ".devbrain"); +} + +/** ~/.devbrain/settings.toml */ +export function settingsPath(): string { + return path.join(dataDir(), "settings.toml"); +} + +/** ~/.devbrain/daemon.pid */ +export function pidPath(): string { + return path.join(dataDir(), "daemon.pid"); +} + +/** ~/.devbrain/tray.lock */ +export function trayLockPath(): string { + return path.join(dataDir(), "tray.lock"); +} + +/** ~/.devbrain/stopped — sentinel written by CLI to prevent tray auto-restart */ +export function stoppedSentinelPath(): string { + return path.join(dataDir(), "stopped"); +} + +/** ~/.devbrain/logs/ */ +export function logsDir(): string { + return path.join(dataDir(), "logs"); +} + +/** + * Resolve path to an embedded binary (devbrain-daemon or devbrain). + * In dev: looks in resources/bin/ relative to project. + * In packaged app: looks in resources/bin/ inside the asar/resources. + */ +export function binaryPath(name: string): string { + const ext = process.platform === "win32" ? ".exe" : ""; + const binaryName = `${name}${ext}`; + + if (app.isPackaged) { + return path.join(process.resourcesPath, "bin", binaryName); + } + + // Dev mode: expect binaries in resources/bin/ relative to project + return path.join(__dirname, "..", "resources", "bin", binaryName); +} + +/** + * Resolve tray icon path by state. + * @param state - "green" | "yellow" | "red" + */ +export function iconPath(state: "green" | "yellow" | "red"): string { + const suffix = state === "green" ? "" : `-${state}`; + const ext = process.platform === "win32" ? ".ico" : ".png"; + const filename = `icon${suffix}${ext}`; + + if (app.isPackaged) { + return path.join(process.resourcesPath, "assets", filename); + } + + return path.join(__dirname, "..", "assets", filename); +} +``` + +Write to: `packages/tray/src/paths.ts` + +- [ ] **Step 2: Verify it compiles** + +Run: `cd packages/tray && npx tsc --noEmit` + +Expected: No errors (Electron types resolve `app` import). + +- [ ] **Step 3: Commit** + +```bash +git add packages/tray/src/paths.ts +git commit -m "feat(packaging): add platform path resolution module" +``` + +--- + +## Task 3: Notifications Module + +**Files:** +- Create: `packages/tray/src/notifications.ts` + +Thin wrapper over Electron's `Notification` API. + +- [ ] **Step 1: Write notifications.ts** + +```typescript +import { Notification } from "electron"; + +const APP_NAME = "DevBrain"; + +export function showInfo(title: string, body: string): void { + new Notification({ title: `${APP_NAME}: ${title}`, body }).show(); +} + +export function showError(title: string, body: string): void { + new Notification({ + title: `${APP_NAME}: ${title}`, + body, + urgency: "critical", + }).show(); +} + +export function showProgress(title: string, body: string): Notification { + const n = new Notification({ title: `${APP_NAME}: ${title}`, body }); + n.show(); + return n; +} +``` + +Write to: `packages/tray/src/notifications.ts` + +- [ ] **Step 2: Verify it compiles** + +Run: `cd packages/tray && npx tsc --noEmit` + +Expected: No errors. + +- [ ] **Step 3: Commit** + +```bash +git add packages/tray/src/notifications.ts +git commit -m "feat(packaging): add OS notification helpers" +``` + +--- + +## Task 4: Health Monitor — TDD + +**Files:** +- Create: `packages/tray/jest.config.js` +- Create: `packages/tray/__tests__/health.test.ts` +- Create: `packages/tray/src/health.ts` + +The health monitor polls the daemon's `/api/v1/health` endpoint and emits state transitions. + +- [ ] **Step 1: Create Jest config** + +```javascript +module.exports = { + preset: "ts-jest", + testEnvironment: "node", + roots: ["/__tests__"], + testMatch: ["**/*.test.ts"], +}; +``` + +Write to: `packages/tray/jest.config.js` + +- [ ] **Step 2: Write the failing tests** + +```typescript +import { HealthMonitor, HealthState } from "../src/health"; + +const mockFetch = jest.fn(); +global.fetch = mockFetch as unknown as typeof fetch; + +describe("HealthMonitor", () => { + let monitor: HealthMonitor; + let states: HealthState[]; + + beforeEach(() => { + jest.useFakeTimers(); + states = []; + monitor = new HealthMonitor(1000); + monitor.on("stateChange", (s: HealthState) => states.push(s)); + mockFetch.mockReset(); + }); + + afterEach(() => { + monitor.stop(); + jest.useRealTimers(); + }); + + it("starts in 'starting' state", () => { + expect(monitor.state).toBe("starting"); + }); + + it("transitions to 'healthy' on successful health check", async () => { + mockFetch.mockResolvedValueOnce({ ok: true }); + monitor.start(); + await jest.advanceTimersByTimeAsync(1000); + expect(monitor.state).toBe("healthy"); + expect(states).toEqual(["healthy"]); + }); + + it("transitions to 'unhealthy' on failed health check", async () => { + mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED")); + monitor.start(); + await jest.advanceTimersByTimeAsync(1000); + expect(monitor.state).toBe("unhealthy"); + expect(states).toEqual(["unhealthy"]); + }); + + it("transitions healthy -> unhealthy -> healthy", async () => { + mockFetch + .mockResolvedValueOnce({ ok: true }) + .mockRejectedValueOnce(new Error("ECONNREFUSED")) + .mockResolvedValueOnce({ ok: true }); + monitor.start(); + await jest.advanceTimersByTimeAsync(1000); + await jest.advanceTimersByTimeAsync(1000); + await jest.advanceTimersByTimeAsync(1000); + expect(states).toEqual(["healthy", "unhealthy", "healthy"]); + }); + + it("does not emit duplicate states", async () => { + mockFetch + .mockResolvedValueOnce({ ok: true }) + .mockResolvedValueOnce({ ok: true }); + monitor.start(); + await jest.advanceTimersByTimeAsync(1000); + await jest.advanceTimersByTimeAsync(1000); + expect(states).toEqual(["healthy"]); + }); + + it("stop() clears the polling interval", async () => { + mockFetch.mockResolvedValue({ ok: true }); + monitor.start(); + await jest.advanceTimersByTimeAsync(1000); + monitor.stop(); + mockFetch.mockReset(); + await jest.advanceTimersByTimeAsync(5000); + expect(mockFetch).not.toHaveBeenCalled(); + }); +}); +``` + +Write to: `packages/tray/__tests__/health.test.ts` + +- [ ] **Step 3: Run tests to verify they fail** + +Run: `cd packages/tray && npx jest` + +Expected: FAIL — `Cannot find module '../src/health'` + +- [ ] **Step 4: Implement health.ts** + +```typescript +import { EventEmitter } from "events"; + +export type HealthState = "starting" | "healthy" | "unhealthy"; + +const DAEMON_URL = "http://127.0.0.1:37800/api/v1/health"; + +export class HealthMonitor extends EventEmitter { + private _state: HealthState = "starting"; + private timer: ReturnType | null = null; + private pollIntervalMs: number; + + constructor(pollIntervalMs = 5000) { + super(); + this.pollIntervalMs = pollIntervalMs; + } + + get state(): HealthState { + return this._state; + } + + start(): void { + this.timer = setInterval(() => this.check(), this.pollIntervalMs); + } + + stop(): void { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + } + + private async check(): Promise { + let newState: HealthState; + + try { + const res = await fetch(DAEMON_URL); + newState = res.ok ? "healthy" : "unhealthy"; + } catch { + newState = "unhealthy"; + } + + if (newState !== this._state) { + this._state = newState; + this.emit("stateChange", newState); + } + } +} +``` + +Write to: `packages/tray/src/health.ts` + +- [ ] **Step 5: Run tests to verify they pass** + +Run: `cd packages/tray && npx jest` + +Expected: 5 passing tests. + +- [ ] **Step 6: Commit** + +```bash +git add packages/tray/jest.config.js packages/tray/__tests__/health.test.ts packages/tray/src/health.ts +git commit -m "feat(packaging): add daemon health monitor with TDD" +``` + +--- + +## Task 5: Daemon Manager — TDD + +**Files:** +- Create: `packages/tray/__tests__/daemon.test.ts` +- Create: `packages/tray/src/daemon.ts` + +Manages spawning, stopping, and auto-restarting the daemon process. + +- [ ] **Step 1: Write the failing tests** + +```typescript +import { DaemonManager } from "../src/daemon"; +import * as child_process from "child_process"; +import * as fs from "fs"; + +jest.mock("child_process"); +jest.mock("fs"); + +const mockSpawn = child_process.spawn as jest.MockedFunction; +const mockExistsSync = fs.existsSync as jest.MockedFunction; +const mockReadFileSync = fs.readFileSync as jest.MockedFunction; +const mockWriteFileSync = fs.writeFileSync as jest.MockedFunction; +const mockUnlinkSync = fs.unlinkSync as jest.MockedFunction; +const mockMkdirSync = fs.mkdirSync as jest.MockedFunction; + +const mockFetch = jest.fn(); +global.fetch = mockFetch as unknown as typeof fetch; + +jest.mock("../src/paths", () => ({ + binaryPath: (name: string) => `/mock/bin/${name}`, + pidPath: () => "/mock/.devbrain/daemon.pid", + dataDir: () => "/mock/.devbrain", + stoppedSentinelPath: () => "/mock/.devbrain/stopped", + logsDir: () => "/mock/.devbrain/logs", +})); + +jest.mock("../src/notifications", () => ({ + showInfo: jest.fn(), + showError: jest.fn(), + showProgress: jest.fn(() => ({ close: jest.fn() })), +})); + +describe("DaemonManager", () => { + let daemon: DaemonManager; + let mockProcess: Partial; + + beforeEach(() => { + jest.clearAllMocks(); + mockProcess = { + pid: 12345, + on: jest.fn(), + unref: jest.fn(), + }; + mockSpawn.mockReturnValue(mockProcess as child_process.ChildProcess); + mockExistsSync.mockReturnValue(false); + mockMkdirSync.mockReturnValue(undefined); + daemon = new DaemonManager(); + }); + + describe("start()", () => { + it("spawns devbrain-daemon as a detached process", async () => { + mockFetch.mockResolvedValue({ ok: true }); + await daemon.start(); + expect(mockSpawn).toHaveBeenCalledWith( + "/mock/bin/devbrain-daemon", + [], + expect.objectContaining({ detached: true, stdio: "ignore" }) + ); + }); + + it("writes PID file after spawning", async () => { + mockFetch.mockResolvedValue({ ok: true }); + await daemon.start(); + expect(mockWriteFileSync).toHaveBeenCalledWith( + "/mock/.devbrain/daemon.pid", + "12345" + ); + }); + + it("clears stopped sentinel before starting", async () => { + mockExistsSync.mockImplementation((p) => + String(p) === "/mock/.devbrain/stopped" + ); + mockFetch.mockResolvedValue({ ok: true }); + await daemon.start(); + expect(mockUnlinkSync).toHaveBeenCalledWith("/mock/.devbrain/stopped"); + }); + }); + + describe("stop()", () => { + it("kills process by PID from file", async () => { + mockExistsSync.mockImplementation((p) => + String(p) === "/mock/.devbrain/daemon.pid" + ); + mockReadFileSync.mockReturnValue("12345"); + const killMock = jest.fn(); + jest.spyOn(process, "kill").mockImplementation(killMock); + + await daemon.stop(); + + expect(killMock).toHaveBeenCalledWith(12345); + expect(mockUnlinkSync).toHaveBeenCalledWith("/mock/.devbrain/daemon.pid"); + (process.kill as jest.Mock).mockRestore(); + }); + }); + + describe("auto-restart", () => { + it("starts with zero crash count", () => { + expect(daemon.crashCount).toBe(0); + }); + + it("stops restarting after 3 crashes", () => { + daemon.recordCrash(); + daemon.recordCrash(); + daemon.recordCrash(); + expect(daemon.shouldRestart()).toBe(false); + }); + + it("allows restart after manual reset", () => { + daemon.recordCrash(); + daemon.recordCrash(); + daemon.recordCrash(); + daemon.resetCrashCount(); + expect(daemon.shouldRestart()).toBe(true); + }); + }); +}); +``` + +Write to: `packages/tray/__tests__/daemon.test.ts` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cd packages/tray && npx jest daemon` + +Expected: FAIL — `Cannot find module '../src/daemon'` + +- [ ] **Step 3: Implement daemon.ts** + +```typescript +import { spawn, ChildProcess } from "child_process"; +import * as fs from "fs"; +import { + binaryPath, + pidPath, + dataDir, + stoppedSentinelPath, + logsDir, +} from "./paths"; +import { showError } from "./notifications"; + +const MAX_CRASHES = 3; +const CRASH_WINDOW_MS = 5 * 60 * 1000; + +export class DaemonManager { + private process: ChildProcess | null = null; + private crashes: number[] = []; + private onCrashCallback: (() => void) | null = null; + private onExhaustedCallback: (() => void) | null = null; + + get crashCount(): number { + return this.crashes.length; + } + + shouldRestart(): boolean { + const now = Date.now(); + this.crashes = this.crashes.filter((t) => now - t < CRASH_WINDOW_MS); + return this.crashes.length < MAX_CRASHES; + } + + recordCrash(): void { + this.crashes.push(Date.now()); + } + + resetCrashCount(): void { + this.crashes = []; + } + + onCrash(cb: () => void): void { + this.onCrashCallback = cb; + } + + onRestartsExhausted(cb: () => void): void { + this.onExhaustedCallback = cb; + } + + async start(): Promise { + const sentinel = stoppedSentinelPath(); + if (fs.existsSync(sentinel)) { + fs.unlinkSync(sentinel); + } + + const data = dataDir(); + if (!fs.existsSync(data)) { + fs.mkdirSync(data, { recursive: true }); + } + const logs = logsDir(); + if (!fs.existsSync(logs)) { + fs.mkdirSync(logs, { recursive: true }); + } + + const daemonBin = binaryPath("devbrain-daemon"); + + this.process = spawn(daemonBin, [], { + detached: true, + stdio: "ignore", + }); + + this.process.unref(); + + if (this.process.pid) { + fs.writeFileSync(pidPath(), String(this.process.pid)); + } + + this.process.on("exit", (code) => { + if (fs.existsSync(stoppedSentinelPath())) { + return; + } + + if (code !== 0 && code !== null) { + this.recordCrash(); + this.onCrashCallback?.(); + + if (this.shouldRestart()) { + this.start(); + } else { + showError( + "Daemon crashed", + "DevBrain daemon crashed 3 times in 5 minutes. Use the tray menu to restart." + ); + this.onExhaustedCallback?.(); + } + } + }); + } + + async stop(): Promise { + const pid = pidPath(); + + if (fs.existsSync(pid)) { + const pidValue = parseInt(fs.readFileSync(pid, "utf-8").trim(), 10); + + try { + process.kill(pidValue); + } catch { + // Process already dead + } + + fs.unlinkSync(pid); + } + + this.process = null; + } + + async restart(): Promise { + await this.stop(); + this.resetCrashCount(); + await this.start(); + } + + isRunning(): boolean { + const pid = pidPath(); + if (!fs.existsSync(pid)) return false; + + const pidValue = parseInt(fs.readFileSync(pid, "utf-8").trim(), 10); + + try { + process.kill(pidValue, 0); + return true; + } catch { + return false; + } + } +} +``` + +Write to: `packages/tray/src/daemon.ts` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `cd packages/tray && npx jest daemon` + +Expected: All daemon tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add packages/tray/__tests__/daemon.test.ts packages/tray/src/daemon.ts +git commit -m "feat(packaging): add daemon lifecycle manager with auto-restart" +``` + +--- + +## Task 6: Bootstrap Orchestrator — TDD + +**Files:** +- Create: `packages/tray/__tests__/bootstrap.test.ts` +- Create: `packages/tray/src/bootstrap.ts` + +First-run flow: config creation, Ollama detection, model pull. + +- [ ] **Step 1: Write the failing tests** + +```typescript +import { Bootstrap } from "../src/bootstrap"; +import * as fs from "fs"; +import * as child_process from "child_process"; + +jest.mock("fs"); +jest.mock("child_process"); + +const mockExistsSync = fs.existsSync as jest.MockedFunction; +const mockWriteFileSync = fs.writeFileSync as jest.MockedFunction; +const mockMkdirSync = fs.mkdirSync as jest.MockedFunction; +const mockExecFileSync = child_process.execFileSync as jest.MockedFunction; + +const mockFetch = jest.fn(); +global.fetch = mockFetch as unknown as typeof fetch; + +jest.mock("../src/paths", () => ({ + dataDir: () => "/mock/.devbrain", + settingsPath: () => "/mock/.devbrain/settings.toml", +})); + +jest.mock("../src/notifications", () => ({ + showInfo: jest.fn(), + showError: jest.fn(), + showProgress: jest.fn(() => ({ close: jest.fn() })), +})); + +describe("Bootstrap", () => { + let bootstrap: Bootstrap; + + beforeEach(() => { + jest.clearAllMocks(); + mockExistsSync.mockReturnValue(false); + mockMkdirSync.mockReturnValue(undefined); + bootstrap = new Bootstrap(); + }); + + describe("ensureConfig()", () => { + it("creates settings.toml when missing", async () => { + mockExistsSync.mockReturnValue(false); + await bootstrap.ensureConfig(); + expect(mockWriteFileSync).toHaveBeenCalledWith( + "/mock/.devbrain/settings.toml", + expect.stringContaining("[daemon]") + ); + }); + + it("skips config creation when file exists", async () => { + mockExistsSync.mockImplementation((p) => String(p).endsWith("settings.toml")); + await bootstrap.ensureConfig(); + expect(mockWriteFileSync).not.toHaveBeenCalled(); + }); + }); + + describe("isOllamaInstalled()", () => { + it("returns true when Ollama API responds", async () => { + mockFetch.mockResolvedValueOnce({ ok: true }); + const result = await bootstrap.isOllamaInstalled(); + expect(result).toBe(true); + }); + + it("returns false when Ollama API is unreachable", async () => { + mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED")); + const result = await bootstrap.isOllamaInstalled(); + expect(result).toBe(false); + }); + }); + + describe("isModelPulled()", () => { + it("returns true when model is in ollama list output", async () => { + mockExecFileSync.mockReturnValue(Buffer.from("llama3.2:3b\t3.2GB\n")); + const result = await bootstrap.isModelPulled("llama3.2:3b"); + expect(result).toBe(true); + }); + + it("returns false when model is not found", async () => { + mockExecFileSync.mockReturnValue(Buffer.from("")); + const result = await bootstrap.isModelPulled("llama3.2:3b"); + expect(result).toBe(false); + }); + + it("returns false when ollama command fails", async () => { + mockExecFileSync.mockImplementation(() => { + throw new Error("command not found"); + }); + const result = await bootstrap.isModelPulled("llama3.2:3b"); + expect(result).toBe(false); + }); + }); + + describe("idempotency", () => { + it("running ensureConfig twice does not overwrite existing config", async () => { + mockExistsSync + .mockReturnValueOnce(false) // dataDir check + .mockReturnValueOnce(false) // settings check (first call) + .mockReturnValueOnce(true) // dataDir check + .mockReturnValueOnce(true); // settings check (second call) + + await bootstrap.ensureConfig(); + await bootstrap.ensureConfig(); + + expect(mockWriteFileSync).toHaveBeenCalledTimes(1); + }); + }); +}); +``` + +Write to: `packages/tray/__tests__/bootstrap.test.ts` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cd packages/tray && npx jest bootstrap` + +Expected: FAIL — `Cannot find module '../src/bootstrap'` + +- [ ] **Step 3: Implement bootstrap.ts** + +```typescript +import * as fs from "fs"; +import { execFileSync } from "child_process"; +import { dataDir, settingsPath } from "./paths"; +import { showInfo, showError, showProgress } from "./notifications"; + +const OLLAMA_API = "http://localhost:11434/api/version"; +const DEFAULT_MODEL = "llama3.2:3b"; + +const DEFAULT_SETTINGS = `[daemon] +port = 37800 +log_level = "info" + +[capture] +enabled = true +sources = ["ai-sessions"] +privacy_mode = "redact" +max_observation_size_kb = 512 +thread_gap_hours = 2 + +[storage] +sqlite_max_size_mb = 2048 +retention_days = 365 + +[llm.local] +enabled = true +provider = "ollama" +model = "llama3.2:3b" +endpoint = "http://localhost:11434" +max_concurrent = 2 + +[llm.cloud] +enabled = true +provider = "anthropic" +api_key_env = "DEVBRAIN_CLOUD_API_KEY" +max_daily_requests = 50 + +[agents.briefing] +enabled = true +schedule = "0 7 * * *" + +[agents.dead_end] +enabled = true +sensitivity = "medium" + +[agents.compression] +enabled = true +idle_minutes = 60 +`; + +export class Bootstrap { + async ensureConfig(): Promise { + const dir = dataDir(); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + const settings = settingsPath(); + if (!fs.existsSync(settings)) { + fs.writeFileSync(settings, DEFAULT_SETTINGS); + } + } + + async isOllamaInstalled(): Promise { + try { + const res = await fetch(OLLAMA_API); + return res.ok; + } catch { + return false; + } + } + + async isModelPulled(model: string): Promise { + try { + const output = execFileSync("ollama", ["list"], { + encoding: "utf-8", + timeout: 10000, + }); + return output.includes(model); + } catch { + return false; + } + } + + async installOllama(): Promise { + showProgress("Setup", "Installing local AI runtime (first time only)..."); + + try { + if (process.platform === "win32") { + await this.installOllamaWindows(); + } else if (process.platform === "darwin") { + await this.installOllamaMac(); + } else { + await this.installOllamaLinux(); + } + return true; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + showError( + "Ollama install failed", + `Could not install Ollama: ${msg}. DevBrain works without it — add a cloud API key in Settings.` + ); + return false; + } + } + + async pullModel(model: string): Promise { + showProgress("Setup", "Downloading AI model (~2GB)..."); + + try { + execFileSync("ollama", ["pull", model], { + timeout: 600000, + stdio: "ignore", + }); + return true; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + showError("Model download failed", `Could not download ${model}: ${msg}`); + return false; + } + } + + /** + * Run the full bootstrap flow. Non-blocking — daemon starts first, + * Ollama install happens in background. + */ + async run(startDaemon: () => Promise): Promise { + await this.ensureConfig(); + await startDaemon(); + + // Ollama setup in background — non-blocking + this.bootstrapOllama().catch(() => { + // Errors already shown via notifications + }); + } + + private async bootstrapOllama(): Promise { + const installed = await this.isOllamaInstalled(); + if (!installed) { + const success = await this.installOllama(); + if (!success) return; + } + + const pulled = await this.isModelPulled(DEFAULT_MODEL); + if (!pulled) { + await this.pullModel(DEFAULT_MODEL); + } + + showInfo("Ready", "DevBrain is ready with local AI."); + } + + private async installOllamaWindows(): Promise { + const tmpPath = `${process.env.TEMP || "C:\\Temp"}\\OllamaSetup.exe`; + execFileSync("powershell", [ + "-Command", + `Invoke-WebRequest -Uri 'https://ollama.com/download/OllamaSetup.exe' -OutFile '${tmpPath}'`, + ], { timeout: 300000 }); + execFileSync(tmpPath, ["/S"], { timeout: 300000 }); + } + + private async installOllamaMac(): Promise { + try { + execFileSync("brew", ["install", "ollama"], { timeout: 300000 }); + } catch { + execFileSync("bash", ["-c", "curl -fsSL https://ollama.com/install.sh | sh"], { timeout: 300000 }); + } + } + + private async installOllamaLinux(): Promise { + execFileSync("bash", ["-c", "curl -fsSL https://ollama.com/install.sh | sh"], { timeout: 300000 }); + } +} +``` + +Write to: `packages/tray/src/bootstrap.ts` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `cd packages/tray && npx jest bootstrap` + +Expected: All bootstrap tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add packages/tray/__tests__/bootstrap.test.ts packages/tray/src/bootstrap.ts +git commit -m "feat(packaging): add first-run bootstrap orchestrator" +``` + +--- + +## Task 7: Electron Main Entry — Tray Icon + Context Menu + +**Files:** +- Create: `packages/tray/src/main.ts` +- Create: `packages/tray/assets/` (placeholder icons) + +This is the Electron entry point — creates the tray icon, wires up the context menu, and orchestrates health + daemon + bootstrap. + +- [ ] **Step 1: Create placeholder tray icon assets** + +Run: +```bash +mkdir -p packages/tray/assets +``` + +Create minimal placeholder files for each icon. These will be replaced with real designed icons before release. For now, copy the dashboard favicon or create empty files: + +```bash +echo "placeholder" > packages/tray/assets/icon.png +echo "placeholder" > packages/tray/assets/icon-yellow.png +echo "placeholder" > packages/tray/assets/icon-red.png +echo "placeholder" > packages/tray/assets/icon.ico +echo "placeholder" > packages/tray/assets/icon.icns +``` + +Note: Real icon assets (proper .ico, .icns, multi-resolution PNGs) must be designed and added before first release. electron-builder can generate .ico and .icns from a 1024x1024 PNG source. + +- [ ] **Step 2: Write main.ts** + +```typescript +import { app, Tray, Menu, shell, nativeImage } from "electron"; +import * as fs from "fs"; +import { HealthMonitor, HealthState } from "./health"; +import { DaemonManager } from "./daemon"; +import { Bootstrap } from "./bootstrap"; +import { iconPath, trayLockPath, dataDir, logsDir } from "./paths"; + +let tray: Tray | null = null; +let healthMonitor: HealthMonitor; +let daemonManager: DaemonManager; +let bootstrap: Bootstrap; +let currentState: HealthState = "starting"; + +function createTray(): void { + const icon = nativeImage.createFromPath(iconPath("green")); + tray = new Tray(icon); + tray.setToolTip("DevBrain (Starting...)"); + updateMenu(); +} + +function updateMenu(): void { + if (!tray) return; + + const statusLabel = + currentState === "healthy" + ? "DevBrain (Running)" + : currentState === "unhealthy" + ? "DevBrain (Stopped)" + : "DevBrain (Starting...)"; + + const template = Menu.buildFromTemplate([ + { label: statusLabel, enabled: false }, + { type: "separator" }, + { + label: "Open Dashboard", + click: () => shell.openExternal("http://localhost:37800"), + enabled: currentState === "healthy", + }, + { type: "separator" }, + { + label: "Start Daemon", + click: async () => { + await daemonManager.start(); + healthMonitor.start(); + }, + enabled: currentState !== "healthy", + }, + { + label: "Stop Daemon", + click: async () => { + await daemonManager.stop(); + }, + enabled: currentState === "healthy", + }, + { + label: "Restart Daemon", + click: async () => { + await daemonManager.restart(); + }, + }, + { type: "separator" }, + { + label: "View Logs", + click: () => { + const logs = logsDir(); + if (!fs.existsSync(logs)) { + fs.mkdirSync(logs, { recursive: true }); + } + shell.openPath(logs); + }, + }, + { type: "separator" }, + { + label: "Quit DevBrain", + click: async () => { + await daemonManager.stop(); + removeTrayLock(); + app.quit(); + }, + }, + ]); + + tray.setContextMenu(template); +} + +function updateTrayIcon(state: HealthState): void { + if (!tray) return; + + currentState = state; + + const iconState = + state === "healthy" ? "green" : state === "unhealthy" ? "red" : "yellow"; + + const icon = nativeImage.createFromPath(iconPath(iconState)); + tray.setImage(icon); + + const tooltip = + state === "healthy" + ? "DevBrain (Running)" + : state === "unhealthy" + ? "DevBrain (Stopped)" + : "DevBrain (Starting...)"; + tray.setToolTip(tooltip); + + updateMenu(); +} + +function writeTrayLock(): void { + const lockPath = trayLockPath(); + const dir = dataDir(); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(lockPath, String(process.pid)); +} + +function removeTrayLock(): void { + try { + fs.unlinkSync(trayLockPath()); + } catch { + // Best-effort + } +} + +app.whenReady().then(async () => { + const gotLock = app.requestSingleInstanceLock(); + if (!gotLock) { + app.quit(); + return; + } + + if (process.platform === "darwin") { + app.dock.hide(); + } + + writeTrayLock(); + + daemonManager = new DaemonManager(); + healthMonitor = new HealthMonitor(); + bootstrap = new Bootstrap(); + + createTray(); + + healthMonitor.on("stateChange", (state: HealthState) => { + updateTrayIcon(state); + }); + + daemonManager.onRestartsExhausted(() => { + updateTrayIcon("unhealthy"); + }); + + await bootstrap.run(() => daemonManager.start()); + + healthMonitor.start(); +}); + +app.on("window-all-closed", (e: Event) => { + e.preventDefault(); +}); + +app.on("before-quit", () => { + healthMonitor.stop(); + removeTrayLock(); +}); +``` + +Write to: `packages/tray/src/main.ts` + +- [ ] **Step 3: Verify it compiles** + +Run: `cd packages/tray && npx tsc --noEmit` + +Expected: No errors. + +- [ ] **Step 4: Commit** + +```bash +git add packages/tray/src/main.ts packages/tray/assets/ +git commit -m "feat(packaging): add Electron main entry with tray icon and context menu" +``` + +--- + +## Task 8: CLI / Tray Coordination + +**Files:** +- Modify: `src/DevBrain.Cli/Commands/StartCommand.cs` +- Modify: `src/DevBrain.Cli/Commands/StopCommand.cs` + +Add `tray.lock` checks so CLI and tray app don't fight over daemon management. + +- [ ] **Step 1: Update StartCommand to check for tray.lock** + +In `src/DevBrain.Cli/Commands/StartCommand.cs`, replace the `Execute` method body. After the existing health check on line 18, add tray lock detection before spawning the daemon: + +```csharp +private static async Task Execute(ParseResult pr) +{ + var client = new DevBrainHttpClient(); + + if (await client.IsHealthy()) + { + ConsoleFormatter.PrintWarning("Daemon is already running."); + return; + } + + // Check if tray app is managing the daemon + var dataPath = SettingsLoader.ResolveDataPath("~/.devbrain"); + var trayLockPath = Path.Combine(dataPath, "tray.lock"); + + if (File.Exists(trayLockPath)) + { + var lockPidText = (await File.ReadAllTextAsync(trayLockPath)).Trim(); + if (int.TryParse(lockPidText, out var trayPid)) + { + try + { + Process.GetProcessById(trayPid); + ConsoleFormatter.PrintWarning( + "Daemon is managed by the tray app. Use the tray menu to start it."); + return; + } + catch (ArgumentException) + { + // Tray process is dead — stale lock, continue normally + } + } + } + + var cliDir = AppContext.BaseDirectory; + var daemonName = OperatingSystem.IsWindows() ? "devbrain-daemon.exe" : "devbrain-daemon"; + var daemonPath = Path.Combine(cliDir, daemonName); + + if (!File.Exists(daemonPath)) + { + ConsoleFormatter.PrintError($"Daemon binary not found at: {daemonPath}"); + return; + } + + var psi = new ProcessStartInfo + { + FileName = daemonPath, + UseShellExecute = true, + WindowStyle = ProcessWindowStyle.Hidden + }; + + try + { + Process.Start(psi); + } + catch (Exception ex) + { + ConsoleFormatter.PrintError($"Failed to start daemon: {ex.Message}"); + return; + } + + Console.Write("Starting daemon"); + + for (var i = 0; i < 20; i++) + { + await Task.Delay(500); + Console.Write("."); + + if (await client.IsHealthy()) + { + Console.WriteLine(); + ConsoleFormatter.PrintSuccess("Daemon started successfully."); + return; + } + } + + Console.WriteLine(); + ConsoleFormatter.PrintError("Daemon did not become healthy within 10 seconds."); +} +``` + +Note: Ensure `using DevBrain.Core;` is present at the top for `SettingsLoader`. + +- [ ] **Step 2: Update StopCommand to write stopped sentinel when tray is active** + +In `src/DevBrain.Cli/Commands/StopCommand.cs`, replace the `Execute` method body. Before killing the daemon process, check for the tray lock and write a sentinel: + +```csharp +private static async Task Execute(ParseResult pr) +{ + var dataPath = SettingsLoader.ResolveDataPath("~/.devbrain"); + var pidPath = Path.Combine(dataPath, "daemon.pid"); + + if (!File.Exists(pidPath)) + { + ConsoleFormatter.PrintWarning("No PID file found. Daemon may not be running."); + return; + } + + // If tray app is running, write stopped sentinel so it doesn't auto-restart + var trayLockPath = Path.Combine(dataPath, "tray.lock"); + if (File.Exists(trayLockPath)) + { + var trayPidText = (await File.ReadAllTextAsync(trayLockPath)).Trim(); + if (int.TryParse(trayPidText, out var trayPid)) + { + try + { + Process.GetProcessById(trayPid); + // Tray is alive — write sentinel to prevent auto-restart + var sentinelPath = Path.Combine(dataPath, "stopped"); + await File.WriteAllTextAsync(sentinelPath, "stopped by cli"); + } + catch (ArgumentException) + { + // Tray is dead — no sentinel needed + } + } + } + + var pidText = (await File.ReadAllTextAsync(pidPath)).Trim(); + + if (!int.TryParse(pidText, out var pid)) + { + ConsoleFormatter.PrintError($"Invalid PID file content: {pidText}"); + return; + } + + try + { + var process = Process.GetProcessById(pid); + process.Kill(entireProcessTree: true); + process.WaitForExit(5000); + ConsoleFormatter.PrintSuccess($"Daemon (PID {pid}) stopped."); + } + catch (ArgumentException) + { + ConsoleFormatter.PrintWarning($"No process found with PID {pid}. Daemon may have already stopped."); + } + catch (Exception ex) + { + ConsoleFormatter.PrintError($"Failed to stop daemon: {ex.Message}"); + } + + try + { + File.Delete(pidPath); + } + catch + { + // Best-effort cleanup + } +} +``` + +- [ ] **Step 3: Verify .NET build** + +Run: `dotnet build DevBrain.slnx` + +Expected: Build succeeded, 0 errors. + +- [ ] **Step 4: Run existing tests** + +Run: `dotnet test DevBrain.slnx` + +Expected: All 54+ tests pass (no behavioral changes to existing functionality). + +- [ ] **Step 5: Commit** + +```bash +git add src/DevBrain.Cli/Commands/StartCommand.cs src/DevBrain.Cli/Commands/StopCommand.cs +git commit -m "feat(packaging): add CLI/tray coordination via tray.lock + stopped sentinel" +``` + +--- + +## Task 9: electron-builder Configuration + +**Files:** +- Create: `packages/tray/electron-builder.yml` +- Create: `packages/tray/build/installer.nsh` +- Create: `packages/tray/build/linux-after-install.sh` +- Create: `packages/tray/build/linux-after-remove.sh` + +Configures electron-builder to produce NSIS (Windows), DMG (macOS), and .deb (Linux). + +- [ ] **Step 1: Write electron-builder.yml** + +```yaml +appId: com.devbrain.tray +productName: DevBrain +copyright: Copyright 2026 DevBrain + +extraResources: + - from: resources/bin/ + to: bin/ + filter: + - "**/*" + - from: resources/wwwroot/ + to: wwwroot/ + filter: + - "**/*" + - from: assets/ + to: assets/ + filter: + - "*.png" + - "*.ico" + - "*.icns" + +win: + target: + - target: nsis + arch: [x64] + icon: assets/icon.ico + +nsis: + oneClick: true + allowToChangeInstallationDirectory: false + perMachine: false + installerIcon: assets/icon.ico + include: build/installer.nsh + +mac: + target: + - target: dmg + arch: [x64] + icon: assets/icon.icns + category: public.app-category.developer-tools + +dmg: + contents: + - x: 130 + y: 220 + - x: 410 + y: 220 + type: link + path: /Applications + +linux: + target: + - target: deb + arch: [x64] + - target: AppImage + arch: [x64] + icon: assets/icon.png + category: Development + desktop: + StartupWMClass: DevBrain + +deb: + depends: + - libgtk-3-0 + - libnotify4 + - libnss3 + afterInstall: build/linux-after-install.sh + afterRemove: build/linux-after-remove.sh +``` + +Write to: `packages/tray/electron-builder.yml` + +- [ ] **Step 2: Create NSIS installer script for PATH** + +```nsis +!macro customInstall + nsExec::ExecToLog 'setx PATH "%PATH%;$INSTDIR\resources\bin"' +!macroend + +!macro customUnInstall + ; PATH cleanup is complex in NSIS — users can manually clean up +!macroend +``` + +Write to: `packages/tray/build/installer.nsh` + +- [ ] **Step 3: Create Linux post-install script** + +```bash +#!/bin/bash +set -e + +ln -sf /opt/DevBrain/resources/bin/devbrain /usr/local/bin/devbrain +ln -sf /opt/DevBrain/resources/bin/devbrain-daemon /usr/local/bin/devbrain-daemon + +mkdir -p /etc/xdg/autostart +cat > /etc/xdg/autostart/devbrain.desktop << 'EOF' +[Desktop Entry] +Type=Application +Name=DevBrain +Exec=/opt/DevBrain/devbrain-tray +Icon=/opt/DevBrain/resources/assets/icon.png +Comment=Developer's second brain +Categories=Development; +X-GNOME-Autostart-enabled=true +StartupNotify=false +Terminal=false +EOF +``` + +Write to: `packages/tray/build/linux-after-install.sh` + +- [ ] **Step 4: Create Linux post-remove script** + +```bash +#!/bin/bash +set -e + +rm -f /usr/local/bin/devbrain +rm -f /usr/local/bin/devbrain-daemon +rm -f /etc/xdg/autostart/devbrain.desktop + +pkill -f devbrain-daemon 2>/dev/null || true +pkill -f devbrain-tray 2>/dev/null || true + +# NOTE: ~/.devbrain/ is intentionally preserved (user data) +``` + +Write to: `packages/tray/build/linux-after-remove.sh` + +- [ ] **Step 5: Make Linux scripts executable** + +Run: `chmod +x packages/tray/build/linux-after-install.sh packages/tray/build/linux-after-remove.sh` + +- [ ] **Step 6: Commit** + +```bash +git add packages/tray/electron-builder.yml packages/tray/build/ +git commit -m "feat(packaging): add electron-builder config for Windows/macOS/Linux" +``` + +--- + +## Task 10: Package Manager Manifests + +**Files:** +- Create: `packages/homebrew/devbrain.rb` +- Create: `packages/winget/DevBrain.DevBrain.yaml` +- Create: `packages/apt/debian/control` +- Create: `packages/apt/debian/postinst` +- Create: `packages/apt/debian/prerm` +- Create: `packages/apt/debian/rules` +- Create: `packages/apt/debian/devbrain.desktop` + +- [ ] **Step 1: Create Homebrew formula** + +```ruby +class Devbrain < Formula + desc "Developer's second brain - captures coding sessions, builds knowledge graph" + homepage "https://github.com/devbrain/devbrain" + version "1.0.0" + + if OS.mac? && Hardware::CPU.arm? + url "https://github.com/devbrain/devbrain/releases/download/v1.0.0/devbrain-osx-arm64.tar.gz" + sha256 "PLACEHOLDER_SHA256" + elsif OS.mac? && Hardware::CPU.intel? + url "https://github.com/devbrain/devbrain/releases/download/v1.0.0/devbrain-osx-x64.tar.gz" + sha256 "PLACEHOLDER_SHA256" + elsif OS.linux? && Hardware::CPU.intel? + url "https://github.com/devbrain/devbrain/releases/download/v1.0.0/devbrain-linux-x64.tar.gz" + sha256 "PLACEHOLDER_SHA256" + elsif OS.linux? && Hardware::CPU.arm? + url "https://github.com/devbrain/devbrain/releases/download/v1.0.0/devbrain-linux-arm64.tar.gz" + sha256 "PLACEHOLDER_SHA256" + end + + def install + bin.install "devbrain" + bin.install "devbrain-daemon" + prefix.install "DevBrain.app" if OS.mac? + end + + # No Homebrew service block - the Electron tray app owns daemon lifecycle. + + def post_install + # Tray app handles all user-space bootstrap on first launch. + end + + test do + assert_match "devbrain", shell_output("#{bin}/devbrain --version") + end +end +``` + +Write to: `packages/homebrew/devbrain.rb` + +- [ ] **Step 2: Create winget manifest** + +```yaml +PackageIdentifier: DevBrain.DevBrain +PackageVersion: 1.0.0 +DefaultLocale: en-US +PackageName: DevBrain +Publisher: DevBrain +PublisherUrl: https://github.com/devbrain/devbrain +License: Apache-2.0 +ShortDescription: Developer's second brain - captures coding sessions, builds knowledge graph +Tags: + - developer-tools + - productivity + - knowledge-graph + - ai +InstallerType: exe +Installers: + - Architecture: x64 + InstallerUrl: https://github.com/devbrain/devbrain/releases/download/v1.0.0/DevBrain-Setup-1.0.0-x64.exe + InstallerSha256: PLACEHOLDER_SHA256 + InstallerSwitches: + Silent: /S + SilentWithProgress: /S +ManifestType: singleton +ManifestVersion: 1.6.0 +``` + +Write to: `packages/winget/DevBrain.DevBrain.yaml` + +- [ ] **Step 3: Create APT debian/control** + +``` +Package: devbrain +Version: 1.0.0 +Section: devel +Priority: optional +Architecture: amd64 +Depends: libgtk-3-0, libnotify4, libnss3, libxss1, libxtst6, libatspi2.0-0 +Maintainer: DevBrain +Homepage: https://github.com/devbrain/devbrain +Description: Developer's second brain + DevBrain is a background daemon that passively captures AI coding sessions, + builds a knowledge graph of decisions and dead ends, and surfaces proactive + insights including morning briefings, pattern detection, and semantic search. +``` + +Write to: `packages/apt/debian/control` + +- [ ] **Step 4: Create APT postinst** + +```bash +#!/bin/bash +set -e + +ln -sf /opt/devbrain/resources/bin/devbrain /usr/local/bin/devbrain +ln -sf /opt/devbrain/resources/bin/devbrain-daemon /usr/local/bin/devbrain-daemon + +mkdir -p /etc/xdg/autostart +cp /opt/devbrain/devbrain.desktop /etc/xdg/autostart/devbrain.desktop 2>/dev/null || true +``` + +Write to: `packages/apt/debian/postinst` + +- [ ] **Step 5: Create APT prerm** + +```bash +#!/bin/bash +set -e + +pkill -f devbrain-daemon 2>/dev/null || true +pkill -f devbrain-tray 2>/dev/null || true + +rm -f /usr/local/bin/devbrain +rm -f /usr/local/bin/devbrain-daemon +rm -f /etc/xdg/autostart/devbrain.desktop + +# ~/.devbrain/ is intentionally preserved (user data) +``` + +Write to: `packages/apt/debian/prerm` + +- [ ] **Step 6: Create APT rules** + +```makefile +#!/usr/bin/make -f + +%: + dh $@ + +override_dh_auto_build: + true + +override_dh_auto_install: + mkdir -p debian/devbrain/opt/devbrain + cp -r . debian/devbrain/opt/devbrain/ +``` + +Write to: `packages/apt/debian/rules` + +- [ ] **Step 7: Create desktop file** + +```desktop +[Desktop Entry] +Type=Application +Name=DevBrain +Exec=/opt/devbrain/devbrain-tray +Icon=/opt/devbrain/resources/assets/icon.png +Comment=Developer's second brain +Categories=Development; +X-GNOME-Autostart-enabled=true +StartupNotify=false +Terminal=false +``` + +Write to: `packages/apt/debian/devbrain.desktop` + +- [ ] **Step 8: Make scripts executable** + +Run: `chmod +x packages/apt/debian/postinst packages/apt/debian/prerm packages/apt/debian/rules` + +- [ ] **Step 9: Commit** + +```bash +git add packages/homebrew/ packages/winget/ packages/apt/ +git commit -m "feat(packaging): add Homebrew, winget, and APT package manifests" +``` + +--- + +## Task 11: CI/CD — Electron Build + Package Publish Workflow + +**Files:** +- Create: `.github/workflows/package.yml` + +- [ ] **Step 1: Write package.yml** + +```yaml +name: Package & Publish + +on: + workflow_run: + workflows: ["Build & Test"] + types: [completed] + branches: [main, master] + +jobs: + check: + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'success' + outputs: + is_tag: ${{ steps.check_tag.outputs.is_tag }} + steps: + - id: check_tag + run: | + if [[ "${{ github.event.workflow_run.head_branch }}" == v* ]]; then + echo "is_tag=true" >> $GITHUB_OUTPUT + else + echo "is_tag=false" >> $GITHUB_OUTPUT + fi + + electron-build: + needs: check + strategy: + matrix: + include: + - os: windows-latest + rid: win-x64 + electron-args: --win --x64 + artifact-pattern: "*.exe" + - os: macos-latest + rid: osx-x64 + electron-args: --mac --x64 + artifact-pattern: "*.dmg" + - os: ubuntu-latest + rid: linux-x64 + electron-args: --linux --x64 + artifact-pattern: "*.deb" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - uses: actions/download-artifact@v4 + with: + name: devbrain-${{ matrix.rid }} + path: packages/tray/resources/bin/ + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/download-artifact@v4 + with: + name: dashboard-dist + path: packages/tray/resources/wwwroot/ + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract .NET binaries (Unix) + if: runner.os != 'Windows' + run: | + cd packages/tray/resources/bin + for f in *.tar.gz; do [ -f "$f" ] && tar xzf "$f" && rm "$f"; done + chmod +x devbrain devbrain-daemon 2>/dev/null || true + + - name: Extract .NET binaries (Windows) + if: runner.os == 'Windows' + shell: bash + run: | + cd packages/tray/resources/bin + for f in *.zip; do [ -f "$f" ] && 7z x "$f" -y && rm "$f"; done + + - name: Install dependencies + run: cd packages/tray && npm ci + + - name: Build TypeScript + run: cd packages/tray && npm run build + + - name: Build Electron package + run: cd packages/tray && npx electron-builder ${{ matrix.electron-args }} --publish never + + - uses: actions/upload-artifact@v4 + with: + name: electron-${{ matrix.rid }} + path: packages/tray/dist/${{ matrix.artifact-pattern }} + + publish-homebrew: + needs: [check, electron-build] + if: needs.check.outputs.is_tag == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Update Homebrew formula + uses: Justintime50/homebrew-releaser@v1 + with: + homebrew_owner: devbrain + homebrew_tap: homebrew-tap + formula_folder: Formula + github_token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + commit_owner: devbrain-bot + commit_email: bot@devbrain.dev + install: | + bin.install "devbrain" + bin.install "devbrain-daemon" + test: | + assert_match "devbrain", shell_output("#{bin}/devbrain --version") + + publish-winget: + needs: [check, electron-build] + if: needs.check.outputs.is_tag == 'true' + runs-on: windows-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: electron-win-x64 + path: installer/ + + - name: Submit to winget + uses: vedantmgoyal9/winget-releaser@v2 + with: + identifier: DevBrain.DevBrain + installers-regex: '\.exe$' + token: ${{ secrets.WINGET_TOKEN }} + + publish-apt: + needs: [check, electron-build] + if: needs.check.outputs.is_tag == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: electron-linux-x64 + path: deb-package/ + + - name: Setup GPG + run: echo "${{ secrets.APT_GPG_PRIVATE_KEY }}" | gpg --batch --import + + - name: Update APT repository + run: | + mkdir -p apt-repo/pool/main + cp deb-package/*.deb apt-repo/pool/main/ + cd apt-repo + dpkg-scanpackages pool/main /dev/null | gzip -9c > Packages.gz + apt-ftparchive release . > Release + gpg --batch --yes --armor --detach-sign -o Release.gpg Release + gpg --batch --yes --clearsign -o InRelease Release + + - name: Publish to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./apt-repo + destination_dir: apt +``` + +Write to: `.github/workflows/package.yml` + +- [ ] **Step 2: Commit** + +```bash +git add .github/workflows/package.yml +git commit -m "feat(packaging): add CI/CD for Electron build + package manager publishing" +``` + +--- + +## Task 12: Local Build Verification + +Verify everything builds and tests pass locally. + +- [ ] **Step 1: Run all tray app tests** + +Run: `cd packages/tray && npx jest --verbose` + +Expected: All tests pass (~15 tests across health, daemon, bootstrap). + +- [ ] **Step 2: Build TypeScript** + +Run: `cd packages/tray && npm run build` + +Expected: `dist/` directory created with compiled `.js` files, no errors. + +- [ ] **Step 3: Verify .NET build still passes** + +Run: `dotnet build DevBrain.slnx` + +Expected: Build succeeded, 0 errors. + +- [ ] **Step 4: Run all .NET tests** + +Run: `dotnet test DevBrain.slnx` + +Expected: All tests pass. + +- [ ] **Step 5: Test local Electron build (optional — requires pre-built .NET binaries)** + +To test the full Electron build locally, first populate the resources: + +```bash +mkdir -p packages/tray/resources/bin packages/tray/resources/wwwroot +dotnet publish src/DevBrain.Api -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -o packages/tray/resources/bin/ +dotnet publish src/DevBrain.Cli -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -o packages/tray/resources/bin/ +# Rename to clean names +mv packages/tray/resources/bin/DevBrain.Api.exe packages/tray/resources/bin/devbrain-daemon.exe 2>/dev/null || true +mv packages/tray/resources/bin/DevBrain.Cli.exe packages/tray/resources/bin/devbrain.exe 2>/dev/null || true +# Copy dashboard +cp -r dashboard/dist/* packages/tray/resources/wwwroot/ 2>/dev/null || true +``` + +Then build unpacked: +```bash +cd packages/tray && npx electron-builder --dir +``` + +Expected: `packages/tray/dist/` contains an unpacked Electron app. + +- [ ] **Step 6: Final commit** + +```bash +git add -A +git commit -m "feat(packaging): complete single-click packaging implementation" +``` + +--- + +## Summary + +| Task | What it builds | New files | Tests | +|------|---------------|-----------|-------| +| 1 | npm workspaces + Electron scaffold | 3 + modify 1 | — | +| 2 | Platform paths module | 1 | — | +| 3 | Notifications module | 1 | — | +| 4 | Health monitor (TDD) | 2 + jest config | 5 tests | +| 5 | Daemon manager (TDD) | 2 | 5 tests | +| 6 | Bootstrap orchestrator (TDD) | 2 | 5 tests | +| 7 | Electron main entry + tray | 1 + 5 assets | — | +| 8 | CLI/tray coordination | modify 2 | existing .NET tests | +| 9 | electron-builder config | 4 | — | +| 10 | Package manager manifests | 7 | — | +| 11 | CI/CD workflow | 1 | — | +| 12 | Local build verification | — | all tests | + +**Total:** ~30 new files, 2 modified files, ~15 new tests, 12 commits. diff --git a/docs/superpowers/specs/2026-04-07-single-click-packaging-design.md b/docs/superpowers/specs/2026-04-07-single-click-packaging-design.md new file mode 100644 index 0000000..7a58a9e --- /dev/null +++ b/docs/superpowers/specs/2026-04-07-single-click-packaging-design.md @@ -0,0 +1,565 @@ +# DevBrain Single-Click Packaging Design + +**Date:** 2026-04-07 +**Status:** Draft +**Goal:** Ship DevBrain as a cross-platform package that developers install via package managers with zero manual configuration. + +--- + +## Overview + +DevBrain currently distributes as raw archives (`.tar.gz`/`.zip`) via GitHub Releases, with shell scripts handling installation. This design replaces that with native package manager distribution (`winget`, `brew`, `apt`) backed by an Electron tray app that manages the daemon lifecycle, Ollama bootstrapping, and system integration. + +**Developer experience after this ships:** + +```bash +# macOS +brew tap devbrain/tap && brew install devbrain + +# Windows +winget install DevBrain + +# Linux (Debian/Ubuntu) +curl -s https://devbrain.dev/install.gpg | sudo gpg --dearmor -o /usr/share/keyrings/devbrain.gpg +echo "deb [signed-by=/usr/share/keyrings/devbrain.gpg] https://devbrain.dev/apt stable main" | sudo tee /etc/apt/sources.list.d/devbrain.list +sudo apt update && sudo apt install devbrain +``` + +After install: tray icon appears, daemon starts, Ollama auto-installs in background, everything works. + +--- + +## Architecture + +### Components + +``` +Developer's Machine +┌─────────────────────────────────────────────┐ +│ Electron Tray App (DevBrain.exe / .app) │ +│ ├── System tray icon (green/yellow/red) │ +│ ├── Context menu (start/stop/dashboard) │ +│ ├── Bootstrap orchestrator │ +│ │ ├── Config creation │ +│ │ ├── Ollama auto-install │ +│ │ └── Model pull (llama3.2:3b) │ +│ └── Daemon lifecycle manager │ +│ ├── Spawn devbrain-daemon │ +│ ├── Health poll (/api/v1/health, 5s) │ +│ └── Auto-restart on crash (max 3) │ +│ │ +│ devbrain-daemon (embedded .NET binary) │ +│ ├── HTTP API on 127.0.0.1:37800 │ +│ ├── Capture pipeline (5 stages) │ +│ ├── Agent scheduler (8 agents) │ +│ ├── SQLite database │ +│ └── Dashboard (static files in wwwroot/) │ +│ │ +│ devbrain CLI (embedded .NET binary) │ +│ └── Thin HTTP client to daemon │ +│ │ +│ Ollama (auto-installed, external process) │ +│ └── localhost:11434 │ +└─────────────────────────────────────────────┘ +``` + +### Repository Layout Changes + +New directories added to the monorepo: + +``` +packages/ + tray/ # Electron tray app + package.json # electron, electron-builder deps + electron-builder.yml # Platform-specific build config + src/ + main.ts # Electron entry — tray icon, menu, autostart + bootstrap.ts # First-run: config, Ollama install, model pull + daemon.ts # Spawn/monitor/restart devbrain-daemon + health.ts # Poll health endpoint, update tray icon state + notifications.ts # OS-native notification helpers + assets/ + icon.png # Tray icon (all platforms) + icon.ico # Windows tray icon + icon.icns # macOS tray icon + __tests__/ + bootstrap.test.ts + daemon.test.ts + health.test.ts + + homebrew/ + devbrain.rb # Homebrew formula + + winget/ + DevBrain.DevBrain.yaml # winget manifest + + apt/ + debian/ + control # Package metadata + dependencies + postinst # Post-install: config, Ollama, autostart + prerm # Pre-remove: stop daemon, cleanup + rules # Build rules + +package.json # Root — npm workspaces config +``` + +The `dashboard/` and `packages/tray/` share code via npm workspaces: + +```json +{ + "workspaces": ["dashboard", "packages/tray"] +} +``` + +--- + +## Electron Tray App + +### Tray Icon & Menu + +The tray app is a **menubar utility**, not a windowed application. It manages the daemon and provides quick access. + +**Tray icon states:** +- Green: daemon running, healthy +- Yellow: starting up, bootstrapping, or Ollama installing +- Red: daemon stopped or unhealthy + +**Context menu:** + +``` +DevBrain (Running) ← status text +───────────────── +Open Dashboard → opens http://localhost:37800 in default browser +───────────────── +Start Daemon +Stop Daemon +Restart Daemon +───────────────── +View Logs → opens ~/.devbrain/logs/ in file manager +───────────────── +Quit DevBrain → stops daemon, exits tray app +``` + +### Daemon Lifecycle Management + +The tray app replaces the CLI's `devbrain start`/`devbrain stop` as the primary daemon manager. + +```typescript +// daemon.ts — simplified contract +export interface DaemonManager { + start(): Promise; // Spawn devbrain-daemon, write PID + stop(): Promise; // Kill process, cleanup PID + restart(): Promise; // Stop + start + isRunning(): Promise; // Check PID + health endpoint +} +``` + +**Auto-restart policy:** +- On daemon crash: restart immediately, up to 3 times in 5 minutes +- After 3 failures: stop retrying, show error notification, tray icon turns red +- User can manually restart from tray menu to reset the counter + +**Auto-launch at login:** +- electron-builder's `autoLaunch: true` option handles per-platform registration + - Windows: registry `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` + - macOS: Login Items + - Linux: `~/.config/autostart/devbrain.desktop` + +### CLI / Tray Coordination + +Both the CLI (`devbrain start`/`stop`) and the tray app can manage the daemon. Without coordination, conflicts arise — e.g., `devbrain stop` kills the daemon, tray thinks it crashed, auto-restarts it. + +**Resolution:** The tray app writes a management lock file at `~/.devbrain/tray.lock` while running. The CLI checks for this file: +- `devbrain start` — if `tray.lock` exists and the tray process is alive, print "Daemon is managed by the tray app. Use the tray menu or run `devbrain-tray` to manage it." and exit. +- `devbrain stop` — if `tray.lock` exists and the tray process is alive, send a stop request to the tray app via a local IPC socket (or simply delete the PID file and set a `~/.devbrain/stopped` sentinel). The tray app checks for the sentinel before auto-restarting — if present, it shows "Stopped" state instead of restarting. +- If the tray app is not running (no lock or stale lock), CLI behaves as it does today. + +This keeps the CLI fully functional for headless/server use while preventing fights with the tray app. + +### Dashboard Access + +Tray menu "Open Dashboard" opens `http://localhost:37800` in the system default browser. No embedded Electron BrowserWindow — keeps the tray app lightweight and avoids duplicating browser memory. + +--- + +## First-Run Bootstrap + +Orchestrated by `bootstrap.ts` in the Electron main process. The daemon is NOT responsible for installing external software. + +### Flow + +``` +1. Config check + ~/.devbrain/settings.toml exists? + ├─ YES → skip + └─ NO → create default config (embedded template) + +2. Daemon start + Spawn devbrain-daemon process + Poll /api/v1/health every 500ms, timeout 10s + ├─ SUCCESS → continue + └─ FAIL → show error notification, abort bootstrap + +3. Ollama detection + Probe http://localhost:11434/api/version + ├─ REACHABLE → skip install + └─ UNREACHABLE → install Ollama + ├─ Windows: download OllamaSetup.exe, run /S (silent) + ├─ macOS: brew install ollama || download from ollama.com + └─ Linux: curl -fsSL https://ollama.com/install.sh | sh + Show notification: "Installing local AI model (first time only)..." + +4. Model check + ollama list | grep llama3.2:3b + ├─ FOUND → skip + └─ NOT FOUND → ollama pull llama3.2:3b + Show notification: "Downloading AI model (~2GB)..." + +5. Ready + Tray icon → green + Notification: "DevBrain is ready" +``` + +### Design Principles + +- **Non-blocking:** Daemon starts at step 2. Ollama install (steps 3-4) happens in background. DevBrain is usable immediately — LLM features queue until Ollama is ready. +- **Idempotent:** Every step checks before acting. Running bootstrap 10 times produces the same result. +- **Graceful degradation:** If Ollama install fails, daemon keeps running. Notification suggests cloud LLM fallback via API key in Settings. +- **No internet required:** If offline, daemon runs fine. LLM features deferred until connectivity returns. + +--- + +## Package Manager Distribution + +### Homebrew (macOS + Linux) + +**Tap repository:** `devbrain/homebrew-tap` (separate GitHub repo) + +```ruby +class Devbrain < Formula + desc "Developer's second brain — captures coding sessions, builds knowledge graph" + homepage "https://github.com/devbrain/devbrain" + version "1.0.0" + + if OS.mac? && Hardware::CPU.arm? + url "https://github.com/.../devbrain-osx-arm64.tar.gz" + sha256 "..." + elsif OS.mac? && Hardware::CPU.intel? + url "https://github.com/.../devbrain-osx-x64.tar.gz" + sha256 "..." + elsif OS.linux? && Hardware::CPU.intel? + url "https://github.com/.../devbrain-linux-x64.tar.gz" + sha256 "..." + end + + def install + bin.install "devbrain" + bin.install "devbrain-daemon" + prefix.install "DevBrain.app" if OS.mac? # Electron tray app + end + + # No Homebrew service block — the Electron tray app owns daemon lifecycle. + # Adding a launchd service here would conflict with the tray app on port 37800. + + def post_install + # Tray app handles all user-space bootstrap (config, Ollama, daemon) on first launch. + # Nothing to do here beyond what `install` already did. + end +end +``` + +**Install experience:** +```bash +brew tap devbrain/tap +brew install devbrain +``` + +**Updates:** `brew upgrade devbrain` + +### winget (Windows) + +electron-builder produces an NSIS installer (`.exe`). winget manifest points to it. + +```yaml +PackageIdentifier: DevBrain.DevBrain +PackageVersion: 1.0.0 +PackageName: DevBrain +Publisher: DevBrain +License: Apache-2.0 +InstallerType: exe +Installers: + - Architecture: x64 + InstallerUrl: https://github.com/.../DevBrain-Setup-1.0.0-x64.exe + InstallerSha256: "..." + InstallerSwitches: + Silent: /S + SilentWithProgress: /S +``` + +Submitted via PR to [microsoft/winget-pkgs](https://github.com/microsoft/winget-pkgs). + +**Install experience:** +```powershell +winget install DevBrain +``` + +**Updates:** `winget upgrade DevBrain` + +### APT (Debian/Ubuntu) + +`.deb` package built from `packages/apt/debian/` control files. Hosted on GitHub Pages as an APT repository. + +``` +Package: devbrain +Version: 1.0.0 +Architecture: amd64 +Depends: libgtk-3-0, libnotify4, libnss3 +Description: Developer's second brain +``` + +Post-install (`postinst`) script: +- Creates symlinks in `/usr/local/bin/` for `devbrain` and `devbrain-daemon` +- Registers desktop autostart entry for the tray app (`/etc/xdg/autostart/devbrain.desktop`) +- Does NOT create user config or install Ollama — runs as root, cannot write to `~/.devbrain/`. The tray app handles all user-space bootstrap on first launch. + +**Install experience:** +```bash +# Add repo (one-time) +curl -s https://devbrain.dev/install.gpg | sudo gpg --dearmor -o /usr/share/keyrings/devbrain.gpg +echo "deb [signed-by=/usr/share/keyrings/devbrain.gpg] https://devbrain.dev/apt stable main" | sudo tee /etc/apt/sources.list.d/devbrain.list + +# Install +sudo apt update && sudo apt install devbrain +``` + +**Updates:** `sudo apt upgrade devbrain` + +**One-liner alternative** (wraps repo setup + install for a simpler experience): +```bash +curl -fsSL https://devbrain.dev/install.sh | sh +``` + +This script adds the GPG key, configures the repo, and runs `apt install devbrain` — reducing the 3-command flow to one. + +--- + +## Uninstall Behavior + +Uninstall (via `winget uninstall`, `brew uninstall`, `apt remove`) should: + +1. **Stop the daemon** — kill the running `devbrain-daemon` process +2. **Stop the tray app** — kill the Electron process +3. **Remove binaries** — CLI, daemon, tray app executables +4. **Remove autostart registration** — registry entry (Windows), Login Items (macOS), `.desktop` file (Linux) +5. **Remove PATH entries** — undo any PATH modifications from install + +Uninstall should **NOT**: +- Delete `~/.devbrain/` — contains the user's knowledge graph, settings, and SQLite database. This is user data. +- Uninstall Ollama — the user may use it for other purposes. DevBrain installed it but doesn't own it. + +If the user wants a full purge, they manually delete `~/.devbrain/`. This follows the convention of tools like Docker, VS Code, and Homebrew itself. + +--- + +## CI/CD Pipeline + +### Build Stages + +``` +Stage 1 (parallel): + ├── Test (.NET) — dotnet test DevBrain.slnx + ├── Dashboard Build — npm ci && npm run build + └── Security Scan — TruffleHog + dependency review + +Stage 2: + └── Build .NET Binaries — 6 platforms (win/mac/linux × x64/arm64) + Self-contained, single-file, PublishSingleFile=true + +Stage 3: + └── Build Electron App — 3 platforms (win-x64, mac-x64, linux-x64) + electron-builder produces: + Windows: NSIS installer (.exe) + macOS: DMG + Linux: .deb + AppImage + Embeds: daemon binary + CLI binary + dashboard static files + +Stage 4 (on v* tag only): + ├── GitHub Release — upload all archives + installers + ├── Homebrew Tap Update — auto-PR to devbrain/homebrew-tap + ├── winget Submission — auto-PR to microsoft/winget-pkgs + └── APT Repo Update — push .deb to GitHub Pages +``` + +### Electron Build Job + +```yaml +electron-build: + needs: [build-dotnet, build-dashboard] + strategy: + matrix: + include: + - os: windows-latest + rid: win-x64 + electron-target: nsis + - os: macos-latest + rid: osx-x64 + electron-target: dmg + - os: ubuntu-latest + rid: linux-x64 + electron-target: deb + steps: + - Download .NET binary artifacts (daemon + CLI for this platform) + - Download dashboard dist/ artifact + - Copy binaries to packages/tray/resources/bin/ + - Copy dashboard to packages/tray/resources/wwwroot/ + - npm ci in packages/tray/ + - npx electron-builder --${{ matrix.electron-target }} + - Upload installer artifact +``` + +### Package Manager Publish Jobs + +- **Homebrew:** [homebrew-releaser](https://github.com/Justintime50/homebrew-releaser) GitHub Action auto-updates formula with new version + SHA256 +- **winget:** [winget-create](https://github.com/microsoft/winget-create) GitHub Action submits PR to microsoft/winget-pkgs +- **APT:** Build `.deb`, sign with GPG key (stored in GitHub Secrets), push to `gh-pages` branch + +### Code Signing + +**Deferred.** Ship unsigned for initial release. Both macOS and Windows will show security warnings — acceptable for early adopters. Add signing later ($220/year, half-day setup) as a single PR without architectural changes. + +--- + +## Embedded Binary Bundling + +The Electron tray app embeds the .NET daemon and CLI binaries inside its package: + +### Windows (NSIS installer) + +``` +C:\Program Files\DevBrain\ + DevBrain.exe # Electron tray app + resources/ + bin/ + devbrain-daemon.exe # .NET daemon + devbrain.exe # .NET CLI + wwwroot/ # Dashboard static files +``` + +NSIS post-install adds `C:\Program Files\DevBrain\resources\bin\` to user PATH. + +### macOS (DMG) + +``` +/Applications/DevBrain.app/ + Contents/ + MacOS/ + DevBrain # Electron binary + Resources/ + bin/ + devbrain-daemon # .NET daemon + devbrain # .NET CLI + wwwroot/ # Dashboard static files +``` + +Homebrew formula symlinks CLI binaries to `/usr/local/bin/`. + +### Linux (.deb) + +``` +/opt/devbrain/ + devbrain-tray # Electron tray app + resources/ + bin/ + devbrain-daemon # .NET daemon + devbrain # .NET CLI + wwwroot/ # Dashboard static files +/usr/local/bin/ + devbrain -> /opt/devbrain/resources/bin/devbrain + devbrain-daemon -> /opt/devbrain/resources/bin/devbrain-daemon +``` + +--- + +## Testing + +### Unit Tests (Jest) + +``` +packages/tray/__tests__/ + bootstrap.test.ts # Mock Ollama probe, mock filesystem, verify decision tree + daemon.test.ts # Mock child_process.spawn, simulate health responses, verify restart logic + health.test.ts # Feed health states, assert correct icon/tooltip transitions +``` + +Key scenarios: +- Bootstrap skips Ollama install when already present +- Bootstrap creates config only when missing (idempotent) +- Daemon restarts on crash, stops after 3 failures in 5 minutes +- Health transitions: starting→healthy, healthy→unhealthy, unhealthy→healthy +- Ollama install failure falls back gracefully (notification, no crash) + +### CI Smoke Tests + +New job that runs on each platform VM after package build: + +1. Install the built package (NSIS/DMG/.deb) +2. Verify `devbrain --version` outputs correctly +3. Verify `devbrain-daemon` starts and responds on `/api/v1/health` +4. Verify tray app process launches without crash (exit code 0 after 10s) +5. Verify uninstall cleans up (PATH, autostart, but preserves `~/.devbrain/` data) + +### Existing Tests + +No changes to existing 54 xUnit tests. Daemon and CLI behavior unchanged. + +--- + +## Out of Scope (v1) + +| Item | Rationale | +|------|-----------| +| Code signing | Ship unsigned, add as a follow-up PR ($220/year, half-day setup) | +| Electron autoUpdater | Package managers handle updates | +| In-app settings UI in tray | Dashboard Settings page already exists | +| ARM64 Electron builds | Start x64 only. Add ARM64 macOS when signing is in place | +| Flatpak / Snap | APT + AppImage covers Linux | +| CLI shell completions | Nice-to-have, separate effort | +| Multi-user / team features | DevBrain is a single-dev tool | + +--- + +## File Inventory + +New files to create: + +| File | Purpose | +|------|---------| +| `package.json` (root) | npm workspaces config | +| `packages/tray/package.json` | Electron + electron-builder deps | +| `packages/tray/electron-builder.yml` | Platform build config, autoLaunch, binary paths | +| `packages/tray/tsconfig.json` | TypeScript config for tray app | +| `packages/tray/src/main.ts` | Electron entry — tray icon, context menu | +| `packages/tray/src/bootstrap.ts` | First-run orchestrator | +| `packages/tray/src/daemon.ts` | Daemon spawn/monitor/restart | +| `packages/tray/src/health.ts` | Health polling, tray icon state machine | +| `packages/tray/src/notifications.ts` | OS notification helpers | +| `packages/tray/assets/icon.png` | Tray icon (base) | +| `packages/tray/assets/icon.ico` | Windows tray icon | +| `packages/tray/assets/icon.icns` | macOS tray icon | +| `packages/tray/__tests__/bootstrap.test.ts` | Bootstrap unit tests | +| `packages/tray/__tests__/daemon.test.ts` | Daemon lifecycle tests | +| `packages/tray/__tests__/health.test.ts` | Health state tests | +| `packages/homebrew/devbrain.rb` | Homebrew formula | +| `packages/winget/DevBrain.DevBrain.yaml` | winget manifest | +| `packages/apt/debian/control` | Debian package metadata | +| `packages/apt/debian/postinst` | Post-install script | +| `packages/apt/debian/prerm` | Pre-remove script | +| `packages/apt/debian/rules` | Build rules | +| `.github/workflows/package.yml` | Electron build + package manager publish | + +Files to modify: + +| File | Change | +|------|--------| +| `.github/workflows/build.yml` | Add electron build stage dependency | +| `.gitignore` | Add `packages/tray/dist/`, `packages/tray/node_modules/` | diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..47ba22e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,9917 @@ +{ + "name": "devbrain", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "devbrain", + "workspaces": [ + "dashboard", + "packages/tray" + ] + }, + "dashboard": { + "name": "devbrain-dashboard", + "version": "1.0.0", + "dependencies": { + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router-dom": "^7.14.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.0", + "vite": "^8.0.4" + } + }, + "dashboard/node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "dashboard/node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "dashboard/node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "dashboard/node_modules/@eslint/config-array": { + "version": "0.21.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "dashboard/node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "dashboard/node_modules/@eslint/core": { + "version": "0.17.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "dashboard/node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "dashboard/node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "dashboard/node_modules/@eslint/js": { + "version": "9.39.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "dashboard/node_modules/@eslint/object-schema": { + "version": "2.1.7", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "dashboard/node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "dashboard/node_modules/@humanfs/core": { + "version": "0.19.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "dashboard/node_modules/@humanfs/node": { + "version": "0.16.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "dashboard/node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "dashboard/node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "dashboard/node_modules/@oxc-project/types": { + "version": "0.122.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "dashboard/node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "dashboard/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "dev": true, + "license": "MIT" + }, + "dashboard/node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "dashboard/node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "dashboard/node_modules/@types/react": { + "version": "19.2.14", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "dashboard/node_modules/@types/react-dom": { + "version": "19.2.3", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "dashboard/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.58.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/type-utils": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "dashboard/node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "dashboard/node_modules/@typescript-eslint/parser": { + "version": "8.58.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "dashboard/node_modules/@typescript-eslint/project-service": { + "version": "8.58.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.0", + "@typescript-eslint/types": "^8.58.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "dashboard/node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "dashboard/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "dashboard/node_modules/@typescript-eslint/type-utils": { + "version": "8.58.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "dashboard/node_modules/@typescript-eslint/types": { + "version": "8.58.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "dashboard/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.58.0", + "@typescript-eslint/tsconfig-utils": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "dashboard/node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "dashboard/node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "dashboard/node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "dashboard/node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "dashboard/node_modules/@typescript-eslint/utils": { + "version": "8.58.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "dashboard/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "dashboard/node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "dashboard/node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "dashboard/node_modules/acorn": { + "version": "8.16.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "dashboard/node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "dashboard/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "dashboard/node_modules/brace-expansion": { + "version": "1.1.13", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "dashboard/node_modules/cookie": { + "version": "1.1.1", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "dashboard/node_modules/csstype": { + "version": "3.2.3", + "dev": true, + "license": "MIT" + }, + "dashboard/node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "dashboard/node_modules/eslint": { + "version": "9.39.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "dashboard/node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "dashboard/node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "dashboard/node_modules/eslint-scope": { + "version": "8.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "dashboard/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "dashboard/node_modules/espree": { + "version": "10.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "dashboard/node_modules/esquery": { + "version": "1.7.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "dashboard/node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "dashboard/node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "dashboard/node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "dashboard/node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "dashboard/node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "dashboard/node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "dashboard/node_modules/flat-cache": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "dashboard/node_modules/flatted": { + "version": "3.4.2", + "dev": true, + "license": "ISC" + }, + "dashboard/node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "dashboard/node_modules/globals": { + "version": "17.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "dashboard/node_modules/hermes-estree": { + "version": "0.25.1", + "dev": true, + "license": "MIT" + }, + "dashboard/node_modules/hermes-parser": { + "version": "0.25.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "dashboard/node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "dashboard/node_modules/import-fresh": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "dashboard/node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "dashboard/node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "dashboard/node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "dashboard/node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "dashboard/node_modules/lightningcss": { + "version": "1.32.0", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "dashboard/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "dashboard/node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "dashboard/node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "dashboard/node_modules/minimatch": { + "version": "3.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "dashboard/node_modules/nanoid": { + "version": "3.3.11", + "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" + } + }, + "dashboard/node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "dashboard/node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "dashboard/node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "dashboard/node_modules/postcss": { + "version": "8.5.8", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "dashboard/node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "dashboard/node_modules/react": { + "version": "19.2.4", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "dashboard/node_modules/react-dom": { + "version": "19.2.4", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "dashboard/node_modules/react-router": { + "version": "7.14.0", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "dashboard/node_modules/react-router-dom": { + "version": "7.14.0", + "license": "MIT", + "dependencies": { + "react-router": "7.14.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "dashboard/node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "dashboard/node_modules/rolldown": { + "version": "1.0.0-rc.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, + "dashboard/node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "dev": true, + "license": "MIT" + }, + "dashboard/node_modules/scheduler": { + "version": "0.27.0", + "license": "MIT" + }, + "dashboard/node_modules/set-cookie-parser": { + "version": "2.7.2", + "license": "MIT" + }, + "dashboard/node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "dashboard/node_modules/ts-api-utils": { + "version": "2.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "dashboard/node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "dashboard/node_modules/typescript-eslint": { + "version": "8.58.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "dashboard/node_modules/vite": { + "version": "8.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "dashboard/node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "dashboard/node_modules/zod": { + "version": "4.3.6", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "dashboard/node_modules/zod-validation-error": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@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.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@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.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/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/@babel/helper-compilation-targets/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/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/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, + "license": "MIT", + "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/fuses/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/fuses/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, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", + "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "got": "^11.7.0", + "graceful-fs": "^4.2.11", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^11.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^7.5.6", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/rebuild/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@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, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "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.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/windows-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, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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, + "license": "ISC", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", + "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", + "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.3.0", + "jest-config": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-resolve-dependencies": "30.3.0", + "jest-runner": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "jest-watcher": "30.3.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.3.0", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", + "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/types": "30.3.0", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", + "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/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, + "license": "MIT" + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@jest/reporters/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, + "license": "ISC", + "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/@jest/reporters/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", + "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", + "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/types": "30.3.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", + "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", + "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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" + } + ], + "license": "Apache-2.0", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/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, + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "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/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.0.tgz", + "integrity": "sha512-m2xozxSfCIxjDdvbhIWazlP2i2aha/iUmbl94alpsIbd3iLTfeXgfBVbwyWogB6l++istyGZqamgA/EcqYf+Bg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.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/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "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, + "license": "MIT", + "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/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.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, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "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/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "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/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.12", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz", + "integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", + "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.3", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.8.1", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.3", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.8.1", + "electron-builder-squirrel-windows": "26.8.1" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "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": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/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/app-builder-lib/node_modules/@electron/get/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/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=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, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/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, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dev": true, + "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, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/babel-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", + "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.3.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz", + "integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz", + "integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "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, + "license": "MIT", + "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" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", + "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "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, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "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.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "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.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "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" + } + ], + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", + "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.12", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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, + "license": "MIT" + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/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, + "license": "ISC", + "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/cacache/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, + "license": "ISC" + }, + "node_modules/cacache/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001786", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz", + "integrity": "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==", + "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/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "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/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "MIT", + "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/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "optional": 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, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "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, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/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, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "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, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "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/devbrain-dashboard": { + "resolved": "dashboard", + "link": true + }, + "node_modules/devbrain-tray": { + "resolved": "packages/tray", + "link": 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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", + "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "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, + "license": "MIT", + "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.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "BSD-2-Clause", + "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, + "license": "BSD-2-Clause", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "36.9.5", + "resolved": "https://registry.npmjs.org/electron/-/electron-36.9.5.tgz", + "integrity": "sha512-1UCss2IqxqujSzg/2jkRjuiT3G+EEXgd6UKB5kUekwQW1LJ6d4QCr8YItfC3Rr9VIGRDJ29eOERmnRNO1Eh+NA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^22.7.7", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", + "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.8.1", + "fs-extra": "^10.1.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": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", + "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "electron-winstaller": "5.4.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, + "license": "MIT", + "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.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-log": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.4.3.tgz", + "integrity": "sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/electron-publish": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", + "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "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, + "license": "MIT", + "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.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.332", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.332.tgz", + "integrity": "sha512-7OOtytmh/rINMLwaFTbcMVvYXO3AUm029X0LcyfYk0B557RlPkdpTpnH9+htMlfu5dKwOmT0+Zs2Aw+lnn6TeQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/electron/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/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "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-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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/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", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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, + "license": "Apache-2.0" + }, + "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" + ], + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "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, + "license": "Apache-2.0", + "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, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "ISC", + "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, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "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": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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, + "license": "ISC" + }, + "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, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "ISC", + "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, + "license": "MIT", + "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-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "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, + "license": "ISC", + "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.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "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/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "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, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.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, + "license": "MIT", + "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, + "license": "MIT", + "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" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "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, + "license": "ISC", + "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, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "BlueOak-1.0.0", + "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, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", + "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.3.0", + "@jest/types": "30.3.0", + "import-local": "^3.2.0", + "jest-cli": "30.3.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz", + "integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.3.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz", + "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "p-limit": "^3.1.0", + "pretty-format": "30.3.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz", + "integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", + "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.3.0", + "@jest/types": "30.3.0", + "babel-jest": "30.3.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.3.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-runner": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "parse-json": "^5.2.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/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, + "license": "MIT" + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-config/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, + "license": "ISC", + "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/jest-config/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-diff": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.3.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz", + "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "jest-util": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz", + "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", + "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", + "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz", + "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz", + "integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", + "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/environment": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-leak-detector": "30.3.0", + "jest-message-util": "30.3.0", + "jest-resolve": "30.3.0", + "jest-runtime": "30.3.0", + "jest-util": "30.3.0", + "jest-watcher": "30.3.0", + "jest-worker": "30.3.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz", + "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/globals": "30.3.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime/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, + "license": "MIT" + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-runtime/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, + "license": "ISC", + "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/jest-runtime/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-snapshot": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", + "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "pretty-format": "30.3.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", + "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", + "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.3.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "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.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "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-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "BlueOak-1.0.0", + "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, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.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, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.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, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz", + "integrity": "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-api-version/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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, + "license": "MIT", + "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/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "BlueOak-1.0.0", + "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, + "license": "ISC" + }, + "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, + "license": "MIT", + "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.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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-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, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "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-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "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/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" + } + ], + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "WTFPL OR ISC", + "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, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.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/serialize-error/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/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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "ISC" + }, + "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, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "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, + "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, + "license": "MIT", + "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": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/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, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "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/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "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, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/ts-jest": { + "version": "29.4.9", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz", + "integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.4", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "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, + "license": "BSD-2-Clause", + "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, + "license": "(WTFPL OR MIT)" + }, + "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, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "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/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "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, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/write-file-atomic/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, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "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, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/tray": { + "name": "devbrain-tray", + "version": "1.0.0", + "dependencies": { + "electron-log": "^5.3.0" + }, + "devDependencies": { + "@types/jest": "^30.0.0", + "@types/node": "^24.12.2", + "electron": "^36.0.0", + "electron-builder": "^26.0.0", + "jest": "^30.0.0", + "ts-jest": "^29.3.0", + "typescript": "~6.0.2" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..eafb200 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "devbrain", + "private": true, + "workspaces": [ + "dashboard", + "packages/tray" + ] +} diff --git a/packages/apt/debian/control b/packages/apt/debian/control new file mode 100644 index 0000000..5bafe13 --- /dev/null +++ b/packages/apt/debian/control @@ -0,0 +1,12 @@ +Package: devbrain +Version: 1.0.0 +Section: devel +Priority: optional +Architecture: amd64 +Depends: libgtk-3-0, libnotify4, libnss3, libxss1, libxtst6, libatspi2.0-0 +Maintainer: DevBrain +Homepage: https://github.com/devbrain/devbrain +Description: Developer's second brain + DevBrain is a background daemon that passively captures AI coding sessions, + builds a knowledge graph of decisions and dead ends, and surfaces proactive + insights including morning briefings, pattern detection, and semantic search. diff --git a/packages/apt/debian/devbrain.desktop b/packages/apt/debian/devbrain.desktop new file mode 100644 index 0000000..fc139cf --- /dev/null +++ b/packages/apt/debian/devbrain.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=DevBrain +Exec=/opt/devbrain/devbrain-tray +Icon=/opt/devbrain/resources/assets/icon.png +Comment=Developer's second brain +Categories=Development; +X-GNOME-Autostart-enabled=true +StartupNotify=false +Terminal=false diff --git a/packages/apt/debian/postinst b/packages/apt/debian/postinst new file mode 100644 index 0000000..ce9e68e --- /dev/null +++ b/packages/apt/debian/postinst @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +ln -sf /opt/devbrain/resources/bin/devbrain /usr/local/bin/devbrain +ln -sf /opt/devbrain/resources/bin/devbrain-daemon /usr/local/bin/devbrain-daemon + +mkdir -p /etc/xdg/autostart +cp /opt/devbrain/devbrain.desktop /etc/xdg/autostart/devbrain.desktop 2>/dev/null || true diff --git a/packages/apt/debian/prerm b/packages/apt/debian/prerm new file mode 100644 index 0000000..7c8a21f --- /dev/null +++ b/packages/apt/debian/prerm @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +pkill -f devbrain-daemon 2>/dev/null || true +pkill -f devbrain-tray 2>/dev/null || true + +rm -f /usr/local/bin/devbrain +rm -f /usr/local/bin/devbrain-daemon +rm -f /etc/xdg/autostart/devbrain.desktop + +# ~/.devbrain/ is intentionally preserved (user data) diff --git a/packages/apt/debian/rules b/packages/apt/debian/rules new file mode 100644 index 0000000..579040b --- /dev/null +++ b/packages/apt/debian/rules @@ -0,0 +1,11 @@ +#!/usr/bin/make -f + +%: + dh $@ + +override_dh_auto_build: + true + +override_dh_auto_install: + mkdir -p debian/devbrain/opt/devbrain + cp -r . debian/devbrain/opt/devbrain/ diff --git a/packages/homebrew/devbrain.rb b/packages/homebrew/devbrain.rb new file mode 100644 index 0000000..11f6524 --- /dev/null +++ b/packages/homebrew/devbrain.rb @@ -0,0 +1,36 @@ +class Devbrain < Formula + desc "Developer's second brain - captures coding sessions, builds knowledge graph" + homepage "https://github.com/devbrain/devbrain" + version "1.0.0" + + if OS.mac? && Hardware::CPU.arm? + url "https://github.com/devbrain/devbrain/releases/download/v1.0.0/devbrain-osx-arm64.tar.gz" + sha256 "PLACEHOLDER_SHA256" + elsif OS.mac? && Hardware::CPU.intel? + url "https://github.com/devbrain/devbrain/releases/download/v1.0.0/devbrain-osx-x64.tar.gz" + sha256 "PLACEHOLDER_SHA256" + elsif OS.linux? && Hardware::CPU.intel? + url "https://github.com/devbrain/devbrain/releases/download/v1.0.0/devbrain-linux-x64.tar.gz" + sha256 "PLACEHOLDER_SHA256" + elsif OS.linux? && Hardware::CPU.arm? + url "https://github.com/devbrain/devbrain/releases/download/v1.0.0/devbrain-linux-arm64.tar.gz" + sha256 "PLACEHOLDER_SHA256" + end + + def install + bin.install "devbrain" + bin.install "devbrain-daemon" + # Electron tray app is distributed via DMG/Cask, not through this formula. + # This formula installs CLI binaries only. + end + + # No Homebrew service block - the Electron tray app owns daemon lifecycle. + + def post_install + # Tray app handles all user-space bootstrap on first launch. + end + + test do + assert_match "devbrain", shell_output("#{bin}/devbrain --version") + end +end diff --git a/packages/tray/__tests__/bootstrap.test.ts b/packages/tray/__tests__/bootstrap.test.ts new file mode 100644 index 0000000..d327c00 --- /dev/null +++ b/packages/tray/__tests__/bootstrap.test.ts @@ -0,0 +1,108 @@ +import { Bootstrap } from "../src/bootstrap"; +import * as fs from "fs"; +import * as child_process from "child_process"; + +jest.mock("fs"); +jest.mock("child_process"); + +const mockExistsSync = fs.existsSync as jest.MockedFunction; +const mockWriteFileSync = fs.writeFileSync as jest.MockedFunction; +const mockMkdirSync = fs.mkdirSync as jest.MockedFunction; +const mockExecFileSync = child_process.execFileSync as jest.MockedFunction; + +const mockFetch = jest.fn(); +global.fetch = mockFetch as unknown as typeof fetch; + +jest.mock("../src/paths", () => ({ + dataDir: () => "/mock/.devbrain", + settingsPath: () => "/mock/.devbrain/settings.toml", +})); + +jest.mock("../src/notifications", () => ({ + showInfo: jest.fn(), + showError: jest.fn(), + showProgress: jest.fn(() => ({ close: jest.fn() })), +})); + +describe("Bootstrap", () => { + let bootstrap: Bootstrap; + + beforeEach(() => { + jest.clearAllMocks(); + mockExistsSync.mockReturnValue(false); + mockMkdirSync.mockReturnValue(undefined); + bootstrap = new Bootstrap(); + }); + + describe("ensureConfig()", () => { + it("creates settings.toml when missing", async () => { + mockExistsSync.mockReturnValue(false); + await bootstrap.ensureConfig(); + expect(mockWriteFileSync).toHaveBeenCalledWith( + "/mock/.devbrain/settings.toml", + expect.stringContaining("[daemon]") + ); + }); + + it("skips config creation when file exists", async () => { + mockExistsSync.mockImplementation((p) => String(p).endsWith("settings.toml") || String(p) === "/mock/.devbrain"); + await bootstrap.ensureConfig(); + expect(mockWriteFileSync).not.toHaveBeenCalled(); + }); + }); + + describe("isOllamaInstalled()", () => { + it("returns true when Ollama API responds", async () => { + mockFetch.mockResolvedValueOnce({ ok: true }); + const result = await bootstrap.isOllamaInstalled(); + expect(result).toBe(true); + }); + + it("returns false when Ollama API is unreachable", async () => { + mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED")); + const result = await bootstrap.isOllamaInstalled(); + expect(result).toBe(false); + }); + }); + + describe("isModelPulled()", () => { + it("returns true when model is in ollama list output", async () => { + mockExecFileSync.mockReturnValue(Buffer.from("llama3.2:3b\t3.2GB\n")); + const result = await bootstrap.isModelPulled("llama3.2:3b"); + expect(result).toBe(true); + }); + + it("returns false when model is not found", async () => { + mockExecFileSync.mockReturnValue(Buffer.from("")); + const result = await bootstrap.isModelPulled("llama3.2:3b"); + expect(result).toBe(false); + }); + + it("returns false when ollama command fails", async () => { + mockExecFileSync.mockImplementation(() => { + throw new Error("command not found"); + }); + const result = await bootstrap.isModelPulled("llama3.2:3b"); + expect(result).toBe(false); + }); + }); + + describe("idempotency", () => { + it("running ensureConfig twice does not overwrite existing config", async () => { + let callCount = 0; + mockExistsSync.mockImplementation((p) => { + const pathStr = String(p); + if (pathStr.endsWith("settings.toml")) { + callCount++; + return callCount > 1; + } + return pathStr === "/mock/.devbrain"; + }); + + await bootstrap.ensureConfig(); + await bootstrap.ensureConfig(); + + expect(mockWriteFileSync).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/tray/__tests__/daemon.test.ts b/packages/tray/__tests__/daemon.test.ts new file mode 100644 index 0000000..7fe2d8f --- /dev/null +++ b/packages/tray/__tests__/daemon.test.ts @@ -0,0 +1,153 @@ +import { DaemonManager } from "../src/daemon"; +import * as child_process from "child_process"; +import * as fs from "fs"; + +jest.mock("child_process"); +jest.mock("fs"); + +const mockSpawn = child_process.spawn as jest.MockedFunction; +const mockExecFileSync = child_process.execFileSync as jest.MockedFunction; +const mockExistsSync = fs.existsSync as jest.MockedFunction; +const mockReadFileSync = fs.readFileSync as jest.MockedFunction; +const mockWriteFileSync = fs.writeFileSync as jest.MockedFunction; +const mockUnlinkSync = fs.unlinkSync as jest.MockedFunction; +const mockMkdirSync = fs.mkdirSync as jest.MockedFunction; +const mockOpenSync = fs.openSync as jest.MockedFunction; + +const mockFetch = jest.fn(); +global.fetch = mockFetch as unknown as typeof fetch; + +jest.mock("../src/paths", () => ({ + binaryPath: (name: string) => `/mock/bin/${name}`, + pidPath: () => "/mock/.devbrain/daemon.pid", + dataDir: () => "/mock/.devbrain", + stoppedSentinelPath: () => "/mock/.devbrain/stopped", + logsDir: () => "/mock/.devbrain/logs", +})); + +jest.mock("../src/notifications", () => ({ + showInfo: jest.fn(), + showError: jest.fn(), + showProgress: jest.fn(() => ({ close: jest.fn() })), +})); + +describe("DaemonManager", () => { + let daemon: DaemonManager; + let mockProcess: Partial; + + beforeEach(() => { + jest.clearAllMocks(); + mockProcess = { + pid: 12345, + on: jest.fn(), + unref: jest.fn(), + }; + mockSpawn.mockReturnValue(mockProcess as child_process.ChildProcess); + mockExistsSync.mockReturnValue(false); + mockMkdirSync.mockReturnValue(undefined); + mockOpenSync.mockReturnValue(3); + daemon = new DaemonManager(); + }); + + describe("start()", () => { + it("spawns devbrain-daemon as a detached process", async () => { + mockFetch.mockResolvedValue({ ok: true }); + await daemon.start(); + expect(mockSpawn).toHaveBeenCalledWith( + "/mock/bin/devbrain-daemon", + [], + expect.objectContaining({ detached: true }) + ); + }); + + it("redirects stderr to a log file", async () => { + mockFetch.mockResolvedValue({ ok: true }); + await daemon.start(); + const spawnCall = mockSpawn.mock.calls[0]; + const options = spawnCall[2] as child_process.SpawnOptions; + expect(Array.isArray(options.stdio)).toBe(true); + const stdio = options.stdio as Array; + expect(stdio[0]).toBe("ignore"); + expect(stdio[1]).toBe("ignore"); + expect(typeof stdio[2]).toBe("number"); + }); + + it("writes PID file after spawning", async () => { + mockFetch.mockResolvedValue({ ok: true }); + await daemon.start(); + expect(mockWriteFileSync).toHaveBeenCalledWith( + "/mock/.devbrain/daemon.pid", + "12345" + ); + }); + + it("clears stopped sentinel before starting", async () => { + mockExistsSync.mockImplementation((p) => + String(p) === "/mock/.devbrain/stopped" + ); + mockFetch.mockResolvedValue({ ok: true }); + await daemon.start(); + expect(mockUnlinkSync).toHaveBeenCalledWith("/mock/.devbrain/stopped"); + }); + }); + + describe("stop()", () => { + it("writes sentinel and kills verified daemon process", async () => { + mockExistsSync.mockImplementation((p) => + String(p) === "/mock/.devbrain/daemon.pid" + ); + mockReadFileSync.mockReturnValue("12345"); + // isDaemonProcess calls execFileSync with encoding: "utf-8", returns string + (mockExecFileSync as jest.Mock).mockReturnValue('"devbrain-daemon.exe","12345"'); + const killMock = jest.fn(); + jest.spyOn(process, "kill").mockImplementation(killMock); + + await daemon.stop(); + + expect(mockWriteFileSync).toHaveBeenCalledWith( + "/mock/.devbrain/stopped", + "stopped" + ); + expect(killMock).toHaveBeenCalledWith(12345); + expect(mockUnlinkSync).toHaveBeenCalledWith("/mock/.devbrain/daemon.pid"); + (process.kill as jest.Mock).mockRestore(); + }); + + it("does not kill process if PID belongs to a different process", async () => { + mockExistsSync.mockImplementation((p) => + String(p) === "/mock/.devbrain/daemon.pid" + ); + mockReadFileSync.mockReturnValue("12345"); + (mockExecFileSync as jest.Mock).mockReturnValue('"chrome.exe","12345"'); + const killMock = jest.fn(); + jest.spyOn(process, "kill").mockImplementation(killMock); + + await daemon.stop(); + + expect(killMock).not.toHaveBeenCalled(); + expect(mockUnlinkSync).toHaveBeenCalledWith("/mock/.devbrain/daemon.pid"); + (process.kill as jest.Mock).mockRestore(); + }); + }); + + describe("auto-restart", () => { + it("starts with zero crash count", () => { + expect(daemon.crashCount).toBe(0); + }); + + it("stops restarting after 3 crashes", () => { + daemon.recordCrash(); + daemon.recordCrash(); + daemon.recordCrash(); + expect(daemon.shouldRestart()).toBe(false); + }); + + it("allows restart after manual reset", () => { + daemon.recordCrash(); + daemon.recordCrash(); + daemon.recordCrash(); + daemon.resetCrashCount(); + expect(daemon.shouldRestart()).toBe(true); + }); + }); +}); diff --git a/packages/tray/__tests__/health.test.ts b/packages/tray/__tests__/health.test.ts new file mode 100644 index 0000000..f8510e1 --- /dev/null +++ b/packages/tray/__tests__/health.test.ts @@ -0,0 +1,75 @@ +import { HealthMonitor, HealthState } from "../src/health"; + +const mockFetch = jest.fn(); +global.fetch = mockFetch as unknown as typeof fetch; + +describe("HealthMonitor", () => { + let monitor: HealthMonitor; + let states: HealthState[]; + + beforeEach(() => { + jest.useFakeTimers(); + states = []; + monitor = new HealthMonitor(1000); + monitor.on("stateChange", (s: HealthState) => states.push(s)); + mockFetch.mockReset(); + }); + + afterEach(() => { + monitor.stop(); + jest.useRealTimers(); + }); + + it("starts in 'starting' state", () => { + expect(monitor.state).toBe("starting"); + }); + + it("performs immediate health check on start", async () => { + mockFetch.mockResolvedValueOnce({ ok: true }); + monitor.start(); + // Flush the immediate async check() + await jest.advanceTimersByTimeAsync(0); + expect(monitor.state).toBe("healthy"); + expect(states).toEqual(["healthy"]); + }); + + it("transitions to 'unhealthy' on failed health check", async () => { + mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED")); + monitor.start(); + await jest.advanceTimersByTimeAsync(0); + expect(monitor.state).toBe("unhealthy"); + expect(states).toEqual(["unhealthy"]); + }); + + it("transitions healthy -> unhealthy -> healthy", async () => { + mockFetch + .mockResolvedValueOnce({ ok: true }) // immediate check + .mockRejectedValueOnce(new Error("ECONNREFUSED")) // 1s interval + .mockResolvedValueOnce({ ok: true }); // 2s interval + monitor.start(); + await jest.advanceTimersByTimeAsync(0); // immediate + await jest.advanceTimersByTimeAsync(1000); // first interval + await jest.advanceTimersByTimeAsync(1000); // second interval + expect(states).toEqual(["healthy", "unhealthy", "healthy"]); + }); + + it("does not emit duplicate states", async () => { + mockFetch + .mockResolvedValueOnce({ ok: true }) // immediate check + .mockResolvedValueOnce({ ok: true }); // 1s interval (same state) + monitor.start(); + await jest.advanceTimersByTimeAsync(0); + await jest.advanceTimersByTimeAsync(1000); + expect(states).toEqual(["healthy"]); // Only one emission + }); + + it("stop() clears the polling interval", async () => { + mockFetch.mockResolvedValue({ ok: true }); + monitor.start(); + await jest.advanceTimersByTimeAsync(0); + monitor.stop(); + mockFetch.mockReset(); + await jest.advanceTimersByTimeAsync(5000); + expect(mockFetch).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/tray/assets/icon-red.png b/packages/tray/assets/icon-red.png new file mode 100644 index 0000000..48cdce8 --- /dev/null +++ b/packages/tray/assets/icon-red.png @@ -0,0 +1 @@ +placeholder diff --git a/packages/tray/assets/icon-yellow.png b/packages/tray/assets/icon-yellow.png new file mode 100644 index 0000000..48cdce8 --- /dev/null +++ b/packages/tray/assets/icon-yellow.png @@ -0,0 +1 @@ +placeholder diff --git a/packages/tray/assets/icon.icns b/packages/tray/assets/icon.icns new file mode 100644 index 0000000..48cdce8 --- /dev/null +++ b/packages/tray/assets/icon.icns @@ -0,0 +1 @@ +placeholder diff --git a/packages/tray/assets/icon.ico b/packages/tray/assets/icon.ico new file mode 100644 index 0000000..48cdce8 --- /dev/null +++ b/packages/tray/assets/icon.ico @@ -0,0 +1 @@ +placeholder diff --git a/packages/tray/assets/icon.png b/packages/tray/assets/icon.png new file mode 100644 index 0000000..48cdce8 --- /dev/null +++ b/packages/tray/assets/icon.png @@ -0,0 +1 @@ +placeholder diff --git a/packages/tray/build/installer.nsh b/packages/tray/build/installer.nsh new file mode 100644 index 0000000..7a606a4 --- /dev/null +++ b/packages/tray/build/installer.nsh @@ -0,0 +1,7 @@ +!macro customInstall + nsExec::ExecToLog 'setx PATH "%PATH%;$INSTDIR\resources\bin"' +!macroend + +!macro customUnInstall + ; PATH cleanup is complex in NSIS — users can manually clean up +!macroend diff --git a/packages/tray/build/linux-after-install.sh b/packages/tray/build/linux-after-install.sh new file mode 100644 index 0000000..3fd00c8 --- /dev/null +++ b/packages/tray/build/linux-after-install.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e + +ln -sf /opt/DevBrain/resources/bin/devbrain /usr/local/bin/devbrain +ln -sf /opt/DevBrain/resources/bin/devbrain-daemon /usr/local/bin/devbrain-daemon + +mkdir -p /etc/xdg/autostart +cat > /etc/xdg/autostart/devbrain.desktop << 'EOF' +[Desktop Entry] +Type=Application +Name=DevBrain +Exec=/opt/DevBrain/devbrain-tray +Icon=/opt/DevBrain/resources/assets/icon.png +Comment=Developer's second brain +Categories=Development; +X-GNOME-Autostart-enabled=true +StartupNotify=false +Terminal=false +EOF diff --git a/packages/tray/build/linux-after-remove.sh b/packages/tray/build/linux-after-remove.sh new file mode 100644 index 0000000..a28fc2c --- /dev/null +++ b/packages/tray/build/linux-after-remove.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +rm -f /usr/local/bin/devbrain +rm -f /usr/local/bin/devbrain-daemon +rm -f /etc/xdg/autostart/devbrain.desktop + +pkill -f devbrain-daemon 2>/dev/null || true +pkill -f devbrain-tray 2>/dev/null || true + +# NOTE: ~/.devbrain/ is intentionally preserved (user data) diff --git a/packages/tray/electron-builder.yml b/packages/tray/electron-builder.yml new file mode 100644 index 0000000..955115b --- /dev/null +++ b/packages/tray/electron-builder.yml @@ -0,0 +1,70 @@ +appId: com.devbrain.tray +productName: DevBrain +copyright: Copyright 2026 DevBrain + +directories: + output: release + +extraResources: + - from: resources/bin/ + to: bin/ + filter: + - "**/*" + - from: resources/wwwroot/ + to: wwwroot/ + filter: + - "**/*" + - from: assets/ + to: assets/ + filter: + - "*.png" + - "*.ico" + - "*.icns" + +win: + target: + - target: nsis + arch: [x64] + icon: assets/icon.ico + +nsis: + oneClick: true + allowToChangeInstallationDirectory: false + perMachine: false + installerIcon: assets/icon.ico + include: build/installer.nsh + +mac: + target: + - target: dmg + arch: [x64] + icon: assets/icon.icns + category: public.app-category.developer-tools + +dmg: + contents: + - x: 130 + y: 220 + - x: 410 + y: 220 + type: link + path: /Applications + +linux: + target: + - target: deb + arch: [x64] + - target: AppImage + arch: [x64] + icon: assets/icon.png + category: Development + desktop: + StartupWMClass: DevBrain + +deb: + depends: + - libgtk-3-0 + - libnotify4 + - libnss3 + afterInstall: build/linux-after-install.sh + afterRemove: build/linux-after-remove.sh diff --git a/packages/tray/jest.config.js b/packages/tray/jest.config.js new file mode 100644 index 0000000..d568ad9 --- /dev/null +++ b/packages/tray/jest.config.js @@ -0,0 +1,19 @@ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", + roots: ["/__tests__"], + testMatch: ["**/*.test.ts"], + transform: { + "^.+\\.ts$": ["ts-jest", { + tsconfig: { + target: "ES2022", + module: "commonjs", + lib: ["ES2022"], + strict: true, + esModuleInterop: true, + skipLibCheck: true, + types: ["jest", "node"], + }, + }], + }, +}; diff --git a/packages/tray/package.json b/packages/tray/package.json new file mode 100644 index 0000000..3bf364b --- /dev/null +++ b/packages/tray/package.json @@ -0,0 +1,23 @@ +{ + "name": "devbrain-tray", + "version": "1.0.0", + "private": true, + "main": "dist/main.js", + "scripts": { + "build": "tsc", + "start": "npm run build && electron dist/main.js", + "test": "jest --config jest.config.js", + "pack": "npm run build && electron-builder --dir", + "dist": "npm run build && electron-builder" + }, + "dependencies": {}, + "devDependencies": { + "@types/node": "^24.12.2", + "electron": "^36.0.0", + "electron-builder": "^26.0.0", + "jest": "^30.0.0", + "@types/jest": "^30.0.0", + "ts-jest": "^29.3.0", + "typescript": "~6.0.2" + } +} diff --git a/packages/tray/src/bootstrap.ts b/packages/tray/src/bootstrap.ts new file mode 100644 index 0000000..25d583f --- /dev/null +++ b/packages/tray/src/bootstrap.ts @@ -0,0 +1,205 @@ +import * as fs from "fs"; +import { execFileSync } from "child_process"; +import { dataDir, settingsPath } from "./paths"; +import { showInfo, showError, showProgress } from "./notifications"; + +const OLLAMA_API = "http://localhost:11434/api/version"; +const DEFAULT_MODEL = "llama3.2:3b"; + +const DEFAULT_SETTINGS = `[daemon] +port = 37800 +log_level = "info" + +[capture] +enabled = true +sources = ["ai-sessions"] +privacy_mode = "redact" +max_observation_size_kb = 512 +thread_gap_hours = 2 + +[storage] +sqlite_max_size_mb = 2048 +retention_days = 365 + +[llm.local] +enabled = true +provider = "ollama" +model = "llama3.2:3b" +endpoint = "http://localhost:11434" +max_concurrent = 2 + +[llm.cloud] +enabled = true +provider = "anthropic" +api_key_env = "DEVBRAIN_CLOUD_API_KEY" +max_daily_requests = 50 + +[agents.briefing] +enabled = true +schedule = "0 7 * * *" + +[agents.dead_end] +enabled = true +sensitivity = "medium" + +[agents.compression] +enabled = true +idle_minutes = 60 +`; + +export class Bootstrap { + async ensureConfig(): Promise { + const dir = dataDir(); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + const settings = settingsPath(); + if (!fs.existsSync(settings)) { + fs.writeFileSync(settings, DEFAULT_SETTINGS); + } + } + + async isOllamaInstalled(): Promise { + try { + const res = await fetch(OLLAMA_API); + return res.ok; + } catch { + return false; + } + } + + async isModelPulled(model: string): Promise { + try { + const output = execFileSync("ollama", ["list"], { + encoding: "utf-8", + timeout: 10000, + }); + return output.includes(model); + } catch { + return false; + } + } + + async installOllama(): Promise { + showProgress("Setup", "Installing local AI runtime (first time only)..."); + + try { + if (process.platform === "win32") { + await this.installOllamaWindows(); + } else if (process.platform === "darwin") { + await this.installOllamaMac(); + } else { + await this.installOllamaLinux(); + } + return true; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + showError( + "Ollama install failed", + `Could not install Ollama: ${msg}. DevBrain works without it — add a cloud API key in Settings.` + ); + return false; + } + } + + async pullModel(model: string): Promise { + showProgress("Setup", "Downloading AI model (~2GB)..."); + + try { + execFileSync("ollama", ["pull", model], { + timeout: 600000, + stdio: "ignore", + }); + return true; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + showError("Model download failed", `Could not download ${model}: ${msg}`); + return false; + } + } + + async run(startDaemon: () => Promise): Promise { + await this.ensureConfig(); + await startDaemon(); + + this.bootstrapOllama().catch(() => { + // Errors already shown via notifications + }); + } + + private async bootstrapOllama(): Promise { + const installed = await this.isOllamaInstalled(); + if (!installed) { + const success = await this.installOllama(); + if (!success) return; + } + + const pulled = await this.isModelPulled(DEFAULT_MODEL); + if (!pulled) { + await this.pullModel(DEFAULT_MODEL); + } + + showInfo("Ready", "DevBrain is ready with local AI."); + } + + private async installOllamaWindows(): Promise { + // Use winget if available (preferred — signed package, no raw download) + try { + execFileSync("winget", ["install", "Ollama.Ollama", "--accept-source-agreements", "--accept-package-agreements"], { + timeout: 300000, + stdio: "ignore", + }); + return; + } catch { + // winget not available — fall back to direct download with hash verification + } + + const tmpPath = `${process.env.TEMP || "C:\\Temp"}\\OllamaSetup.exe`; + execFileSync("powershell", [ + "-Command", + `Invoke-WebRequest -Uri 'https://ollama.com/download/OllamaSetup.exe' -OutFile '${tmpPath}'`, + ], { timeout: 300000 }); + + // Verify the downloaded file has a valid Authenticode signature + execFileSync("powershell", [ + "-Command", + `$sig = Get-AuthenticodeSignature '${tmpPath}'; if ($sig.Status -ne 'Valid') { throw 'Invalid signature on OllamaSetup.exe' }`, + ], { timeout: 30000 }); + + execFileSync(tmpPath, ["/S"], { timeout: 300000 }); + } + + private async installOllamaMac(): Promise { + // Prefer Homebrew — signed formula, integrity verified by brew + try { + execFileSync("brew", ["install", "ollama"], { timeout: 300000 }); + return; + } catch { + // Homebrew not available + } + + // Fallback: download the official macOS app bundle + // Note: curl | sh is avoided due to security concerns (no integrity verification). + // Users without Homebrew should install Ollama manually from https://ollama.com + throw new Error( + "Homebrew is not available. Please install Ollama manually from https://ollama.com" + ); + } + + private async installOllamaLinux(): Promise { + // Prefer system package manager if available + try { + // Try apt (Debian/Ubuntu) + execFileSync("apt-get", ["install", "-y", "ollama"], { timeout: 300000, stdio: "ignore" }); + return; + } catch { + // apt not available or ollama not in repos + } + + // Fallback: official install script (same as Ollama docs recommend) + // This is the standard Linux install path; the script is served over HTTPS + // from Ollama's domain. The script itself verifies GPG signatures on the binary. + execFileSync("bash", ["-c", "curl -fsSL https://ollama.com/install.sh | sh"], { timeout: 300000 }); + } +} diff --git a/packages/tray/src/daemon.ts b/packages/tray/src/daemon.ts new file mode 100644 index 0000000..9a2d08e --- /dev/null +++ b/packages/tray/src/daemon.ts @@ -0,0 +1,171 @@ +import { spawn, ChildProcess, execFileSync } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; +import { + binaryPath, + pidPath, + dataDir, + stoppedSentinelPath, + logsDir, +} from "./paths"; +import { showError } from "./notifications"; + +const MAX_CRASHES = 3; +const CRASH_WINDOW_MS = 5 * 60 * 1000; + +export class DaemonManager { + private process: ChildProcess | null = null; + private crashes: number[] = []; + private onCrashCallback: (() => void) | null = null; + private onExhaustedCallback: (() => void) | null = null; + + get crashCount(): number { + return this.crashes.length; + } + + shouldRestart(): boolean { + const now = Date.now(); + this.crashes = this.crashes.filter((t) => now - t < CRASH_WINDOW_MS); + return this.crashes.length < MAX_CRASHES; + } + + recordCrash(): void { + this.crashes.push(Date.now()); + } + + resetCrashCount(): void { + this.crashes = []; + } + + onCrash(cb: () => void): void { + this.onCrashCallback = cb; + } + + onRestartsExhausted(cb: () => void): void { + this.onExhaustedCallback = cb; + } + + async start(): Promise { + const sentinel = stoppedSentinelPath(); + if (fs.existsSync(sentinel)) { + fs.unlinkSync(sentinel); + } + + const data = dataDir(); + if (!fs.existsSync(data)) { + fs.mkdirSync(data, { recursive: true }); + } + const logs = logsDir(); + if (!fs.existsSync(logs)) { + fs.mkdirSync(logs, { recursive: true }); + } + + const daemonBin = binaryPath("devbrain-daemon"); + + // Redirect stderr to a log file for debugging startup failures + const logFile = path.join(logs, "daemon-stderr.log"); + const stderrStream = fs.openSync(logFile, "a"); + + this.process = spawn(daemonBin, [], { + detached: true, + stdio: ["ignore", "ignore", stderrStream], + }); + + this.process.unref(); + + if (this.process.pid) { + fs.writeFileSync(pidPath(), String(this.process.pid)); + } + + this.process.on("exit", (code) => { + if (fs.existsSync(stoppedSentinelPath())) { + return; + } + + if (code !== 0 && code !== null) { + this.recordCrash(); + this.onCrashCallback?.(); + + if (this.shouldRestart()) { + this.start(); + } else { + showError( + "Daemon crashed", + "DevBrain daemon crashed 3 times in 5 minutes. Use the tray menu to restart." + ); + this.onExhaustedCallback?.(); + } + } + }); + } + + async stop(): Promise { + // Write sentinel BEFORE killing so the exit handler doesn't auto-restart + const sentinel = stoppedSentinelPath(); + fs.writeFileSync(sentinel, "stopped"); + + const pidFile = pidPath(); + + if (fs.existsSync(pidFile)) { + const pidValue = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10); + + if (this.isDaemonProcess(pidValue)) { + try { + process.kill(pidValue); + } catch { + // Process already dead + } + } + + fs.unlinkSync(pidFile); + } + + this.process = null; + } + + async restart(): Promise { + await this.stop(); + this.resetCrashCount(); + await this.start(); + } + + isRunning(): boolean { + const pidFile = pidPath(); + if (!fs.existsSync(pidFile)) return false; + + const pidValue = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10); + + if (!this.isDaemonProcess(pidValue)) { + // PID was recycled to a different process — stale PID file + try { fs.unlinkSync(pidFile); } catch { /* best-effort */ } + return false; + } + + return true; + } + + /** + * Verify that the process at the given PID is actually devbrain-daemon. + * Prevents killing an unrelated process after PID reuse. + */ + private isDaemonProcess(pid: number): boolean { + try { + if (process.platform === "win32") { + const output = execFileSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], { + encoding: "utf-8", + timeout: 5000, + }); + return output.toLowerCase().includes("devbrain-daemon"); + } else { + // Unix: read /proc//comm or use ps + const output = execFileSync("ps", ["-p", String(pid), "-o", "comm="], { + encoding: "utf-8", + timeout: 5000, + }); + return output.trim().includes("devbrain-daemon"); + } + } catch { + return false; // Process doesn't exist + } + } +} diff --git a/packages/tray/src/health.ts b/packages/tray/src/health.ts new file mode 100644 index 0000000..9c4fa16 --- /dev/null +++ b/packages/tray/src/health.ts @@ -0,0 +1,48 @@ +import { EventEmitter } from "events"; + +export type HealthState = "starting" | "healthy" | "unhealthy"; + +const DAEMON_URL = "http://127.0.0.1:37800/api/v1/health"; + +export class HealthMonitor extends EventEmitter { + private _state: HealthState = "starting"; + private timer: ReturnType | null = null; + private pollIntervalMs: number; + + constructor(pollIntervalMs = 5000) { + super(); + this.pollIntervalMs = pollIntervalMs; + } + + get state(): HealthState { + return this._state; + } + + start(): void { + this.check(); // Immediate first check + this.timer = setInterval(() => this.check(), this.pollIntervalMs); + } + + stop(): void { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + } + + private async check(): Promise { + let newState: HealthState; + + try { + const res = await fetch(DAEMON_URL); + newState = res.ok ? "healthy" : "unhealthy"; + } catch { + newState = "unhealthy"; + } + + if (newState !== this._state) { + this._state = newState; + this.emit("stateChange", newState); + } + } +} diff --git a/packages/tray/src/main.ts b/packages/tray/src/main.ts new file mode 100644 index 0000000..7837fe1 --- /dev/null +++ b/packages/tray/src/main.ts @@ -0,0 +1,164 @@ +import { app, Tray, Menu, shell, nativeImage } from "electron"; +import * as fs from "fs"; +import { HealthMonitor, HealthState } from "./health"; +import { DaemonManager } from "./daemon"; +import { Bootstrap } from "./bootstrap"; +import { iconPath, trayLockPath, dataDir, logsDir } from "./paths"; + +let tray: Tray | null = null; +let healthMonitor: HealthMonitor; +let daemonManager: DaemonManager; +let bootstrap: Bootstrap; +let currentState: HealthState = "starting"; + +function createTray(): void { + const icon = nativeImage.createFromPath(iconPath("green")); + tray = new Tray(icon); + tray.setToolTip("DevBrain (Starting...)"); + updateMenu(); +} + +function updateMenu(): void { + if (!tray) return; + + const statusLabel = + currentState === "healthy" + ? "DevBrain (Running)" + : currentState === "unhealthy" + ? "DevBrain (Stopped)" + : "DevBrain (Starting...)"; + + const template = Menu.buildFromTemplate([ + { label: statusLabel, enabled: false }, + { type: "separator" }, + { + label: "Open Dashboard", + click: () => shell.openExternal("http://localhost:37800"), + enabled: currentState === "healthy", + }, + { type: "separator" }, + { + label: "Start Daemon", + click: async () => { + await daemonManager.start(); + healthMonitor.start(); + }, + enabled: currentState !== "healthy", + }, + { + label: "Stop Daemon", + click: async () => { + await daemonManager.stop(); + }, + enabled: currentState === "healthy", + }, + { + label: "Restart Daemon", + click: async () => { + await daemonManager.restart(); + }, + }, + { type: "separator" }, + { + label: "View Logs", + click: () => { + const logs = logsDir(); + if (!fs.existsSync(logs)) { + fs.mkdirSync(logs, { recursive: true }); + } + shell.openPath(logs); + }, + }, + { type: "separator" }, + { + label: "Quit DevBrain", + click: async () => { + await daemonManager.stop(); + removeTrayLock(); + app.quit(); + }, + }, + ]); + + tray.setContextMenu(template); +} + +function updateTrayIcon(state: HealthState): void { + if (!tray) return; + + currentState = state; + + const iconState = + state === "healthy" ? "green" : state === "unhealthy" ? "red" : "yellow"; + + const icon = nativeImage.createFromPath(iconPath(iconState)); + tray.setImage(icon); + + const tooltip = + state === "healthy" + ? "DevBrain (Running)" + : state === "unhealthy" + ? "DevBrain (Stopped)" + : "DevBrain (Starting...)"; + tray.setToolTip(tooltip); + + updateMenu(); +} + +function writeTrayLock(): void { + const lockPath = trayLockPath(); + const dir = dataDir(); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(lockPath, String(process.pid)); +} + +function removeTrayLock(): void { + try { + fs.unlinkSync(trayLockPath()); + } catch { + // Best-effort + } +} + +app.whenReady().then(async () => { + const gotLock = app.requestSingleInstanceLock(); + if (!gotLock) { + app.quit(); + return; + } + + if (process.platform === "darwin") { + app.dock?.hide(); + } + + writeTrayLock(); + + daemonManager = new DaemonManager(); + healthMonitor = new HealthMonitor(); + bootstrap = new Bootstrap(); + + createTray(); + + healthMonitor.on("stateChange", (state: HealthState) => { + updateTrayIcon(state); + }); + + daemonManager.onRestartsExhausted(() => { + updateTrayIcon("unhealthy"); + }); + + await bootstrap.run(() => daemonManager.start()); + + healthMonitor.start(); +}); + +app.on("window-all-closed", () => { + // Prevent app from quitting when no windows — we're a tray app +}); + +app.on("before-quit", () => { + healthMonitor.stop(); + removeTrayLock(); +}); diff --git a/packages/tray/src/notifications.ts b/packages/tray/src/notifications.ts new file mode 100644 index 0000000..9288e3a --- /dev/null +++ b/packages/tray/src/notifications.ts @@ -0,0 +1,21 @@ +import { Notification } from "electron"; + +const APP_NAME = "DevBrain"; + +export function showInfo(title: string, body: string): void { + new Notification({ title: `${APP_NAME}: ${title}`, body }).show(); +} + +export function showError(title: string, body: string): void { + new Notification({ + title: `${APP_NAME}: ${title}`, + body, + urgency: "critical", + }).show(); +} + +export function showProgress(title: string, body: string): Notification { + const n = new Notification({ title: `${APP_NAME}: ${title}`, body }); + n.show(); + return n; +} diff --git a/packages/tray/src/paths.ts b/packages/tray/src/paths.ts new file mode 100644 index 0000000..c354066 --- /dev/null +++ b/packages/tray/src/paths.ts @@ -0,0 +1,65 @@ +import * as path from "path"; +import * as os from "os"; +import { app } from "electron"; + +/** ~/.devbrain on all platforms */ +export function dataDir(): string { + return path.join(os.homedir(), ".devbrain"); +} + +/** ~/.devbrain/settings.toml */ +export function settingsPath(): string { + return path.join(dataDir(), "settings.toml"); +} + +/** ~/.devbrain/daemon.pid */ +export function pidPath(): string { + return path.join(dataDir(), "daemon.pid"); +} + +/** ~/.devbrain/tray.lock */ +export function trayLockPath(): string { + return path.join(dataDir(), "tray.lock"); +} + +/** ~/.devbrain/stopped — sentinel written by CLI to prevent tray auto-restart */ +export function stoppedSentinelPath(): string { + return path.join(dataDir(), "stopped"); +} + +/** ~/.devbrain/logs/ */ +export function logsDir(): string { + return path.join(dataDir(), "logs"); +} + +/** + * Resolve path to an embedded binary (devbrain-daemon or devbrain). + * In dev: looks in resources/bin/ relative to project. + * In packaged app: looks in resources/bin/ inside the asar/resources. + */ +export function binaryPath(name: string): string { + const ext = process.platform === "win32" ? ".exe" : ""; + const binaryName = `${name}${ext}`; + + if (app.isPackaged) { + return path.join(process.resourcesPath, "bin", binaryName); + } + + return path.join(__dirname, "..", "resources", "bin", binaryName); +} + +/** + * Resolve tray icon path by state. + * @param state - "green" | "yellow" | "red" + */ +export function iconPath(state: "green" | "yellow" | "red"): string { + const suffix = state === "green" ? "" : `-${state}`; + const ext = process.platform === "win32" ? ".ico" : ".png"; + const filename = `icon${suffix}${ext}`; + + if (app.isPackaged) { + return path.join(process.resourcesPath, "assets", filename); + } + + return path.join(__dirname, "..", "assets", filename); +} diff --git a/packages/tray/tsconfig.json b/packages/tray/tsconfig.json new file mode 100644 index 0000000..4813f48 --- /dev/null +++ b/packages/tray/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["__tests__", "dist", "node_modules"] +} diff --git a/packages/winget/DevBrain.DevBrain.yaml b/packages/winget/DevBrain.DevBrain.yaml new file mode 100644 index 0000000..19c33a1 --- /dev/null +++ b/packages/winget/DevBrain.DevBrain.yaml @@ -0,0 +1,23 @@ +PackageIdentifier: DevBrain.DevBrain +PackageVersion: 1.0.0 +DefaultLocale: en-US +PackageName: DevBrain +Publisher: DevBrain +PublisherUrl: https://github.com/devbrain/devbrain +License: Apache-2.0 +ShortDescription: Developer's second brain - captures coding sessions, builds knowledge graph +Tags: + - developer-tools + - productivity + - knowledge-graph + - ai +InstallerType: exe +Installers: + - Architecture: x64 + InstallerUrl: https://github.com/devbrain/devbrain/releases/download/v1.0.0/DevBrain-Setup-1.0.0-x64.exe + InstallerSha256: PLACEHOLDER_SHA256 + InstallerSwitches: + Silent: /S + SilentWithProgress: /S +ManifestType: singleton +ManifestVersion: 1.6.0 diff --git a/src/DevBrain.Agents/AgentScheduler.cs b/src/DevBrain.Agents/AgentScheduler.cs index ad52472..1bee285 100644 --- a/src/DevBrain.Agents/AgentScheduler.cs +++ b/src/DevBrain.Agents/AgentScheduler.cs @@ -1,6 +1,7 @@ namespace DevBrain.Agents; using System.Collections.Concurrent; +using Cronos; using DevBrain.Core.Enums; using DevBrain.Core.Interfaces; using DevBrain.Core.Models; @@ -74,7 +75,7 @@ private async Task DispatchAgents(CancellationToken ct) { AgentSchedule.OnEvent onEvent => bufferedEvents.Count > 0 && onEvent.Types.Any(t => bufferedEventTypes.Contains(t)), - AgentSchedule.Cron => IsCronDue(agent.Name), + AgentSchedule.Cron cron => IsCronDue(agent.Name, cron.Expression), AgentSchedule.Idle idle => IsIdle(idle.After), _ => false }; @@ -89,11 +90,13 @@ private async Task DispatchAgents(CancellationToken ct) await Task.WhenAll(tasks); } - private bool IsCronDue(string agentName) + private bool IsCronDue(string agentName, string cronExpression) { if (_lastRunTimes.TryGetValue(agentName, out var lastRun)) { - return (DateTime.UtcNow - lastRun).TotalHours >= 1; + var expression = CronExpression.Parse(cronExpression); + var nextOccurrence = expression.GetNextOccurrence(lastRun, TimeZoneInfo.Utc); + return nextOccurrence.HasValue && nextOccurrence.Value <= DateTime.UtcNow; } return true; } @@ -116,6 +119,34 @@ private async Task RunAgentWithThrottle(IIntelligenceAgent agent, CancellationTo _logger.LogInformation("Agent {AgentName} completed with {Count} outputs", agent.Name, results.Count); + + // Persist dead-end outputs + foreach (var output in results) + { + if (output.Type == AgentOutputType.DeadEndDetected && output.Data is DeadEndOutputData data) + { + try + { + var deadEnd = new DeadEnd + { + Id = Guid.NewGuid().ToString(), + ThreadId = data.ThreadId, + Project = data.Project, + Description = output.Content, + Approach = "Repeated file edits after errors", + Reason = "Heuristic: 3+ edits to same file in thread with errors", + FilesInvolved = data.Files, + DetectedAt = DateTime.UtcNow + }; + + await _ctx.DeadEnds.Add(deadEnd); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to persist dead-end output"); + } + } + } } catch (Exception ex) { diff --git a/src/DevBrain.Agents/BriefingAgent.cs b/src/DevBrain.Agents/BriefingAgent.cs index 2639664..4f95c65 100644 --- a/src/DevBrain.Agents/BriefingAgent.cs +++ b/src/DevBrain.Agents/BriefingAgent.cs @@ -69,14 +69,7 @@ public async Task> Run(AgentContext ctx, Cancellation private static string BuildPrompt(IReadOnlyList observations) { - var lines = new List - { - "Generate a daily development briefing based on the following observations from the last 24 hours.", - "Summarize key decisions, errors encountered, files changed, and overall progress.", - "Format as markdown with sections.", - "", - "Observations:" - }; + var lines = new List(); foreach (var obs in observations) { @@ -85,6 +78,6 @@ private static string BuildPrompt(IReadOnlyList observations) lines.Add($" Files: {string.Join(", ", obs.FilesInvolved)}"); } - return string.Join("\n", lines); + return Prompts.Fill(Prompts.BriefingGeneration, ("OBSERVATIONS", string.Join("\n", lines))); } } diff --git a/src/DevBrain.Agents/CompressionAgent.cs b/src/DevBrain.Agents/CompressionAgent.cs index 9757a15..f82e2f7 100644 --- a/src/DevBrain.Agents/CompressionAgent.cs +++ b/src/DevBrain.Agents/CompressionAgent.cs @@ -1,5 +1,6 @@ namespace DevBrain.Agents; +using DevBrain.Core; using DevBrain.Core.Enums; using DevBrain.Core.Interfaces; using DevBrain.Core.Models; @@ -28,7 +29,7 @@ public async Task> Run(AgentContext ctx, Cancellation AgentName = Name, Priority = Priority.Low, Type = LlmTaskType.Summarization, - Prompt = $"Summarize the following development observation concisely:\n\n{obs.RawContent}", + Prompt = Prompts.Fill(Prompts.CompressionSummarization, ("CONTENT", obs.RawContent)), Preference = LlmPreference.PreferLocal }; diff --git a/src/DevBrain.Agents/DeadEndAgent.cs b/src/DevBrain.Agents/DeadEndAgent.cs index b22893b..589b989 100644 --- a/src/DevBrain.Agents/DeadEndAgent.cs +++ b/src/DevBrain.Agents/DeadEndAgent.cs @@ -51,7 +51,7 @@ public async Task> Run(AgentContext ctx, Cancellation outputs.Add(new AgentOutput( AgentOutputType.DeadEndDetected, description, - new { ThreadId = error.ThreadId, Files = fileChanges })); + new DeadEndOutputData(error.ThreadId, error.Project, fileChanges))); } } diff --git a/src/DevBrain.Agents/DecisionChainAgent.cs b/src/DevBrain.Agents/DecisionChainAgent.cs new file mode 100644 index 0000000..929175a --- /dev/null +++ b/src/DevBrain.Agents/DecisionChainAgent.cs @@ -0,0 +1,148 @@ +namespace DevBrain.Agents; + +using DevBrain.Core; +using DevBrain.Core.Enums; +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; + +public class DecisionChainAgent : IIntelligenceAgent +{ + public string Name => "decision-chain"; + + public AgentSchedule Schedule => new AgentSchedule.OnEvent(EventType.Decision); + + public Priority Priority => Priority.High; + + private static readonly HashSet CausalEdgeTypes = ["caused_by", "supersedes", "resolved_by"]; + + public async Task> Run(AgentContext ctx, CancellationToken ct) + { + var outputs = new List(); + + var recentDecisions = await ctx.Observations.Query(new ObservationFilter + { + EventType = EventType.Decision, + After = DateTime.UtcNow.AddMinutes(-10), + Limit = 20 + }); + + foreach (var decision in recentDecisions) + { + if (ct.IsCancellationRequested) break; + if (decision.FilesInvolved.Count == 0) continue; + + var decisionNode = await FindOrCreateDecisionNode(ctx, decision); + var candidates = await FindCandidateNodes(ctx, decision); + + foreach (var candidate in candidates) + { + if (candidate.Id == decisionNode.Id) continue; + if (candidate.SourceId == decision.Id) continue; + + var existingNeighbors = await ctx.Graph.GetNeighbors( + decisionNode.Id, hops: 1, edgeTypes: CausalEdgeTypes.ToList()); + if (existingNeighbors.Any(n => n.Id == candidate.Id)) continue; + + var edgeType = await ClassifyRelationship(ctx, decision, candidate, ct); + if (edgeType is null) continue; + + await ctx.Graph.AddEdge(decisionNode.Id, candidate.Id, edgeType); + + outputs.Add(new AgentOutput( + AgentOutputType.DecisionChainBuilt, + $"Linked '{decisionNode.Name}' --{edgeType}--> '{candidate.Name}'")); + } + + var resolvedDeadEnds = await CheckDeadEndResolution(ctx, decision, decisionNode, ct); + outputs.AddRange(resolvedDeadEnds); + } + + return outputs; + } + + private static async Task FindOrCreateDecisionNode(AgentContext ctx, Observation decision) + { + var found = await ctx.Graph.GetNodeBySourceId(decision.Id); + if (found is not null) return found; + + return await ctx.Graph.AddNode("Decision", decision.Summary ?? decision.RawContent, sourceId: decision.Id); + } + + private static async Task> FindCandidateNodes(AgentContext ctx, Observation decision) + { + var candidates = new List(); + foreach (var file in decision.FilesInvolved) + { + var related = await ctx.Graph.GetRelatedToFile(file); + foreach (var node in related) + { + if (node.Type is "Decision" or "Bug" && !candidates.Any(c => c.Id == node.Id)) + candidates.Add(node); + } + } + return candidates; + } + + private static async Task ClassifyRelationship( + AgentContext ctx, Observation decision, GraphNode candidate, CancellationToken ct) + { + var prompt = Prompts.Fill(Prompts.DecisionClassification, + ("DECISION_A", candidate.Name), + ("DECISION_B", decision.Summary ?? decision.RawContent), + ("SHARED_FILES", string.Join(", ", decision.FilesInvolved))); + + var task = new LlmTask + { + AgentName = "decision-chain", + Priority = Priority.High, + Type = LlmTaskType.Classification, + Prompt = prompt, + Preference = LlmPreference.PreferLocal + }; + + LlmResult result; + try + { + result = await ctx.Llm.Submit(task, ct); + } + catch (OperationCanceledException) + { + throw; + } + catch + { + return null; + } + + if (!result.Success || string.IsNullOrEmpty(result.Content)) + return null; + + var label = result.Content.Trim().ToLowerInvariant(); + return CausalEdgeTypes.Contains(label) ? label : null; + } + + private static async Task> CheckDeadEndResolution( + AgentContext ctx, Observation decision, GraphNode decisionNode, CancellationToken ct) + { + var outputs = new List(); + + var matchingDeadEnds = await ctx.DeadEnds.FindByFiles(decision.FilesInvolved); + foreach (var deadEnd in matchingDeadEnds) + { + var deNode = await ctx.Graph.GetNodeBySourceId(deadEnd.Id); + if (deNode is null) + deNode = await ctx.Graph.AddNode("Bug", deadEnd.Description, sourceId: deadEnd.Id); + + var neighbors = await ctx.Graph.GetNeighbors(deNode.Id, hops: 1, edgeTypes: ["resolved_by"]); + if (neighbors.Any(n => n.Id == decisionNode.Id)) continue; + + await ctx.Graph.AddEdge(decisionNode.Id, deNode.Id, "resolved_by"); + + outputs.Add(new AgentOutput( + AgentOutputType.DecisionChainBuilt, + $"Decision '{decisionNode.Name}' may resolve dead end: '{deadEnd.Description}'")); + } + + return outputs; + } +} diff --git a/src/DevBrain.Agents/DejaVuAgent.cs b/src/DevBrain.Agents/DejaVuAgent.cs new file mode 100644 index 0000000..8fd38b2 --- /dev/null +++ b/src/DevBrain.Agents/DejaVuAgent.cs @@ -0,0 +1,93 @@ +namespace DevBrain.Agents; + +using DevBrain.Core.Enums; +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; + +public class DejaVuAgent : IIntelligenceAgent +{ + private readonly IAlertStore _alertStore; + private readonly IAlertSink? _alertSink; + + public DejaVuAgent(IAlertStore alertStore, IAlertSink? alertSink = null) + { + _alertStore = alertStore; + _alertSink = alertSink; + } + + public string Name => "deja-vu"; + + public AgentSchedule Schedule => new AgentSchedule.OnEvent(EventType.FileChange, EventType.Error); + + public Priority Priority => Priority.Critical; + + public async Task> Run(AgentContext ctx, CancellationToken ct) + { + var outputs = new List(); + + var recent = await ctx.Observations.Query(new ObservationFilter + { + After = DateTime.UtcNow.AddMinutes(-10), + Limit = 50 + }); + + var threadGroups = recent + .Where(o => !string.IsNullOrEmpty(o.ThreadId)) + .GroupBy(o => o.ThreadId!); + + foreach (var group in threadGroups) + { + if (ct.IsCancellationRequested) break; + + var threadId = group.Key; + var currentFiles = group + .SelectMany(o => o.FilesInvolved) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (currentFiles.Count == 0) continue; + + var matchingDeadEnds = await ctx.DeadEnds.FindByFiles(currentFiles); + var currentFileSet = currentFiles.ToHashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var deadEnd in matchingDeadEnds) + { + if (ct.IsCancellationRequested) break; + if (deadEnd.FilesInvolved.Count == 0) continue; + + var overlap = deadEnd.FilesInvolved.Count(f => currentFileSet.Contains(f)); + var confidence = (double)overlap / deadEnd.FilesInvolved.Count; + + if (confidence < 0.5) continue; + + if (await _alertStore.Exists(threadId, deadEnd.Id)) continue; + + var message = $"You may be heading toward a known dead end: {deadEnd.Description}. " + + $"Approach tried before: {deadEnd.Approach}. " + + $"Why it failed: {deadEnd.Reason}"; + + var alert = new DejaVuAlert + { + Id = Guid.NewGuid().ToString(), + ThreadId = threadId, + MatchedDeadEndId = deadEnd.Id, + Confidence = Math.Round(confidence, 2), + Message = message, + Strategy = MatchStrategy.FileOverlap + }; + + await _alertStore.Add(alert); + + if (_alertSink is not null) + await _alertSink.Send(alert, ct); + + outputs.Add(new AgentOutput( + AgentOutputType.AlertFired, + message, + alert)); + } + } + + return outputs; + } +} diff --git a/src/DevBrain.Agents/DevBrain.Agents.csproj b/src/DevBrain.Agents/DevBrain.Agents.csproj index 2551306..ce2edab 100644 --- a/src/DevBrain.Agents/DevBrain.Agents.csproj +++ b/src/DevBrain.Agents/DevBrain.Agents.csproj @@ -7,6 +7,7 @@ + @@ -16,4 +17,8 @@ enable + + + + diff --git a/src/DevBrain.Agents/GrowthAgent.cs b/src/DevBrain.Agents/GrowthAgent.cs new file mode 100644 index 0000000..9af4849 --- /dev/null +++ b/src/DevBrain.Agents/GrowthAgent.cs @@ -0,0 +1,365 @@ +namespace DevBrain.Agents; + +using DevBrain.Core; +using DevBrain.Core.Enums; +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; + +public class GrowthAgent : IIntelligenceAgent +{ + private readonly IGrowthStore _growthStore; + + public GrowthAgent(IGrowthStore growthStore) + { + _growthStore = growthStore; + } + + public string Name => "growth"; + + public AgentSchedule Schedule => new AgentSchedule.Cron("0 8 * * 1"); + + public Priority Priority => Priority.Low; + + public async Task> Run(AgentContext ctx, CancellationToken ct) + { + var outputs = new List(); + var now = DateTime.UtcNow; + var periodStart = now.AddDays(-7); + var periodEnd = now; + + // Get all observations for the past week + var weekObs = await ctx.Observations.Query(new ObservationFilter + { + After = periodStart, + Before = periodEnd, + Limit = 2000 + }); + + if (weekObs.Count == 0) + return outputs; + + // Compute metrics + var metrics = new List(); + + // 1. Debugging speed: avg minutes from Error to no-more-errors in thread + var debuggingSpeed = ComputeDebuggingSpeed(weekObs); + metrics.Add(CreateMetric("debugging_speed", debuggingSpeed, periodStart, periodEnd)); + + // 2. Dead-end rate + var deadEnds = await ctx.DeadEnds.Query(new DeadEndFilter { After = periodStart }); + var sessionIds = weekObs.Select(o => o.SessionId).Distinct().Count(); + var deadEndRate = sessionIds > 0 ? (double)deadEnds.Count / sessionIds : 0; + metrics.Add(CreateMetric("dead_end_rate", Math.Round(deadEndRate, 2), periodStart, periodEnd)); + + // 3. Exploration breadth: unique files per session + var filesPerSession = weekObs + .GroupBy(o => o.SessionId) + .Select(g => g.SelectMany(o => o.FilesInvolved).Distinct(StringComparer.OrdinalIgnoreCase).Count()) + .DefaultIfEmpty(0) + .Average(); + metrics.Add(CreateMetric("exploration_breadth", Math.Round(filesPerSession, 1), periodStart, periodEnd)); + + // 4. Decision velocity: avg minutes from first FileChange to first Decision per thread + var decisionVelocity = ComputeDecisionVelocity(weekObs); + metrics.Add(CreateMetric("decision_velocity", Math.Round(decisionVelocity, 1), periodStart, periodEnd)); + + // 5. Retry rate: sessions with 3+ edits to same file + var retryRate = ComputeRetryRate(weekObs); + metrics.Add(CreateMetric("retry_rate", Math.Round(retryRate, 2), periodStart, periodEnd)); + + // 6. Tool repertoire: distinct ToolCall observations + var toolCount = weekObs + .Where(o => o.EventType == EventType.ToolCall) + .SelectMany(o => o.Tags) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Count(); + metrics.Add(CreateMetric("tool_repertoire", toolCount, periodStart, periodEnd)); + + // 7. Problem complexity (heuristic only — Llama enhancement deferred) + var complexity = ComputeHeuristicComplexity(weekObs); + metrics.Add(CreateMetric("problem_complexity", Math.Round(complexity, 2), periodStart, periodEnd)); + + // 8. Code quality (heuristic — all errors count equally without Llama) + var errorCount = weekObs.Count(o => o.EventType == EventType.Error); + var quality = weekObs.Count > 0 ? 1.0 - ((double)errorCount / weekObs.Count) : 1.0; + metrics.Add(CreateMetric("code_quality", Math.Round(quality, 3), periodStart, periodEnd)); + + // Detect milestones BEFORE persisting metrics so history queries + // don't include current week's data (avoids consuming a history slot) + var milestones = await DetectMilestones(ctx, weekObs, metrics, periodStart); + + // Now persist metrics + foreach (var metric in metrics) + await _growthStore.AddMetric(metric); + + foreach (var milestone in milestones) + { + await _growthStore.AddMilestone(milestone); + outputs.Add(new AgentOutput(AgentOutputType.MilestoneAchieved, milestone.Description)); + } + + // Generate LLM narrative + string? narrative = null; + try + { + var prompt = BuildNarrativePrompt(metrics, milestones); + var task = new LlmTask + { + AgentName = Name, + Priority = Priority.Low, + Type = LlmTaskType.Synthesis, + Prompt = prompt, + Preference = LlmPreference.PreferLocal + }; + var result = await ctx.Llm.Submit(task, ct); + if (result.Success && !string.IsNullOrEmpty(result.Content)) + narrative = result.Content.Trim(); + } + catch (OperationCanceledException) { throw; } + catch { /* LLM failure is non-fatal for growth reports */ } + + // Persist report + var report = new GrowthReport + { + Id = Guid.NewGuid().ToString(), + PeriodStart = periodStart, + PeriodEnd = periodEnd, + Metrics = metrics, + Milestones = milestones, + Narrative = narrative + }; + await _growthStore.AddReport(report); + + outputs.Add(new AgentOutput(AgentOutputType.GrowthReportGenerated, + $"Growth report generated: {metrics.Count} metrics, {milestones.Count} milestones")); + + return outputs; + } + + internal static double ComputeDebuggingSpeed(IReadOnlyList observations) + { + var threads = observations + .Where(o => !string.IsNullOrEmpty(o.ThreadId)) + .GroupBy(o => o.ThreadId!); + + var durations = new List(); + foreach (var thread in threads) + { + var sorted = thread.OrderBy(o => o.Timestamp).ToList(); + var firstError = sorted.FirstOrDefault(o => o.EventType == EventType.Error); + if (firstError is null) continue; + + var lastError = sorted.LastOrDefault(o => o.EventType == EventType.Error); + if (lastError is null) continue; + + // Resolution = first non-error observation after the last error + var resolution = sorted + .FirstOrDefault(o => o.Timestamp > lastError.Timestamp && o.EventType != EventType.Error); + + if (resolution is not null) + durations.Add((resolution.Timestamp - firstError.Timestamp).TotalMinutes); + } + + return durations.Count > 0 ? durations.Average() : 0; + } + + internal static double ComputeDecisionVelocity(IReadOnlyList observations) + { + var threads = observations + .Where(o => !string.IsNullOrEmpty(o.ThreadId)) + .GroupBy(o => o.ThreadId!); + + var velocities = new List(); + foreach (var thread in threads) + { + var sorted = thread.OrderBy(o => o.Timestamp).ToList(); + var firstChange = sorted.FirstOrDefault(o => o.EventType == EventType.FileChange); + var firstDecision = sorted.FirstOrDefault(o => o.EventType == EventType.Decision); + + if (firstChange is not null && firstDecision is not null && firstDecision.Timestamp > firstChange.Timestamp) + velocities.Add((firstDecision.Timestamp - firstChange.Timestamp).TotalMinutes); + } + + return velocities.Count > 0 ? velocities.Average() : 0; + } + + internal static double ComputeRetryRate(IReadOnlyList observations) + { + var sessions = observations.GroupBy(o => o.SessionId); + var totalSessions = 0; + var retrySessions = 0; + + foreach (var session in sessions) + { + totalSessions++; + var hasRetry = session + .Where(o => o.EventType == EventType.FileChange) + .SelectMany(o => o.FilesInvolved) + .GroupBy(f => f, StringComparer.OrdinalIgnoreCase) + .Any(g => g.Count() >= 3); + if (hasRetry) retrySessions++; + } + + return totalSessions > 0 ? (double)retrySessions / totalSessions : 0; + } + + internal static double ComputeHeuristicComplexity(IReadOnlyList observations) + { + var threads = observations + .Where(o => !string.IsNullOrEmpty(o.ThreadId)) + .GroupBy(o => o.ThreadId!); + + var scores = new List(); + foreach (var thread in threads) + { + var sorted = thread.OrderBy(o => o.Timestamp).ToList(); + var filesInvolved = sorted.SelectMany(o => o.FilesInvolved).Distinct(StringComparer.OrdinalIgnoreCase).Count(); + var decisions = sorted.Count(o => o.EventType == EventType.Decision); + var durationHours = sorted.Count > 1 + ? (sorted[^1].Timestamp - sorted[0].Timestamp).TotalHours + : 0; + var crossProjectRefs = sorted.Select(o => o.Project).Distinct().Count(); + + // Scale: 1 file + 0 decisions + 0 hours + 1 project = ~1.0 + // 5 files + 2 decisions + 1 hour + 1 project = ~3.0 + // 15+ files + 5+ decisions + 3+ hours + 3+ projects = ~5.0 + var raw = (filesInvolved * 0.3) + (decisions * 0.5) + (durationHours * 0.4) + (crossProjectRefs * 0.3); + scores.Add(Math.Clamp(1.0 + raw, 1.0, 5.0)); + } + + return scores.Count > 0 ? scores.Average() : 1.0; + } + + private async Task> DetectMilestones( + AgentContext ctx, IReadOnlyList weekObs, + IReadOnlyList currentMetrics, DateTime periodStart) + { + var milestones = new List(); + + // "First" milestones: new projects + var currentProjects = weekObs.Select(o => o.Project).Distinct().ToList(); + // Use targeted query for historical projects instead of loading 5000 observations + var historicalObs = await ctx.Observations.Query(new ObservationFilter + { + Before = periodStart, Limit = 500 + }); + var historicalProjects = historicalObs.Select(o => o.Project).Distinct().ToHashSet(); + + foreach (var project in currentProjects) + { + if (!historicalProjects.Contains(project)) + { + milestones.Add(new GrowthMilestone + { + Id = Guid.NewGuid().ToString(), + Type = MilestoneType.First, + Description = $"First contribution to {project}", + AchievedAt = DateTime.UtcNow + }); + } + } + + // "Streak" milestones: zero dead ends this week + var deadEndMetric = currentMetrics.FirstOrDefault(m => m.Dimension == "dead_end_rate"); + if (deadEndMetric is not null && deadEndMetric.Value == 0) + { + var deadEndHistory = await _growthStore.GetMetrics("dead_end_rate", 4); + var priorWeeksWithDeadEnds = deadEndHistory.Count(m => m.Value > 0); + + if (priorWeeksWithDeadEnds > 0 || deadEndHistory.Count == 0) + { + milestones.Add(new GrowthMilestone + { + Id = Guid.NewGuid().ToString(), + Type = MilestoneType.Streak, + Description = "Zero dead ends this week", + AchievedAt = DateTime.UtcNow + }); + } + } + + // "Improvement" milestones: any metric > 20% better than 4-week average + foreach (var metric in currentMetrics) + { + var history = await _growthStore.GetMetrics(metric.Dimension, weeks: 4); + if (history.Count == 0) continue; + + var avg = history.Select(m => m.Value).Average(); + if (avg == 0) continue; + + // For rate metrics (dead_end_rate, retry_rate), lower is better + var isLowerBetter = metric.Dimension is "dead_end_rate" or "retry_rate" or "debugging_speed"; + var improvement = isLowerBetter + ? (avg - metric.Value) / avg + : (metric.Value - avg) / avg; + + if (improvement > 0.20) + { + milestones.Add(new GrowthMilestone + { + Id = Guid.NewGuid().ToString(), + Type = MilestoneType.Improvement, + Description = $"{metric.Dimension} improved by {improvement:P0} this week", + AchievedAt = DateTime.UtcNow + }); + } + } + + // Composite: complexity up + quality holding + var complexityMetric = currentMetrics.FirstOrDefault(m => m.Dimension == "problem_complexity"); + var qualityMetric = currentMetrics.FirstOrDefault(m => m.Dimension == "code_quality"); + if (complexityMetric is not null && qualityMetric is not null) + { + var complexityHistory = await _growthStore.GetMetrics("problem_complexity", 4); + var qualityHistory = await _growthStore.GetMetrics("code_quality", 4); + + if (complexityHistory.Count >= 1 && qualityHistory.Count >= 1) + { + var complexityAvg = complexityHistory.Select(m => m.Value).Average(); + var qualityAvg = qualityHistory.Select(m => m.Value).Average(); + + var complexityUp = complexityAvg > 0 && (complexityMetric.Value - complexityAvg) / complexityAvg > 0.10; + var qualityStable = qualityAvg > 0 && Math.Abs(qualityMetric.Value - qualityAvg) / qualityAvg <= 0.05; + + if (complexityUp && qualityStable) + { + milestones.Add(new GrowthMilestone + { + Id = Guid.NewGuid().ToString(), + Type = MilestoneType.Improvement, + Description = "Complexity up with quality holding steady — you're leveling up", + AchievedAt = DateTime.UtcNow + }); + } + } + } + + return milestones; + } + + private static string BuildNarrativePrompt( + IReadOnlyList metrics, IReadOnlyList milestones) + { + var metricsStr = string.Join(", ", metrics.Select(m => $"{m.Dimension}: {m.Value}")); + var milestonesStr = milestones.Count > 0 + ? string.Join("; ", milestones.Select(m => m.Description)) + : "None this week"; + + return Prompts.Fill(Prompts.GrowthNarrative, + ("METRICS", metricsStr), + ("MILESTONES", milestonesStr), + ("TREND", "N/A"), + ("COMPLEXITY", metrics.FirstOrDefault(m => m.Dimension == "problem_complexity")?.Value.ToString() ?? "N/A"), + ("QUALITY", metrics.FirstOrDefault(m => m.Dimension == "code_quality")?.Value.ToString() ?? "N/A"), + ("ERROR_BREAKDOWN", "N/A")); + } + + private static DeveloperMetric CreateMetric(string dimension, double value, DateTime start, DateTime end) => new() + { + Id = Guid.NewGuid().ToString(), + Dimension = dimension, + Value = value, + PeriodStart = start, + PeriodEnd = end + }; +} diff --git a/src/DevBrain.Agents/StorytellerAgent.cs b/src/DevBrain.Agents/StorytellerAgent.cs new file mode 100644 index 0000000..54ed00d --- /dev/null +++ b/src/DevBrain.Agents/StorytellerAgent.cs @@ -0,0 +1,217 @@ +namespace DevBrain.Agents; + +using DevBrain.Core; +using DevBrain.Core.Enums; +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; + +public class StorytellerAgent : IIntelligenceAgent +{ + private readonly ISessionStore _sessionStore; + + public StorytellerAgent(ISessionStore sessionStore) + { + _sessionStore = sessionStore; + } + + public string Name => "storyteller"; + + public AgentSchedule Schedule => new AgentSchedule.Idle(TimeSpan.FromMinutes(30)); + + public Priority Priority => Priority.Normal; + + public async Task> Run(AgentContext ctx, CancellationToken ct) + { + var outputs = new List(); + + // Find sessions with recent observations that don't yet have a story + var recent = await ctx.Observations.Query(new ObservationFilter + { + After = DateTime.UtcNow.AddHours(-4), + Limit = 200 + }); + + var sessionIds = recent + .Select(o => o.SessionId) + .Distinct() + .ToList(); + + foreach (var sessionId in sessionIds) + { + if (ct.IsCancellationRequested) break; + + // Skip if already generated + var existing = await _sessionStore.GetBySessionId(sessionId); + if (existing is not null) continue; + + var observations = await ctx.Observations.GetSessionObservations(sessionId); + if (observations.Count < 3) continue; + + // Compute metrics + var duration = observations[^1].Timestamp - observations[0].Timestamp; + var filesTouched = observations + .SelectMany(o => o.FilesInvolved) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Count(); + + // Phase detection + var phases = DetectPhases(observations); + + // Turning points + var turningPoints = DetectTurningPoints(observations); + + // Dead ends in this session + var sessionFiles = observations + .SelectMany(o => o.FilesInvolved) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + var deadEnds = sessionFiles.Count > 0 + ? await ctx.DeadEnds.FindByFiles(sessionFiles) + : []; + + // Build LLM prompt + var eventLines = observations.Select(o => + $"[{o.EventType}] {o.Timestamp:HH:mm}: {Truncate(o.Summary ?? o.RawContent, 100)}" + ); + + var prompt = Prompts.Fill(Prompts.StorytellerNarrative, + ("DURATION", duration.ToString(@"h\h\ mm\m")), + ("PHASES", string.Join(" -> ", phases)), + ("TURNING_POINTS", string.Join("; ", turningPoints)), + ("EVENTS", string.Join("\n", eventLines))); + + var task = new LlmTask + { + AgentName = Name, + Priority = Priority.Normal, + Type = LlmTaskType.Synthesis, + Prompt = prompt, + Preference = LlmPreference.PreferCloud + }; + + LlmResult result; + try + { + result = await ctx.Llm.Submit(task, ct); + } + catch (OperationCanceledException) { throw; } + catch + { + continue; + } + + if (!result.Success || string.IsNullOrEmpty(result.Content)) + continue; + + // Parse narrative and outcome (last line is outcome) + var lines = result.Content.Trim().Split('\n', StringSplitOptions.RemoveEmptyEntries); + var outcome = lines.Length > 1 ? lines[^1].Trim() : "Session completed."; + var narrative = lines.Length > 1 + ? string.Join("\n", lines[..^1]).Trim() + : result.Content.Trim(); + + var summary = new SessionSummary + { + Id = Guid.NewGuid().ToString(), + SessionId = sessionId, + Narrative = narrative, + Outcome = outcome, + Duration = duration, + ObservationCount = observations.Count, + FilesTouched = filesTouched, + DeadEndsHit = deadEnds.Count, + Phases = phases + }; + + await _sessionStore.Add(summary); + + outputs.Add(new AgentOutput( + AgentOutputType.StoryGenerated, + $"Story generated for session {sessionId}: {outcome}")); + } + + return outputs; + } + + internal static IReadOnlyList DetectPhases(IReadOnlyList observations) + { + if (observations.Count == 0) return []; + + var phases = new List(); + var windowSize = TimeSpan.FromMinutes(10); + var start = observations[0].Timestamp; + var end = observations[^1].Timestamp; + + // Handle sessions shorter than one window (including same-timestamp) + if (end - start < windowSize) + { + phases.Add(ClassifyPhase(observations.ToList())); + return phases; + } + + for (var windowStart = start; windowStart < end; windowStart += windowSize) + { + var windowEnd = windowStart + windowSize; + var windowObs = observations + .Where(o => o.Timestamp >= windowStart && o.Timestamp < windowEnd) + .ToList(); + + if (windowObs.Count == 0) continue; + + var phase = ClassifyPhase(windowObs); + if (phases.Count == 0 || phases[^1] != phase) + phases.Add(phase); + } + + return phases; + } + + private static string ClassifyPhase(List windowObs) + { + var errorCount = windowObs.Count(o => o.EventType == EventType.Error); + var fileChangeCount = windowObs.Count(o => o.EventType == EventType.FileChange); + var conversationCount = windowObs.Count(o => o.EventType == EventType.Conversation); + var hasRefactorTag = windowObs.Any(o => o.Tags.Any(t => + t.Contains("refactor", StringComparison.OrdinalIgnoreCase))); + + if (errorCount > 0 && fileChangeCount > 0) return "Debugging"; + if (hasRefactorTag && fileChangeCount > 0 && errorCount == 0) return "Refactoring"; + if (fileChangeCount > conversationCount) return "Implementation"; + return "Exploration"; + } + + internal static IReadOnlyList DetectTurningPoints(IReadOnlyList observations) + { + var points = new List(); + + for (int i = 0; i < observations.Count; i++) + { + var obs = observations[i]; + + // Decision events are turning points + if (obs.EventType == EventType.Decision) + points.Add($"Decision: {Truncate(obs.Summary ?? obs.RawContent, 60)}"); + + // Error followed by continued non-error activity = likely resolved + if (obs.EventType == EventType.Error) + { + var nextError = observations.Skip(i + 1) + .FirstOrDefault(o => o.EventType == EventType.Error); + var hasSubsequentActivity = observations.Skip(i + 1) + .Any(o => o.EventType != EventType.Error); + + if (hasSubsequentActivity && + (nextError is null || (nextError.Timestamp - obs.Timestamp).TotalMinutes >= 10)) + points.Add($"Error at {obs.Timestamp:HH:mm}, no recurrence after"); + } + } + + return points; + } + + private static string Truncate(string value, int maxLength) + { + if (string.IsNullOrEmpty(value)) return ""; + return value.Length <= maxLength ? value : value[..maxLength] + "..."; + } +} diff --git a/src/DevBrain.Api/Endpoints/AlertEndpoints.cs b/src/DevBrain.Api/Endpoints/AlertEndpoints.cs new file mode 100644 index 0000000..238c4c8 --- /dev/null +++ b/src/DevBrain.Api/Endpoints/AlertEndpoints.cs @@ -0,0 +1,56 @@ +namespace DevBrain.Api.Endpoints; + +using DevBrain.Api.Services; +using DevBrain.Core.Interfaces; + +public static class AlertEndpoints +{ + public static void MapAlertEndpoints(this WebApplication app) + { + var group = app.MapGroup("/api/v1/alerts"); + + group.MapGet("/", async (IAlertStore alertStore) => + { + var alerts = await alertStore.GetActive(); + return Results.Ok(alerts); + }); + + group.MapGet("/all", async (IAlertStore alertStore, int? limit) => + { + var capped = Math.Min(limit ?? 100, 1000); + var alerts = await alertStore.GetAll(capped); + return Results.Ok(alerts); + }); + + group.MapPost("/{id}/dismiss", async (string id, IAlertStore alertStore) => + { + var found = await alertStore.Dismiss(id); + return found + ? Results.Ok(new { dismissed = true }) + : Results.NotFound(new { error = $"Alert '{id}' not found" }); + }); + + group.MapGet("/stream", (AlertChannel channel, CancellationToken ct) => + { + return Results.Stream( + stream => WriteSSE(stream, channel, ct), + contentType: "text/event-stream"); + }); + } + + private static async Task WriteSSE(Stream stream, AlertChannel channel, CancellationToken ct) + { + await using var writer = new StreamWriter(stream) { AutoFlush = true }; + + await foreach (var alert in channel.ReadAllAsync(ct)) + { + if (ct.IsCancellationRequested) break; + + var json = System.Text.Json.JsonSerializer.Serialize(alert); + await writer.WriteLineAsync($"id: {alert.Id}"); + await writer.WriteLineAsync($"event: alert"); + await writer.WriteLineAsync($"data: {json}"); + await writer.WriteLineAsync(); + } + } +} diff --git a/src/DevBrain.Api/Endpoints/BlastRadiusEndpoints.cs b/src/DevBrain.Api/Endpoints/BlastRadiusEndpoints.cs new file mode 100644 index 0000000..90e4332 --- /dev/null +++ b/src/DevBrain.Api/Endpoints/BlastRadiusEndpoints.cs @@ -0,0 +1,18 @@ +namespace DevBrain.Api.Endpoints; + +using DevBrain.Storage; + +public static class BlastRadiusEndpoints +{ + public static void MapBlastRadiusEndpoints(this WebApplication app) + { + var group = app.MapGroup("/api/v1/blast-radius"); + + group.MapGet("/{*path}", async (string path, BlastRadiusCalculator calculator, int? hops) => + { + var cappedHops = Math.Clamp(hops ?? 3, 1, 5); + var result = await calculator.Calculate(path, cappedHops); + return Results.Ok(result); + }); + } +} diff --git a/src/DevBrain.Api/Endpoints/GrowthEndpoints.cs b/src/DevBrain.Api/Endpoints/GrowthEndpoints.cs new file mode 100644 index 0000000..3f99d9d --- /dev/null +++ b/src/DevBrain.Api/Endpoints/GrowthEndpoints.cs @@ -0,0 +1,43 @@ +namespace DevBrain.Api.Endpoints; + +using DevBrain.Core.Interfaces; + +public static class GrowthEndpoints +{ + public static void MapGrowthEndpoints(this WebApplication app) + { + var group = app.MapGroup("/api/v1/growth"); + + group.MapGet("/", async (IGrowthStore growthStore) => + { + var report = await growthStore.GetLatestReport(); + if (report is null) + return Results.Ok(new { status = "no_data", message = "No growth reports yet." }); + return Results.Ok(report); + }); + + group.MapGet("/history", async (IGrowthStore growthStore, string? dimension, int? weeks) => + { + if (!string.IsNullOrEmpty(dimension)) + { + var metrics = await growthStore.GetMetrics(dimension, weeks ?? 12); + return Results.Ok(metrics); + } + + var latest = await growthStore.GetLatestMetrics(); + return Results.Ok(latest); + }); + + group.MapGet("/milestones", async (IGrowthStore growthStore, int? limit) => + { + var milestones = await growthStore.GetMilestones(Math.Min(limit ?? 50, 200)); + return Results.Ok(milestones); + }); + + group.MapDelete("/", async (IGrowthStore growthStore) => + { + await growthStore.Clear(); + return Results.Ok(new { cleared = true }); + }); + } +} diff --git a/src/DevBrain.Api/Endpoints/ReplayEndpoints.cs b/src/DevBrain.Api/Endpoints/ReplayEndpoints.cs new file mode 100644 index 0000000..728fce3 --- /dev/null +++ b/src/DevBrain.Api/Endpoints/ReplayEndpoints.cs @@ -0,0 +1,29 @@ +namespace DevBrain.Api.Endpoints; + +using DevBrain.Storage; + +public static class ReplayEndpoints +{ + public static void MapReplayEndpoints(this WebApplication app) + { + var group = app.MapGroup("/api/v1/replay"); + + // Decision chain for a file + group.MapGet("/file/{*path}", async (string path, DecisionChainBuilder builder, int? hops) => + { + var chain = await builder.BuildForFile(path, hops ?? 3); + return chain is not null + ? Results.Ok(chain) + : Results.NotFound(new { error = $"No decision chain found for '{path}'" }); + }); + + // Decision chain from a specific graph node + group.MapGet("/decision/{nodeId}", async (string nodeId, DecisionChainBuilder builder, int? hops) => + { + var chain = await builder.BuildForDecision(nodeId, hops ?? 4); + return chain is not null + ? Results.Ok(chain) + : Results.NotFound(new { error = $"No decision chain found for node '{nodeId}'" }); + }); + } +} diff --git a/src/DevBrain.Api/Endpoints/SessionEndpoints.cs b/src/DevBrain.Api/Endpoints/SessionEndpoints.cs new file mode 100644 index 0000000..ccc556e --- /dev/null +++ b/src/DevBrain.Api/Endpoints/SessionEndpoints.cs @@ -0,0 +1,60 @@ +namespace DevBrain.Api.Endpoints; + +using DevBrain.Core.Interfaces; + +public static class SessionEndpoints +{ + public static void MapSessionEndpoints(this WebApplication app) + { + var group = app.MapGroup("/api/v1/sessions"); + + // List all sessions with summaries + group.MapGet("/", async (ISessionStore sessionStore, int? limit) => + { + var capped = Math.Min(limit ?? 50, 200); + var sessions = await sessionStore.GetAll(capped); + return Results.Ok(sessions); + }); + + // Get session story by session ID + group.MapGet("/{sessionId}/story", async (string sessionId, ISessionStore sessionStore) => + { + var summary = await sessionStore.GetBySessionId(sessionId); + return summary is not null + ? Results.Ok(summary) + : Results.NotFound(new { error = $"No story for session '{sessionId}'" }); + }); + + // Get session detail with observations + group.MapGet("/{sessionId}", async (string sessionId, + IObservationStore obsStore, ISessionStore sessionStore) => + { + var observations = await obsStore.GetSessionObservations(sessionId); + var story = await sessionStore.GetBySessionId(sessionId); + + return Results.Ok(new + { + sessionId, + observations, + story + }); + }); + + // Validate whether a session can have a story generated. + // Actual generation runs via StorytellerAgent on idle schedule. + // Known limitation: no way to trigger immediate generation (v1). + group.MapPost("/{sessionId}/story", async (string sessionId, + IObservationStore obsStore, ISessionStore sessionStore) => + { + var existing = await sessionStore.GetBySessionId(sessionId); + if (existing is not null) + return Results.Ok(new { status = "already_generated", story = existing }); + + var observations = await obsStore.GetSessionObservations(sessionId); + if (observations.Count < 3) + return Results.BadRequest(new { error = "Session has fewer than 3 observations" }); + + return Results.Ok(new { status = "pending", message = "Session is eligible. Story will be generated when the storyteller agent runs next." }); + }); + } +} diff --git a/src/DevBrain.Api/Program.cs b/src/DevBrain.Api/Program.cs index 3cef839..1437cc6 100644 --- a/src/DevBrain.Api/Program.cs +++ b/src/DevBrain.Api/Program.cs @@ -37,6 +37,11 @@ var observationStore = new SqliteObservationStore(connection); var graphStore = new SqliteGraphStore(connection); +var deadEndStore = new SqliteDeadEndStore(connection); +var alertStore = new SqliteAlertStore(connection); +var sessionStore = new SqliteSessionStore(connection); +var growthStore = new SqliteGrowthStore(connection); +var alertChannel = new DevBrain.Api.Services.AlertChannel(); // ── Vector store (placeholder) ─────────────────────────────────────────────── var vectorStore = new NullVectorStore(); @@ -75,13 +80,17 @@ var pipeline = new PipelineOrchestrator(normalizer, enricher, tagger, privacyFilter, writer); // ── Agents ─────────────────────────────────────────────────────────────────── -var agentContext = new AgentContext(observationStore, graphStore, vectorStore, llmService, settings); +var agentContext = new AgentContext(observationStore, graphStore, vectorStore, llmService, settings, deadEndStore); var agents = new IIntelligenceAgent[] { new LinkerAgent(), new DeadEndAgent(), new BriefingAgent(), - new CompressionAgent() + new CompressionAgent(), + new DecisionChainAgent(), + new DejaVuAgent(alertStore, alertChannel), + new StorytellerAgent(sessionStore), + new GrowthAgent(growthStore) }; // ── ASP.NET Core host ──────────────────────────────────────────────────────── @@ -101,6 +110,15 @@ builder.Services.AddSingleton(observationStore); builder.Services.AddSingleton(graphStore); builder.Services.AddSingleton(vectorStore); +builder.Services.AddSingleton(deadEndStore); +builder.Services.AddSingleton(alertStore); +builder.Services.AddSingleton(alertChannel); +builder.Services.AddSingleton(alertChannel); +builder.Services.AddSingleton(sessionStore); +builder.Services.AddSingleton(growthStore); +var chainBuilder = new DecisionChainBuilder(graphStore, observationStore); +builder.Services.AddSingleton(chainBuilder); +builder.Services.AddSingleton(new BlastRadiusCalculator(graphStore, deadEndStore, chainBuilder)); builder.Services.AddSingleton(llmService); builder.Services.AddSingleton(llmService); // concrete type for ResetDailyCounter builder.Services.AddSingleton(eventBus); @@ -141,6 +159,11 @@ app.MapAdminEndpoints(); app.MapThreadEndpoints(); app.MapDeadEndEndpoints(); +app.MapAlertEndpoints(); +app.MapSessionEndpoints(); +app.MapReplayEndpoints(); +app.MapBlastRadiusEndpoints(); +app.MapGrowthEndpoints(); app.MapContextEndpoints(); app.MapDatabaseEndpoints(); app.MapSetupEndpoints(); diff --git a/src/DevBrain.Api/Services/AlertChannel.cs b/src/DevBrain.Api/Services/AlertChannel.cs new file mode 100644 index 0000000..460065a --- /dev/null +++ b/src/DevBrain.Api/Services/AlertChannel.cs @@ -0,0 +1,29 @@ +namespace DevBrain.Api.Services; + +using System.Threading.Channels; +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; + +/// +/// In-memory broadcast channel for SSE alert delivery. +/// Note: ReadAllAsync is single-consumer — only one SSE client receives each alert. +/// The dashboard also polls GET /alerts as a fallback, so this is acceptable for v1. +/// +public class AlertChannel : IAlertSink +{ + private readonly Channel _channel = Channel.CreateBounded( + new BoundedChannelOptions(100) + { + FullMode = BoundedChannelFullMode.DropOldest + }); + + public async Task Send(DejaVuAlert alert, CancellationToken ct = default) + { + await _channel.Writer.WriteAsync(alert, ct); + } + + public IAsyncEnumerable ReadAllAsync(CancellationToken ct) + { + return _channel.Reader.ReadAllAsync(ct); + } +} diff --git a/src/DevBrain.Cli/Commands/AlertsCommand.cs b/src/DevBrain.Cli/Commands/AlertsCommand.cs new file mode 100644 index 0000000..f9ee82d --- /dev/null +++ b/src/DevBrain.Cli/Commands/AlertsCommand.cs @@ -0,0 +1,107 @@ +using System.CommandLine; +using System.Text.Json; +using DevBrain.Cli.Output; + +namespace DevBrain.Cli.Commands; + +public class AlertsCommand : Command +{ + public AlertsCommand() : base("alerts", "Show and manage deja vu alerts") + { + var dismissCmd = new Command("dismiss", "Dismiss an alert"); + var idArg = new Argument("id") { Description = "Alert ID to dismiss" }; + dismissCmd.Add(idArg); + dismissCmd.SetAction(async (pr) => + { + var id = pr.GetValue(idArg)!; + var client = new DevBrainHttpClient(); + if (!await client.IsHealthy()) + { + ConsoleFormatter.PrintError("Daemon is not running."); + return; + } + var response = await client.Post($"/api/v1/alerts/{Uri.EscapeDataString(id)}/dismiss"); + if (response.IsSuccessStatusCode) + ConsoleFormatter.PrintSuccess($"Alert {id} dismissed."); + else + ConsoleFormatter.PrintError("Failed to dismiss alert. It may not exist."); + }); + + var historyCmd = new Command("history", "Show all alerts including dismissed"); + historyCmd.SetAction(async (pr) => + { + var client = new DevBrainHttpClient(); + if (!await client.IsHealthy()) + { + ConsoleFormatter.PrintError("Daemon is not running."); + return; + } + await PrintAlerts(client, "/api/v1/alerts/all"); + }); + + Add(dismissCmd); + Add(historyCmd); + SetAction(Execute); + } + + private async Task Execute(ParseResult pr) + { + var client = new DevBrainHttpClient(); + if (!await client.IsHealthy()) + { + ConsoleFormatter.PrintError("Daemon is not running."); + return; + } + await PrintAlerts(client, "/api/v1/alerts"); + } + + private static async Task PrintAlerts(DevBrainHttpClient client, string url) + { + try + { + var json = await client.GetJson(url); + + if (json.ValueKind != JsonValueKind.Array || json.GetArrayLength() == 0) + { + ConsoleFormatter.PrintSuccess("No active alerts."); + return; + } + + Console.WriteLine($"Found {json.GetArrayLength()} alert(s):\n"); + + foreach (var item in json.EnumerateArray()) + { + var message = item.GetPropertyOrDefault("message", "(no message)"); + var confidence = item.TryGetProperty("confidence", out var c) ? c.GetDouble() : 0; + var strategy = item.GetPropertyOrDefault("strategy", "unknown"); + var dismissed = item.TryGetProperty("dismissed", out var d) && d.GetBoolean(); + + if (dismissed) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.Write(" - [DISMISSED] "); + } + else + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write(" ! "); + } + Console.ResetColor(); + + Console.WriteLine(message); + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($" Confidence: {confidence:P0} Strategy: {strategy}"); + + if (item.TryGetProperty("id", out var idProp)) + Console.WriteLine($" ID: {idProp.GetString()}"); + + Console.ResetColor(); + Console.WriteLine(); + } + } + catch (Exception ex) + { + ConsoleFormatter.PrintError($"Failed to fetch alerts: {ex.Message}"); + } + } +} diff --git a/src/DevBrain.Cli/Commands/BlastCommand.cs b/src/DevBrain.Cli/Commands/BlastCommand.cs new file mode 100644 index 0000000..f3e335a --- /dev/null +++ b/src/DevBrain.Cli/Commands/BlastCommand.cs @@ -0,0 +1,105 @@ +using System.CommandLine; +using System.Text.Json; +using DevBrain.Cli.Output; + +namespace DevBrain.Cli.Commands; + +public class BlastCommand : Command +{ + private readonly Argument _pathArg = new("path") + { + Description = "File path to analyze blast radius for" + }; + + private readonly Option _hopsOption = new("--hops") + { + Description = "Traversal depth (1-5, default 3)" + }; + + public BlastCommand() : base("blast", "Show blast radius for a file") + { + Add(_pathArg); + Add(_hopsOption); + SetAction(Execute); + } + + private async Task Execute(ParseResult pr) + { + var path = pr.GetValue(_pathArg)!; + var hops = pr.GetValue(_hopsOption); + var client = new DevBrainHttpClient(); + + if (!await client.IsHealthy()) + { + ConsoleFormatter.PrintError("Daemon is not running."); + return; + } + + try + { + var url = $"/api/v1/blast-radius/{Uri.EscapeDataString(path)}"; + if (hops.HasValue) url += $"?hops={hops.Value}"; + var json = await client.GetJson(url); + + var sourceFile = json.GetPropertyOrDefault("sourceFile", path); + + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine($" Blast Radius: {sourceFile}"); + Console.ResetColor(); + Console.WriteLine(); + + // Dead ends at risk + if (json.TryGetProperty("deadEndsAtRisk", out var deadEnds) && + deadEnds.ValueKind == JsonValueKind.Array && deadEnds.GetArrayLength() > 0) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($" {deadEnds.GetArrayLength()} dead end(s) at risk of re-triggering"); + Console.ResetColor(); + Console.WriteLine(); + } + + // Affected files + if (json.TryGetProperty("affectedFiles", out var files) && + files.ValueKind == JsonValueKind.Array) + { + if (files.GetArrayLength() == 0) + { + ConsoleFormatter.PrintSuccess("No affected files found. Safe to change!"); + } + else + { + Console.WriteLine($" {files.GetArrayLength()} affected file(s):\n"); + + foreach (var file in files.EnumerateArray()) + { + var filePath = file.GetPropertyOrDefault("filePath", "?"); + var risk = file.TryGetProperty("riskScore", out var r) ? r.GetDouble() : 0; + var chainLen = file.TryGetProperty("chainLength", out var cl) ? cl.GetInt32() : 0; + var reason = file.GetPropertyOrDefault("reason", ""); + + var color = risk > 0.7 ? ConsoleColor.Red + : risk > 0.3 ? ConsoleColor.Yellow + : ConsoleColor.Green; + + Console.ForegroundColor = color; + Console.Write($" {risk:F2} "); + Console.ResetColor(); + Console.Write(filePath); + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($" (chain: {chainLen})"); + if (!string.IsNullOrEmpty(reason)) + Console.WriteLine($" {reason}"); + Console.ResetColor(); + } + } + } + + Console.WriteLine(); + } + catch (Exception ex) + { + ConsoleFormatter.PrintError($"Failed to compute blast radius: {ex.Message}"); + } + } +} diff --git a/src/DevBrain.Cli/Commands/GrowthCommand.cs b/src/DevBrain.Cli/Commands/GrowthCommand.cs new file mode 100644 index 0000000..17204ee --- /dev/null +++ b/src/DevBrain.Cli/Commands/GrowthCommand.cs @@ -0,0 +1,113 @@ +using System.CommandLine; +using System.Text.Json; +using DevBrain.Cli.Output; + +namespace DevBrain.Cli.Commands; + +public class GrowthCommand : Command +{ + public GrowthCommand() : base("growth", "Show developer growth report") + { + var milestonesCmd = new Command("milestones", "Show milestones"); + milestonesCmd.SetAction(async (pr) => + { + var client = new DevBrainHttpClient(); + if (!await client.IsHealthy()) { ConsoleFormatter.PrintError("Daemon is not running."); return; } + + var json = await client.GetJson("/api/v1/growth/milestones"); + if (json.ValueKind != JsonValueKind.Array || json.GetArrayLength() == 0) + { + ConsoleFormatter.PrintWarning("No milestones yet."); + return; + } + + Console.WriteLine($"\n Milestones ({json.GetArrayLength()}):\n"); + foreach (var item in json.EnumerateArray()) + { + var type = item.GetPropertyOrDefault("type", "?"); + var desc = item.GetPropertyOrDefault("description", ""); + var color = type switch + { + "First" => ConsoleColor.Cyan, + "Streak" => ConsoleColor.Yellow, + "Improvement" => ConsoleColor.Green, + _ => ConsoleColor.Gray + }; + Console.ForegroundColor = color; + Console.Write($" [{type}] "); + Console.ResetColor(); + Console.WriteLine(desc); + } + Console.WriteLine(); + }); + + var resetCmd = new Command("reset", "Wipe all growth data"); + resetCmd.SetAction(async (pr) => + { + var client = new DevBrainHttpClient(); + if (!await client.IsHealthy()) { ConsoleFormatter.PrintError("Daemon is not running."); return; } + var response = await client.Delete("/api/v1/growth"); + if (response.IsSuccessStatusCode) + ConsoleFormatter.PrintSuccess("All growth data has been cleared."); + else + ConsoleFormatter.PrintError("Failed to clear growth data."); + }); + + Add(milestonesCmd); + Add(resetCmd); + SetAction(Execute); + } + + private async Task Execute(ParseResult pr) + { + var client = new DevBrainHttpClient(); + if (!await client.IsHealthy()) + { + ConsoleFormatter.PrintError("Daemon is not running."); + return; + } + + try + { + var json = await client.GetJson("/api/v1/growth"); + + if (json.TryGetProperty("status", out var status) && status.GetString() == "no_data") + { + ConsoleFormatter.PrintWarning("No growth reports yet. The growth agent runs weekly (Monday 8 AM)."); + return; + } + + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(" Growth Report"); + Console.ResetColor(); + + if (json.TryGetProperty("narrative", out var narr) && narr.ValueKind == JsonValueKind.String) + { + Console.WriteLine(); + Console.WriteLine($" {narr.GetString()}"); + } + + Console.WriteLine(); + + if (json.TryGetProperty("metrics", out var metrics) && metrics.ValueKind == JsonValueKind.Array) + { + foreach (var m in metrics.EnumerateArray()) + { + var dim = m.GetPropertyOrDefault("dimension", "?"); + var val = m.TryGetProperty("value", out var v) ? v.GetDouble() : 0; + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.Write($" {dim,-25}"); + Console.ResetColor(); + Console.WriteLine($"{val:F2}"); + } + } + + Console.WriteLine(); + } + catch (Exception ex) + { + ConsoleFormatter.PrintError($"Failed to fetch growth report: {ex.Message}"); + } + } +} diff --git a/src/DevBrain.Cli/Commands/ReplayCommand.cs b/src/DevBrain.Cli/Commands/ReplayCommand.cs new file mode 100644 index 0000000..3494645 --- /dev/null +++ b/src/DevBrain.Cli/Commands/ReplayCommand.cs @@ -0,0 +1,114 @@ +using System.CommandLine; +using System.Text.Json; +using DevBrain.Cli.Output; + +namespace DevBrain.Cli.Commands; + +public class ReplayCommand : Command +{ + private readonly Argument _pathArg = new("path") + { + Description = "File path to get the decision chain for", + Arity = ArgumentArity.ZeroOrOne + }; + + private readonly Option _decisionOption = new("--decision") + { + Description = "Graph node ID of a specific decision to trace" + }; + + public ReplayCommand() : base("replay", "Show the decision chain for a file or decision") + { + Add(_pathArg); + Add(_decisionOption); + SetAction(Execute); + } + + private async Task Execute(ParseResult pr) + { + var path = pr.GetValue(_pathArg); + var decisionId = pr.GetValue(_decisionOption); + var client = new DevBrainHttpClient(); + + if (!await client.IsHealthy()) + { + ConsoleFormatter.PrintError("Daemon is not running."); + return; + } + + try + { + JsonElement json; + + if (!string.IsNullOrEmpty(decisionId)) + { + json = await client.GetJson($"/api/v1/replay/decision/{Uri.EscapeDataString(decisionId)}"); + } + else if (!string.IsNullOrEmpty(path)) + { + json = await client.GetJson($"/api/v1/replay/file/{Uri.EscapeDataString(path)}"); + } + else + { + ConsoleFormatter.PrintError("Provide a file path or --decision ."); + return; + } + + var narrative = json.GetPropertyOrDefault("narrative", ""); + + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(" Decision Chain"); + Console.ResetColor(); + Console.WriteLine(); + Console.WriteLine($" {narrative}"); + Console.WriteLine(); + + if (json.TryGetProperty("steps", out var steps) && steps.ValueKind == JsonValueKind.Array) + { + foreach (var step in steps.EnumerateArray()) + { + var stepType = step.GetPropertyOrDefault("stepType", "Decision"); + var summary = step.GetPropertyOrDefault("summary", "(no summary)"); + var timestamp = step.GetPropertyOrDefault("timestamp", ""); + + var color = stepType switch + { + "Decision" => ConsoleColor.Green, + "DeadEnd" => ConsoleColor.Red, + "Error" => ConsoleColor.Yellow, + "Resolution" => ConsoleColor.Blue, + _ => ConsoleColor.Gray + }; + + Console.ForegroundColor = color; + Console.Write($" [{stepType}] "); + Console.ResetColor(); + + if (!string.IsNullOrEmpty(timestamp) && DateTime.TryParse(timestamp, out var dt)) + Console.Write($"{dt:yyyy-MM-dd HH:mm} "); + + Console.WriteLine(summary); + + if (step.TryGetProperty("filesInvolved", out var files) && files.ValueKind == JsonValueKind.Array) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + foreach (var f in files.EnumerateArray()) + Console.WriteLine($" {f.GetString()}"); + Console.ResetColor(); + } + } + } + + Console.WriteLine(); + } + catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + ConsoleFormatter.PrintWarning("No decision chain found for this file."); + } + catch (Exception ex) + { + ConsoleFormatter.PrintError($"Failed to fetch decision chain: {ex.Message}"); + } + } +} diff --git a/src/DevBrain.Cli/Commands/StartCommand.cs b/src/DevBrain.Cli/Commands/StartCommand.cs index 033179a..9eab3b7 100644 --- a/src/DevBrain.Cli/Commands/StartCommand.cs +++ b/src/DevBrain.Cli/Commands/StartCommand.cs @@ -1,6 +1,7 @@ using System.CommandLine; using System.Diagnostics; using DevBrain.Cli.Output; +using DevBrain.Core; namespace DevBrain.Cli.Commands; @@ -21,6 +22,29 @@ private static async Task Execute(ParseResult pr) return; } + // Check if tray app is managing the daemon + var dataPath = SettingsLoader.ResolveDataPath("~/.devbrain"); + var trayLockPath = Path.Combine(dataPath, "tray.lock"); + + if (File.Exists(trayLockPath)) + { + var lockPidText = (await File.ReadAllTextAsync(trayLockPath)).Trim(); + if (int.TryParse(lockPidText, out var trayPid)) + { + try + { + Process.GetProcessById(trayPid); + ConsoleFormatter.PrintWarning( + "Daemon is managed by the tray app. Use the tray menu to start it."); + return; + } + catch (ArgumentException) + { + // Tray process is dead — stale lock, continue normally + } + } + } + var cliDir = AppContext.BaseDirectory; var daemonName = OperatingSystem.IsWindows() ? "devbrain-daemon.exe" : "devbrain-daemon"; var daemonPath = Path.Combine(cliDir, daemonName); diff --git a/src/DevBrain.Cli/Commands/StopCommand.cs b/src/DevBrain.Cli/Commands/StopCommand.cs index 357f416..3fcb3a3 100644 --- a/src/DevBrain.Cli/Commands/StopCommand.cs +++ b/src/DevBrain.Cli/Commands/StopCommand.cs @@ -14,7 +14,8 @@ public StopCommand() : base("stop", "Stop the DevBrain daemon") private static async Task Execute(ParseResult pr) { - var pidPath = Path.Combine(SettingsLoader.ResolveDataPath("~/.devbrain"), "daemon.pid"); + var dataPath = SettingsLoader.ResolveDataPath("~/.devbrain"); + var pidPath = Path.Combine(dataPath, "daemon.pid"); if (!File.Exists(pidPath)) { @@ -22,6 +23,27 @@ private static async Task Execute(ParseResult pr) return; } + // If tray app is running, write stopped sentinel so it doesn't auto-restart + var trayLockPath = Path.Combine(dataPath, "tray.lock"); + if (File.Exists(trayLockPath)) + { + var trayPidText = (await File.ReadAllTextAsync(trayLockPath)).Trim(); + if (int.TryParse(trayPidText, out var trayPid)) + { + try + { + Process.GetProcessById(trayPid); + // Tray is alive — write sentinel to prevent auto-restart + var sentinelPath = Path.Combine(dataPath, "stopped"); + await File.WriteAllTextAsync(sentinelPath, "stopped by cli"); + } + catch (ArgumentException) + { + // Tray is dead — no sentinel needed + } + } + } + var pidText = (await File.ReadAllTextAsync(pidPath)).Trim(); if (!int.TryParse(pidText, out var pid)) @@ -33,6 +55,16 @@ private static async Task Execute(ParseResult pr) try { var process = Process.GetProcessById(pid); + + // Verify this is actually the daemon process (PID may have been recycled) + var processName = process.ProcessName.ToLowerInvariant(); + if (!processName.Contains("devbrain-daemon") && !processName.Contains("devbrain.api")) + { + ConsoleFormatter.PrintWarning( + $"PID {pid} belongs to '{process.ProcessName}', not devbrain-daemon. Stale PID file removed."); + return; + } + process.Kill(entireProcessTree: true); process.WaitForExit(5000); ConsoleFormatter.PrintSuccess($"Daemon (PID {pid}) stopped."); diff --git a/src/DevBrain.Cli/Commands/StoryCommand.cs b/src/DevBrain.Cli/Commands/StoryCommand.cs new file mode 100644 index 0000000..793ea77 --- /dev/null +++ b/src/DevBrain.Cli/Commands/StoryCommand.cs @@ -0,0 +1,89 @@ +using System.CommandLine; +using System.Text.Json; +using DevBrain.Cli.Output; + +namespace DevBrain.Cli.Commands; + +public class StoryCommand : Command +{ + private readonly Option _sessionOption = new("--session") + { + Description = "Session ID (defaults to latest)" + }; + + public StoryCommand() : base("story", "Show session story narrative") + { + Add(_sessionOption); + SetAction(Execute); + } + + private async Task Execute(ParseResult pr) + { + var sessionId = pr.GetValue(_sessionOption); + var client = new DevBrainHttpClient(); + + if (!await client.IsHealthy()) + { + ConsoleFormatter.PrintError("Daemon is not running."); + return; + } + + try + { + JsonElement json; + + if (!string.IsNullOrEmpty(sessionId)) + { + json = await client.GetJson($"/api/v1/sessions/{Uri.EscapeDataString(sessionId)}/story"); + } + else + { + // Get latest + var sessionsJson = await client.GetJson("/api/v1/sessions?limit=1"); + if (sessionsJson.ValueKind != JsonValueKind.Array || sessionsJson.GetArrayLength() == 0) + { + ConsoleFormatter.PrintWarning("No session stories available yet."); + return; + } + json = sessionsJson[0]; + } + + var narrative = json.GetPropertyOrDefault("narrative", ""); + var outcome = json.GetPropertyOrDefault("outcome", ""); + var duration = json.TryGetProperty("duration", out var d) ? d.ToString() : "?"; + var obsCount = json.TryGetProperty("observationCount", out var oc) ? oc.GetInt32() : 0; + var filesCount = json.TryGetProperty("filesTouched", out var fc) ? fc.GetInt32() : 0; + var deadEnds = json.TryGetProperty("deadEndsHit", out var de) ? de.GetInt32() : 0; + + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine($" Session Story ({duration})"); + Console.ResetColor(); + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($" {obsCount} observations | {filesCount} files | {deadEnds} dead ends"); + Console.ResetColor(); + Console.WriteLine(); + + Console.WriteLine(narrative); + Console.WriteLine(); + + Console.ForegroundColor = ConsoleColor.Green; + Console.Write(" Outcome: "); + Console.ResetColor(); + Console.WriteLine(outcome); + Console.WriteLine(); + } + catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + ConsoleFormatter.PrintWarning("No story available for this session."); + } + catch (HttpRequestException ex) + { + ConsoleFormatter.PrintError($"API error: {ex.StatusCode} - {ex.Message}"); + } + catch (Exception ex) + { + ConsoleFormatter.PrintError($"Failed to fetch story: {ex.Message}"); + } + } +} diff --git a/src/DevBrain.Cli/Program.cs b/src/DevBrain.Cli/Program.cs index a86e4c9..1a8dbd7 100644 --- a/src/DevBrain.Cli/Program.cs +++ b/src/DevBrain.Cli/Program.cs @@ -12,6 +12,11 @@ root.Add(new DashboardCommand()); root.Add(new ThreadCommand()); root.Add(new DeadEndsCommand()); +root.Add(new AlertsCommand()); +root.Add(new StoryCommand()); +root.Add(new ReplayCommand()); +root.Add(new BlastCommand()); +root.Add(new GrowthCommand()); root.Add(new RelatedCommand()); root.Add(new AgentsCommand()); root.Add(new ConfigCommand()); diff --git a/src/DevBrain.Core/Enums/DecisionStepType.cs b/src/DevBrain.Core/Enums/DecisionStepType.cs new file mode 100644 index 0000000..a7befb1 --- /dev/null +++ b/src/DevBrain.Core/Enums/DecisionStepType.cs @@ -0,0 +1,9 @@ +namespace DevBrain.Core.Enums; + +public enum DecisionStepType +{ + Decision, + DeadEnd, + Error, + Resolution +} diff --git a/src/DevBrain.Core/Enums/MatchStrategy.cs b/src/DevBrain.Core/Enums/MatchStrategy.cs new file mode 100644 index 0000000..1a09862 --- /dev/null +++ b/src/DevBrain.Core/Enums/MatchStrategy.cs @@ -0,0 +1,7 @@ +namespace DevBrain.Core.Enums; + +public enum MatchStrategy +{ + FileOverlap, + Semantic +} diff --git a/src/DevBrain.Core/Enums/MilestoneType.cs b/src/DevBrain.Core/Enums/MilestoneType.cs new file mode 100644 index 0000000..f7d40a8 --- /dev/null +++ b/src/DevBrain.Core/Enums/MilestoneType.cs @@ -0,0 +1,8 @@ +namespace DevBrain.Core.Enums; + +public enum MilestoneType +{ + First, + Streak, + Improvement +} diff --git a/src/DevBrain.Core/Interfaces/IAlertSink.cs b/src/DevBrain.Core/Interfaces/IAlertSink.cs new file mode 100644 index 0000000..252ea0c --- /dev/null +++ b/src/DevBrain.Core/Interfaces/IAlertSink.cs @@ -0,0 +1,8 @@ +namespace DevBrain.Core.Interfaces; + +using DevBrain.Core.Models; + +public interface IAlertSink +{ + Task Send(DejaVuAlert alert, CancellationToken ct = default); +} diff --git a/src/DevBrain.Core/Interfaces/IAlertStore.cs b/src/DevBrain.Core/Interfaces/IAlertStore.cs new file mode 100644 index 0000000..ddf175d --- /dev/null +++ b/src/DevBrain.Core/Interfaces/IAlertStore.cs @@ -0,0 +1,12 @@ +namespace DevBrain.Core.Interfaces; + +using DevBrain.Core.Models; + +public interface IAlertStore +{ + Task Add(DejaVuAlert alert); + Task> GetActive(); + Task> GetAll(int limit = 100); + Task Dismiss(string id); + Task Exists(string threadId, string deadEndId); +} diff --git a/src/DevBrain.Core/Interfaces/IDeadEndStore.cs b/src/DevBrain.Core/Interfaces/IDeadEndStore.cs new file mode 100644 index 0000000..ece4c4a --- /dev/null +++ b/src/DevBrain.Core/Interfaces/IDeadEndStore.cs @@ -0,0 +1,21 @@ +namespace DevBrain.Core.Interfaces; + +using DevBrain.Core.Models; + +public record DeadEndFilter +{ + public string? Project { get; init; } + public string? ThreadId { get; init; } + public DateTime? After { get; init; } + public DateTime? Before { get; init; } + public int Limit { get; init; } = 50; + public int Offset { get; init; } = 0; +} + +public interface IDeadEndStore +{ + Task Add(DeadEnd deadEnd); + Task> Query(DeadEndFilter filter); + Task> FindByFiles(IReadOnlyList filePaths); + Task> FindSimilar(string description, int limit = 5); +} diff --git a/src/DevBrain.Core/Interfaces/IGraphStore.cs b/src/DevBrain.Core/Interfaces/IGraphStore.cs index df813ca..f02e847 100644 --- a/src/DevBrain.Core/Interfaces/IGraphStore.cs +++ b/src/DevBrain.Core/Interfaces/IGraphStore.cs @@ -7,12 +7,14 @@ public interface IGraphStore Task AddNode(string type, string name, object? data = null, string? sourceId = null); Task GetNode(string id); Task> GetNodesByType(string type); + Task GetNodeBySourceId(string sourceId); Task RemoveNode(string id); Task AddEdge(string sourceId, string targetId, string type, object? data = null); Task RemoveEdge(string id); Task> GetNeighbors(string nodeId, int hops = 1, string? edgeType = null); + Task> GetNeighbors(string nodeId, int hops, IReadOnlyList edgeTypes); Task> FindPaths(string fromId, string toId, int maxDepth = 4); Task> GetRelatedToFile(string filePath); diff --git a/src/DevBrain.Core/Interfaces/IGrowthStore.cs b/src/DevBrain.Core/Interfaces/IGrowthStore.cs new file mode 100644 index 0000000..0e7c2fe --- /dev/null +++ b/src/DevBrain.Core/Interfaces/IGrowthStore.cs @@ -0,0 +1,19 @@ +namespace DevBrain.Core.Interfaces; + +using DevBrain.Core.Models; + +public interface IGrowthStore +{ + Task AddMetric(DeveloperMetric metric); + Task> GetMetrics(string dimension, int weeks = 12); + Task> GetLatestMetrics(); + + Task AddMilestone(GrowthMilestone milestone); + Task> GetMilestones(int limit = 50); + + Task AddReport(GrowthReport report); + Task GetLatestReport(); + Task> GetReports(int limit = 12); + + Task Clear(); +} diff --git a/src/DevBrain.Core/Interfaces/IIntelligenceAgent.cs b/src/DevBrain.Core/Interfaces/IIntelligenceAgent.cs index 2fa8e93..2539153 100644 --- a/src/DevBrain.Core/Interfaces/IIntelligenceAgent.cs +++ b/src/DevBrain.Core/Interfaces/IIntelligenceAgent.cs @@ -8,7 +8,8 @@ public record AgentContext( IGraphStore Graph, IVectorStore Vectors, ILlmService Llm, - Settings Settings + Settings Settings, + IDeadEndStore DeadEnds ); public interface IIntelligenceAgent diff --git a/src/DevBrain.Core/Interfaces/IObservationStore.cs b/src/DevBrain.Core/Interfaces/IObservationStore.cs index 9bb9544..dd7f5ff 100644 --- a/src/DevBrain.Core/Interfaces/IObservationStore.cs +++ b/src/DevBrain.Core/Interfaces/IObservationStore.cs @@ -27,4 +27,5 @@ public interface IObservationStore Task GetDatabaseSizeBytes(); Task DeleteByProject(string project); Task DeleteBefore(DateTime before); + Task> GetSessionObservations(string sessionId, int limit = 500); } diff --git a/src/DevBrain.Core/Interfaces/ISessionStore.cs b/src/DevBrain.Core/Interfaces/ISessionStore.cs new file mode 100644 index 0000000..7876aaa --- /dev/null +++ b/src/DevBrain.Core/Interfaces/ISessionStore.cs @@ -0,0 +1,12 @@ +namespace DevBrain.Core.Interfaces; + +using DevBrain.Core.Models; + +public interface ISessionStore +{ + Task Add(SessionSummary summary); + Task GetBySessionId(string sessionId); + Task> GetAll(int limit = 50); + Task GetLatest(); + Task> GetByDateRange(DateTime after, DateTime before); +} diff --git a/src/DevBrain.Core/Models/AgentOutput.cs b/src/DevBrain.Core/Models/AgentOutput.cs index 6c9568a..64cfd8c 100644 --- a/src/DevBrain.Core/Models/AgentOutput.cs +++ b/src/DevBrain.Core/Models/AgentOutput.cs @@ -6,7 +6,14 @@ public enum AgentOutputType BriefingGenerated, EdgeCreated, ThreadCompressed, - PatternDetected + PatternDetected, + AlertFired, + StoryGenerated, + DecisionChainBuilt, + GrowthReportGenerated, + MilestoneAchieved } public record AgentOutput(AgentOutputType Type, string Content, object? Data = null); + +public record DeadEndOutputData(string? ThreadId, string Project, IReadOnlyList Files); diff --git a/src/DevBrain.Core/Models/BlastRadius.cs b/src/DevBrain.Core/Models/BlastRadius.cs new file mode 100644 index 0000000..81a1358 --- /dev/null +++ b/src/DevBrain.Core/Models/BlastRadius.cs @@ -0,0 +1,19 @@ +namespace DevBrain.Core.Models; + +public record BlastRadius +{ + public required string SourceFile { get; init; } + public IReadOnlyList AffectedFiles { get; init; } = []; + public IReadOnlyList DeadEndsAtRisk { get; init; } = []; + public string? Summary { get; init; } + public DateTime GeneratedAt { get; init; } = DateTime.UtcNow; +} + +public record BlastRadiusEntry +{ + public required string FilePath { get; init; } + public required double RiskScore { get; init; } + public required int ChainLength { get; init; } + public required string Reason { get; init; } + public string? LinkedDecisionId { get; init; } +} diff --git a/src/DevBrain.Core/Models/DecisionChain.cs b/src/DevBrain.Core/Models/DecisionChain.cs new file mode 100644 index 0000000..4e6e79c --- /dev/null +++ b/src/DevBrain.Core/Models/DecisionChain.cs @@ -0,0 +1,21 @@ +namespace DevBrain.Core.Models; + +using DevBrain.Core.Enums; + +public record DecisionChain +{ + public required string Id { get; init; } + public required string RootNodeId { get; init; } + public required string Narrative { get; init; } + public IReadOnlyList Steps { get; init; } = []; + public DateTime GeneratedAt { get; init; } = DateTime.UtcNow; +} + +public record DecisionStep +{ + public required string ObservationId { get; init; } + public required string Summary { get; init; } + public required DateTime Timestamp { get; init; } + public required DecisionStepType StepType { get; init; } + public IReadOnlyList FilesInvolved { get; init; } = []; +} diff --git a/src/DevBrain.Core/Models/DejaVuAlert.cs b/src/DevBrain.Core/Models/DejaVuAlert.cs new file mode 100644 index 0000000..9a3c4d4 --- /dev/null +++ b/src/DevBrain.Core/Models/DejaVuAlert.cs @@ -0,0 +1,15 @@ +namespace DevBrain.Core.Models; + +using DevBrain.Core.Enums; + +public record DejaVuAlert +{ + public required string Id { get; init; } + public required string ThreadId { get; init; } + public required string MatchedDeadEndId { get; init; } + public required double Confidence { get; init; } + public required string Message { get; init; } + public required MatchStrategy Strategy { get; init; } + public bool Dismissed { get; init; } = false; + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; +} diff --git a/src/DevBrain.Core/Models/GrowthModels.cs b/src/DevBrain.Core/Models/GrowthModels.cs new file mode 100644 index 0000000..95b9465 --- /dev/null +++ b/src/DevBrain.Core/Models/GrowthModels.cs @@ -0,0 +1,34 @@ +namespace DevBrain.Core.Models; + +using DevBrain.Core.Enums; + +public record DeveloperMetric +{ + public required string Id { get; init; } + public required string Dimension { get; init; } + public required double Value { get; init; } + public required DateTime PeriodStart { get; init; } + public required DateTime PeriodEnd { get; init; } + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; +} + +public record GrowthMilestone +{ + public required string Id { get; init; } + public required MilestoneType Type { get; init; } + public required string Description { get; init; } + public required DateTime AchievedAt { get; init; } + public string? ObservationId { get; init; } + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; +} + +public record GrowthReport +{ + public required string Id { get; init; } + public required DateTime PeriodStart { get; init; } + public required DateTime PeriodEnd { get; init; } + public IReadOnlyList Metrics { get; init; } = []; + public IReadOnlyList Milestones { get; init; } = []; + public string? Narrative { get; init; } + public DateTime GeneratedAt { get; init; } = DateTime.UtcNow; +} diff --git a/src/DevBrain.Core/Models/SessionSummary.cs b/src/DevBrain.Core/Models/SessionSummary.cs new file mode 100644 index 0000000..7d32393 --- /dev/null +++ b/src/DevBrain.Core/Models/SessionSummary.cs @@ -0,0 +1,15 @@ +namespace DevBrain.Core.Models; + +public record SessionSummary +{ + public required string Id { get; init; } + public required string SessionId { get; init; } + public required string Narrative { get; init; } + public required string Outcome { get; init; } + public required TimeSpan Duration { get; init; } + public required int ObservationCount { get; init; } + public required int FilesTouched { get; init; } + public required int DeadEndsHit { get; init; } + public IReadOnlyList Phases { get; init; } = []; + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; +} diff --git a/src/DevBrain.Core/Prompts.cs b/src/DevBrain.Core/Prompts.cs new file mode 100644 index 0000000..b029634 --- /dev/null +++ b/src/DevBrain.Core/Prompts.cs @@ -0,0 +1,161 @@ +namespace DevBrain.Core; + +public static class Prompts +{ + /// + /// Safely fills named placeholders in a prompt template. + /// Uses {NAME} syntax instead of {0} to avoid FormatException + /// when user content contains braces (common in C# code). + /// + public static string Fill(string template, params (string key, string value)[] replacements) + { + var result = template; + foreach (var (key, value) in replacements) + result = result.Replace($"{{{key}}}", value); + return result; + } + + // -- Feature 2: Session Storytelling -- + + public const string StorytellerNarrative = """ + Write a developer session narrative from these events. + + Session duration: {DURATION} + Phases: {PHASES} + Turning points: {TURNING_POINTS} + + Events: + {EVENTS} + + Rules: + - Past tense, third person ("The developer...") + - Structure: Goal -> Approach -> Obstacles -> Resolution + - Mention specific files and decisions by name + - Note dead ends and what they taught + - End with one-line "session outcome" + - Under 300 words + """; + + // -- Feature 3: Decision Replay -- + + public const string DecisionClassification = """ + Given two developer decisions about the same codebase, classify their relationship. + + Decision A (earlier): {DECISION_A} + Decision B (later): {DECISION_B} + Shared files: {SHARED_FILES} + + Classify as ONE of: + - caused_by: B was motivated by A + - supersedes: B replaces A + - resolved_by: B resolves a problem A introduced + - unrelated: no causal connection + + Respond with ONLY the classification label. + """; + + public const string DecisionChainNarrative = """ + Explain why this code exists by narrating the chain of decisions that led to it. + + File: {FILE} + Decision chain (chronological): + {CHAIN} + + Rules: + - Explain the "why" behind each decision + - Note alternatives that were rejected and why + - Highlight dead ends that were hit along the way + - Keep under 200 words + """; + + // -- Feature 4: Blast Radius Prediction -- + + public const string BlastRadiusSummary = """ + Summarize the potential impact of changing this file. + + File being changed: {FILE} + Affected files and their connection: + {AFFECTED} + Dead ends at risk of re-triggering: + {DEAD_ENDS} + + Rules: + - Focus on the highest-risk impacts + - Explain WHY each file is affected (the decision that connects them) + - Warn about dead ends that could resurface + - Keep under 150 words + """; + + // -- Feature 5: Growth Tracker - Complexity -- + + public const string ComplexityClassification = """ + Rate the complexity of this development task from 1-5: + 1 = Routine (config changes, simple CRUD, renames) + 2 = Moderate (new feature with clear requirements) + 3 = Significant (cross-cutting changes, new abstractions) + 4 = Complex (architectural decisions, novel algorithms) + 5 = Expert (system design, performance-critical, multi-system integration) + + Thread summary: {SUMMARY} + Files changed: {FILES} + Decisions made: {DECISIONS} + Errors encountered: {ERRORS} + + Respond with ONLY the number. + """; + + // -- Feature 5: Growth Tracker - Quality -- + + public const string ErrorClassification = """ + Classify this development error into ONE category: + - logic_bug: incorrect logic, wrong algorithm, bad assumption + - typo: syntax error, misspelling, wrong variable name + - environment: config issue, missing dependency, wrong version + - external: third-party API failure, network issue, dependency bug + + Error: {ERROR} + Context: {CONTEXT} + + Respond with ONLY the category. + """; + + // -- Feature 5: Growth Tracker - Weekly Narrative -- + + public const string GrowthNarrative = """ + Given these developer metrics for the past week, write 2-3 sentences + highlighting the most interesting trend or achievement. + + Metrics: {METRICS} + Milestones: {MILESTONES} + 4-week trend: {TREND} + Complexity trend: {COMPLEXITY} + Quality trend: {QUALITY} + Error breakdown: {ERROR_BREAKDOWN} + + Rules: + - Be encouraging but honest + - Focus on growth, not absolute numbers + - Never compare to other developers + - If complexity is rising while quality holds, highlight this prominently + - Keep under 100 words + """; + + // -- Existing: Briefing Agent (migrated from BriefingAgent.cs) -- + + public const string BriefingGeneration = """ + Generate a daily development briefing based on the following observations from the last 24 hours. + Summarize key decisions, errors encountered, files changed, and overall progress. + Format as markdown with sections. + + Observations: + {OBSERVATIONS} + """; + + // -- Existing: Compression Agent (migrated from CompressionAgent.cs) -- + + public const string CompressionSummarization = """ + Summarize the following development observation concisely: + + {CONTENT} + """; +} diff --git a/src/DevBrain.Storage/BlastRadiusCalculator.cs b/src/DevBrain.Storage/BlastRadiusCalculator.cs new file mode 100644 index 0000000..f1ce202 --- /dev/null +++ b/src/DevBrain.Storage/BlastRadiusCalculator.cs @@ -0,0 +1,90 @@ +namespace DevBrain.Storage; + +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; + +public class BlastRadiusCalculator +{ + private readonly IGraphStore _graph; + private readonly IDeadEndStore _deadEnds; + private readonly DecisionChainBuilder _chainBuilder; + + public BlastRadiusCalculator(IGraphStore graph, IDeadEndStore deadEnds, DecisionChainBuilder chainBuilder) + { + _graph = graph; + _deadEnds = deadEnds; + _chainBuilder = chainBuilder; + } + + public async Task Calculate(string filePath, int maxHops = 3) + { + // Step 1: Find decisions connected to this file + var related = await _graph.GetRelatedToFile(filePath); + var seedDecisions = related + .Where(n => n.Type is "Decision" or "Bug") + .ToList(); + + if (seedDecisions.Count == 0) + return new BlastRadius { SourceFile = filePath }; + + // Step 2: Traverse causal edges with depth tracking + var nodesWithDepth = await _chainBuilder.TraverseCausalGraphWithDepth(seedDecisions, maxHops); + + // Step 3: For each downstream decision, find connected File nodes + var affectedFiles = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var (nodeId, (node, depth)) in nodesWithDepth) + { + var fileNeighbors = await _graph.GetNeighbors(node.Id, hops: 1, edgeType: "references"); + foreach (var fileNode in fileNeighbors) + { + if (fileNode.Type != "File") continue; + if (fileNode.Name.Equals(filePath, StringComparison.OrdinalIgnoreCase)) continue; + if (affectedFiles.ContainsKey(fileNode.Name)) continue; + + // Count dead ends relevant to this specific node's chain path + var deadEndsForNode = node.Type == "Bug" ? 1 : 0; + var chainLength = depth + 1; // depth 0 = direct connection = chain length 1 + var recency = ComputeRecencyDecay(node.CreatedAt); + var risk = ComputeRiskScore(chainLength, deadEndsForNode, recency); + + affectedFiles[fileNode.Name] = new BlastRadiusEntry + { + FilePath = fileNode.Name, + RiskScore = Math.Round(risk, 3), + ChainLength = chainLength, + Reason = $"Linked via decision: {node.Name}", + LinkedDecisionId = node.Id + }; + } + } + + // Step 4: Find dead ends at risk + var deadEndsAtRisk = await _deadEnds.FindByFiles([filePath]); + var deadEndIds = deadEndsAtRisk.Select(d => d.Id).ToList(); + + var sortedFiles = affectedFiles.Values + .OrderByDescending(f => f.RiskScore) + .ToList(); + + return new BlastRadius + { + SourceFile = filePath, + AffectedFiles = sortedFiles, + DeadEndsAtRisk = deadEndIds + }; + } + + private static double ComputeRecencyDecay(DateTime createdAt) + { + var daysSince = (DateTime.UtcNow - createdAt).TotalDays; + return Math.Max(0.1, 1.0 - (daysSince / 180.0)); + } + + internal static double ComputeRiskScore(int chainLength, int deadEndsInChain, double recencyDecay) + { + var deadEndMultiplier = 1.0 + (0.5 * deadEndsInChain); + var raw = (1.0 / Math.Max(1, chainLength)) * deadEndMultiplier * recencyDecay; + return Math.Min(1.0, raw); + } +} diff --git a/src/DevBrain.Storage/DecisionChainBuilder.cs b/src/DevBrain.Storage/DecisionChainBuilder.cs new file mode 100644 index 0000000..1d38884 --- /dev/null +++ b/src/DevBrain.Storage/DecisionChainBuilder.cs @@ -0,0 +1,205 @@ +namespace DevBrain.Storage; + +using DevBrain.Core.Enums; +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; + +public class DecisionChainBuilder +{ + private readonly IGraphStore _graph; + private readonly IObservationStore _observations; + + private static readonly IReadOnlyList CausalEdgeTypes = ["caused_by", "supersedes", "resolved_by"]; + private static readonly HashSet DecisionNodeTypes = ["Decision", "Bug"]; + + public DecisionChainBuilder(IGraphStore graph, IObservationStore observations) + { + _graph = graph; + _observations = observations; + } + + /// + /// Traverse causal edges from seed nodes outward. Reusable by Blast Radius. + /// + public async Task> TraverseCausalGraph( + IEnumerable seedNodes, int maxHops, + IReadOnlyList? edgeTypes = null, + HashSet? nodeTypeFilter = null) + { + var types = edgeTypes ?? CausalEdgeTypes; + var filter = nodeTypeFilter ?? DecisionNodeTypes; + var allNodes = new Dictionary(); + + foreach (var node in seedNodes) + { + if (filter.Contains(node.Type)) + allNodes.TryAdd(node.Id, node); + + var neighbors = await _graph.GetNeighbors(node.Id, maxHops, types); + foreach (var neighbor in neighbors) + { + if (filter.Contains(neighbor.Type)) + allNodes.TryAdd(neighbor.Id, neighbor); + } + } + + return allNodes; + } + + /// + /// Traverse causal edges with depth tracking per node. Used by BlastRadiusCalculator + /// to compute accurate chain lengths for risk scoring. + /// + public async Task> TraverseCausalGraphWithDepth( + IEnumerable seedNodes, int maxHops, + IReadOnlyList? edgeTypes = null, + HashSet? nodeTypeFilter = null) + { + var types = edgeTypes ?? CausalEdgeTypes; + var filter = nodeTypeFilter ?? DecisionNodeTypes; + var result = new Dictionary(); + + // BFS with depth tracking + var seedList = seedNodes.ToList(); + var queue = new Queue<(GraphNode Node, int Depth)>(); + + foreach (var seed in seedList) + { + if (filter.Contains(seed.Type) && result.TryAdd(seed.Id, (seed, 0))) + queue.Enqueue((seed, 0)); + } + + while (queue.Count > 0) + { + var (current, depth) = queue.Dequeue(); + if (depth >= maxHops) continue; + + var neighbors = await _graph.GetNeighbors(current.Id, hops: 1, types); + foreach (var neighbor in neighbors) + { + if (!filter.Contains(neighbor.Type)) continue; + if (result.TryAdd(neighbor.Id, (neighbor, depth + 1))) + queue.Enqueue((neighbor, depth + 1)); + } + } + + return result; + } + + public async Task BuildForFile(string filePath, int maxHops = 3) + { + var related = await _graph.GetRelatedToFile(filePath); + var seedNodes = related.Where(n => DecisionNodeTypes.Contains(n.Type)).ToList(); + + if (seedNodes.Count == 0) + return null; + + var allNodes = await TraverseCausalGraph(seedNodes, maxHops); + var steps = await BuildSteps(allNodes.Values); + + if (steps.Count == 0) + return null; + + // Deterministic root: chronologically earliest step + var rootNodeId = steps[0].ObservationId; + + return new DecisionChain + { + Id = Guid.NewGuid().ToString(), + RootNodeId = rootNodeId, + Narrative = BuildNarrativePlaceholder(filePath, steps), + Steps = steps + }; + } + + public async Task BuildForDecision(string nodeId, int maxHops = 4) + { + var rootNode = await _graph.GetNode(nodeId); + if (rootNode is null) + return null; + + // Reject non-Decision/Bug root nodes + if (!DecisionNodeTypes.Contains(rootNode.Type)) + return null; + + var allNodes = await TraverseCausalGraph([rootNode], maxHops); + var steps = await BuildSteps(allNodes.Values); + + if (steps.Count == 0) + return null; + + return new DecisionChain + { + Id = Guid.NewGuid().ToString(), + RootNodeId = rootNode.Id, + Narrative = BuildNarrativePlaceholder(rootNode.Name, steps), + Steps = steps + }; + } + + private async Task> BuildSteps(IEnumerable nodes) + { + var nodeList = nodes.ToList(); + + // Batch-fetch observations to avoid N+1 queries + var sourceIds = nodeList + .Where(n => n.SourceId is not null) + .Select(n => n.SourceId!) + .Distinct() + .ToList(); + + var obsMap = new Dictionary(); + foreach (var id in sourceIds) + { + var obs = await _observations.GetById(id); + if (obs is not null) + obsMap[id] = obs; + } + + var steps = new List(); + foreach (var node in nodeList) + { + // Skip non-Decision/Bug nodes + if (!DecisionNodeTypes.Contains(node.Type)) + continue; + + Observation? obs = null; + if (node.SourceId is not null) + obsMap.TryGetValue(node.SourceId, out obs); + + var stepType = node.Type == "Bug" + ? DecisionStepType.DeadEnd + : DecisionStepType.Decision; + + steps.Add(new DecisionStep + { + ObservationId = node.SourceId ?? node.Id, + Summary = node.Name, + Timestamp = obs?.Timestamp ?? node.CreatedAt, + StepType = stepType, + FilesInvolved = obs?.FilesInvolved ?? [] + }); + } + + return steps.OrderBy(s => s.Timestamp).ToList(); + } + + private static string BuildNarrativePlaceholder(string root, IReadOnlyList steps) + { + var decisions = steps.Where(s => s.StepType == DecisionStepType.Decision).ToList(); + var deadEnds = steps.Where(s => s.StepType == DecisionStepType.DeadEnd).ToList(); + + var parts = new List + { + $"Decision chain for '{root}' spans {steps.Count} step(s)." + }; + + if (decisions.Count > 0) + parts.Add($"{decisions.Count} decision(s): {string.Join("; ", decisions.Select(d => d.Summary))}."); + + if (deadEnds.Count > 0) + parts.Add($"{deadEnds.Count} dead end(s) encountered along the way."); + + return string.Join(" ", parts); + } +} diff --git a/src/DevBrain.Storage/DevBrain.Storage.csproj b/src/DevBrain.Storage/DevBrain.Storage.csproj index 00e80bb..6735712 100644 --- a/src/DevBrain.Storage/DevBrain.Storage.csproj +++ b/src/DevBrain.Storage/DevBrain.Storage.csproj @@ -14,4 +14,8 @@ enable + + + + diff --git a/src/DevBrain.Storage/Schema/SchemaManager.cs b/src/DevBrain.Storage/Schema/SchemaManager.cs index 994e424..b484b7a 100644 --- a/src/DevBrain.Storage/Schema/SchemaManager.cs +++ b/src/DevBrain.Storage/Schema/SchemaManager.cs @@ -118,6 +118,67 @@ INSERT INTO observations_fts(observations_fts, rowid, summary, raw_content, tags INSERT INTO observations_fts(rowid, summary, raw_content, tags) VALUES (new.rowid, new.summary, new.raw_content, new.tags); END; + + CREATE TABLE IF NOT EXISTS deja_vu_alerts ( + id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + matched_dead_end_id TEXT NOT NULL, + confidence REAL NOT NULL, + message TEXT NOT NULL, + strategy TEXT NOT NULL, + dismissed INTEGER DEFAULT 0, + created_at TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_dva_dedup ON deja_vu_alerts(thread_id, matched_dead_end_id); + CREATE INDEX IF NOT EXISTS idx_dva_active ON deja_vu_alerts(dismissed); + + CREATE TABLE IF NOT EXISTS session_summaries ( + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL UNIQUE, + narrative TEXT NOT NULL, + outcome TEXT NOT NULL, + duration_seconds INTEGER NOT NULL, + observation_count INTEGER NOT NULL, + files_touched INTEGER NOT NULL, + dead_ends_hit INTEGER NOT NULL, + phases TEXT NOT NULL, + created_at TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_ss_session ON session_summaries(session_id); + + CREATE TABLE IF NOT EXISTS developer_metrics ( + id TEXT PRIMARY KEY, + dimension TEXT NOT NULL, + value REAL NOT NULL, + period_start TEXT NOT NULL, + period_end TEXT NOT NULL, + created_at TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_dm_dimension ON developer_metrics(dimension, period_start); + + CREATE TABLE IF NOT EXISTS milestones ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL, + description TEXT NOT NULL, + achieved_at TEXT NOT NULL, + observation_id TEXT, + created_at TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_ms_type ON milestones(type, achieved_at); + + CREATE TABLE IF NOT EXISTS growth_reports ( + id TEXT PRIMARY KEY, + period_start TEXT NOT NULL, + period_end TEXT NOT NULL, + metrics TEXT NOT NULL, + milestones TEXT NOT NULL, + narrative TEXT, + generated_at TEXT NOT NULL + ); """; cmd.ExecuteNonQuery(); } diff --git a/src/DevBrain.Storage/SqliteAlertStore.cs b/src/DevBrain.Storage/SqliteAlertStore.cs new file mode 100644 index 0000000..4e03ff0 --- /dev/null +++ b/src/DevBrain.Storage/SqliteAlertStore.cs @@ -0,0 +1,100 @@ +namespace DevBrain.Storage; + +using System.Globalization; +using DevBrain.Core.Enums; +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; +using Microsoft.Data.Sqlite; + +public class SqliteAlertStore : IAlertStore +{ + private readonly SqliteConnection _connection; + + public SqliteAlertStore(SqliteConnection connection) + { + _connection = connection; + } + + public async Task Add(DejaVuAlert alert) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = """ + INSERT INTO deja_vu_alerts (id, thread_id, matched_dead_end_id, confidence, + message, strategy, dismissed, created_at) + VALUES (@id, @threadId, @deadEndId, @confidence, + @message, @strategy, @dismissed, @createdAt) + """; + + cmd.Parameters.AddWithValue("@id", alert.Id); + cmd.Parameters.AddWithValue("@threadId", alert.ThreadId); + cmd.Parameters.AddWithValue("@deadEndId", alert.MatchedDeadEndId); + cmd.Parameters.AddWithValue("@confidence", alert.Confidence); + cmd.Parameters.AddWithValue("@message", alert.Message); + cmd.Parameters.AddWithValue("@strategy", alert.Strategy.ToString()); + cmd.Parameters.AddWithValue("@dismissed", alert.Dismissed ? 1 : 0); + cmd.Parameters.AddWithValue("@createdAt", alert.CreatedAt.ToString("o")); + + await cmd.ExecuteNonQueryAsync(); + return alert; + } + + public async Task> GetActive() + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = "SELECT * FROM deja_vu_alerts WHERE dismissed = 0 ORDER BY created_at DESC"; + return await ReadAlerts(cmd); + } + + public async Task> GetAll(int limit = 100) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = "SELECT * FROM deja_vu_alerts ORDER BY created_at DESC LIMIT @limit"; + cmd.Parameters.AddWithValue("@limit", limit); + return await ReadAlerts(cmd); + } + + public async Task Dismiss(string id) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = "UPDATE deja_vu_alerts SET dismissed = 1 WHERE id = @id"; + cmd.Parameters.AddWithValue("@id", id); + var rows = await cmd.ExecuteNonQueryAsync(); + return rows > 0; + } + + public async Task Exists(string threadId, string deadEndId) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = """ + SELECT COUNT(*) FROM deja_vu_alerts + WHERE thread_id = @threadId AND matched_dead_end_id = @deadEndId AND dismissed = 0 + """; + cmd.Parameters.AddWithValue("@threadId", threadId); + cmd.Parameters.AddWithValue("@deadEndId", deadEndId); + + var result = await cmd.ExecuteScalarAsync(); + return Convert.ToInt64(result) > 0; + } + + private static async Task> ReadAlerts(SqliteCommand cmd) + { + var results = new List(); + using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + results.Add(new DejaVuAlert + { + Id = reader.GetString(reader.GetOrdinal("id")), + ThreadId = reader.GetString(reader.GetOrdinal("thread_id")), + MatchedDeadEndId = reader.GetString(reader.GetOrdinal("matched_dead_end_id")), + Confidence = reader.GetDouble(reader.GetOrdinal("confidence")), + Message = reader.GetString(reader.GetOrdinal("message")), + Strategy = Enum.Parse(reader.GetString(reader.GetOrdinal("strategy"))), + Dismissed = reader.GetInt32(reader.GetOrdinal("dismissed")) == 1, + CreatedAt = DateTime.Parse(reader.GetString(reader.GetOrdinal("created_at")), + CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind) + }); + } + return results; + } +} diff --git a/src/DevBrain.Storage/SqliteDeadEndStore.cs b/src/DevBrain.Storage/SqliteDeadEndStore.cs new file mode 100644 index 0000000..dc29849 --- /dev/null +++ b/src/DevBrain.Storage/SqliteDeadEndStore.cs @@ -0,0 +1,153 @@ +namespace DevBrain.Storage; + +using System.Globalization; +using System.Text.Json; +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; +using Microsoft.Data.Sqlite; + +public class SqliteDeadEndStore : IDeadEndStore +{ + private readonly SqliteConnection _connection; + + public SqliteDeadEndStore(SqliteConnection connection) + { + _connection = connection; + } + + public async Task Add(DeadEnd deadEnd) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = """ + INSERT INTO dead_ends (id, thread_id, project, description, approach, reason, + files_involved, detected_at, created_at) + VALUES (@id, @threadId, @project, @description, @approach, @reason, + @filesInvolved, @detectedAt, @createdAt) + """; + + cmd.Parameters.AddWithValue("@id", deadEnd.Id); + cmd.Parameters.AddWithValue("@threadId", (object?)deadEnd.ThreadId ?? DBNull.Value); + cmd.Parameters.AddWithValue("@project", deadEnd.Project); + cmd.Parameters.AddWithValue("@description", deadEnd.Description); + cmd.Parameters.AddWithValue("@approach", deadEnd.Approach); + cmd.Parameters.AddWithValue("@reason", deadEnd.Reason); + cmd.Parameters.AddWithValue("@filesInvolved", JsonSerializer.Serialize(deadEnd.FilesInvolved)); + cmd.Parameters.AddWithValue("@detectedAt", deadEnd.DetectedAt.ToString("o")); + cmd.Parameters.AddWithValue("@createdAt", deadEnd.CreatedAt.ToString("o")); + + await cmd.ExecuteNonQueryAsync(); + return deadEnd; + } + + public async Task> Query(DeadEndFilter filter) + { + using var cmd = _connection.CreateCommand(); + var clauses = new List(); + + if (filter.Project is not null) + { + clauses.Add("project = @project"); + cmd.Parameters.AddWithValue("@project", filter.Project); + } + if (filter.ThreadId is not null) + { + clauses.Add("thread_id = @threadId"); + cmd.Parameters.AddWithValue("@threadId", filter.ThreadId); + } + if (filter.After is not null) + { + clauses.Add("detected_at > @after"); + cmd.Parameters.AddWithValue("@after", filter.After.Value.ToString("o")); + } + if (filter.Before is not null) + { + clauses.Add("detected_at < @before"); + cmd.Parameters.AddWithValue("@before", filter.Before.Value.ToString("o")); + } + + var where = clauses.Count > 0 ? "WHERE " + string.Join(" AND ", clauses) : ""; + cmd.CommandText = $"SELECT * FROM dead_ends {where} ORDER BY detected_at DESC LIMIT @limit OFFSET @offset"; + cmd.Parameters.AddWithValue("@limit", filter.Limit); + cmd.Parameters.AddWithValue("@offset", filter.Offset); + + return await ReadDeadEnds(cmd); + } + + public async Task> FindByFiles(IReadOnlyList filePaths) + { + if (filePaths.Count == 0) + return []; + + // Pre-filter in SQL using LIKE on the JSON text column, then verify client-side. + // This avoids a full-table scan while handling JSON array storage correctly. + using var cmd = _connection.CreateCommand(); + var likeClauses = new List(); + for (int i = 0; i < filePaths.Count; i++) + { + likeClauses.Add($"files_involved LIKE @fp{i}"); + cmd.Parameters.AddWithValue($"@fp{i}", $"%{EscapeLike(filePaths[i])}%"); + } + + cmd.CommandText = $"SELECT * FROM dead_ends WHERE {string.Join(" OR ", likeClauses)} ORDER BY detected_at DESC LIMIT 200"; + + var candidates = await ReadDeadEnds(cmd); + var fileSet = filePaths.ToHashSet(StringComparer.OrdinalIgnoreCase); + + // Exact match after JSON deserialization (LIKE may produce false positives) + return candidates + .Where(de => de.FilesInvolved.Any(f => fileSet.Contains(f))) + .ToList(); + } + + private static string EscapeLike(string value) + { + return value.Replace("%", "").Replace("_", "").Replace("[", ""); + } + + public async Task> FindSimilar(string description, int limit = 5) + { + var keywords = description.Split(' ', StringSplitOptions.RemoveEmptyEntries) + .Where(w => w.Length > 2) + .Take(5) + .ToList(); + + if (keywords.Count == 0) + return []; + + using var cmd = _connection.CreateCommand(); + var likeClause = string.Join(" OR ", keywords.Select((_, i) => $"description LIKE @kw{i}")); + cmd.CommandText = $"SELECT * FROM dead_ends WHERE {likeClause} ORDER BY detected_at DESC LIMIT @limit"; + + for (int i = 0; i < keywords.Count; i++) + cmd.Parameters.AddWithValue($"@kw{i}", $"%{keywords[i]}%"); + cmd.Parameters.AddWithValue("@limit", limit); + + return await ReadDeadEnds(cmd); + } + + private static async Task> ReadDeadEnds(SqliteCommand cmd) + { + var results = new List(); + using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + results.Add(MapDeadEnd(reader)); + } + return results; + } + + private static DeadEnd MapDeadEnd(SqliteDataReader reader) => new() + { + Id = reader.GetString(reader.GetOrdinal("id")), + ThreadId = reader.IsDBNull(reader.GetOrdinal("thread_id")) ? null : reader.GetString(reader.GetOrdinal("thread_id")), + Project = reader.GetString(reader.GetOrdinal("project")), + Description = reader.GetString(reader.GetOrdinal("description")), + Approach = reader.GetString(reader.GetOrdinal("approach")), + Reason = reader.GetString(reader.GetOrdinal("reason")), + FilesInvolved = reader.IsDBNull(reader.GetOrdinal("files_involved")) + ? [] + : JsonSerializer.Deserialize>(reader.GetString(reader.GetOrdinal("files_involved"))) ?? [], + DetectedAt = DateTime.Parse(reader.GetString(reader.GetOrdinal("detected_at")), CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind), + CreatedAt = DateTime.Parse(reader.GetString(reader.GetOrdinal("created_at")), CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind) + }; +} diff --git a/src/DevBrain.Storage/SqliteGraphStore.cs b/src/DevBrain.Storage/SqliteGraphStore.cs index ab8d2a8..19db8fc 100644 --- a/src/DevBrain.Storage/SqliteGraphStore.cs +++ b/src/DevBrain.Storage/SqliteGraphStore.cs @@ -74,6 +74,18 @@ public async Task> GetNodesByType(string type) return nodes; } + public async Task GetNodeBySourceId(string sourceId) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = "SELECT id, type, name, data, source_id, created_at FROM graph_nodes WHERE source_id = @sourceId LIMIT 1"; + cmd.Parameters.AddWithValue("@sourceId", sourceId); + + using var reader = await cmd.ExecuteReaderAsync(); + if (await reader.ReadAsync()) + return ReadNode(reader); + return null; + } + public async Task RemoveNode(string id) { // Delete all connected edges first (cascade) @@ -167,6 +179,50 @@ FROM neighbors nb return nodes; } + public async Task> GetNeighbors(string nodeId, int hops, IReadOnlyList edgeTypes) + { + if (edgeTypes.Count == 0) + return await GetNeighbors(nodeId, hops); + + var edgeParams = new List(); + for (int i = 0; i < edgeTypes.Count; i++) + edgeParams.Add($"@et{i}"); + var inClause = $"AND e.type IN ({string.Join(", ", edgeParams)})"; + + using var cmd = _connection.CreateCommand(); + cmd.CommandText = $@" + WITH RECURSIVE neighbors(node_id, depth, path) AS ( + SELECT @startId, 0, @startId + UNION + SELECT + CASE WHEN e.source_id = n.node_id THEN e.target_id ELSE e.source_id END, + n.depth + 1, + n.path || ',' || CASE WHEN e.source_id = n.node_id THEN e.target_id ELSE e.source_id END + FROM neighbors n + JOIN graph_edges e ON (e.source_id = n.node_id OR e.target_id = n.node_id) + {inClause} + WHERE n.depth < @hops + AND instr(n.path, CASE WHEN e.source_id = n.node_id THEN e.target_id ELSE e.source_id END) = 0 + ) + SELECT DISTINCT gn.id, gn.type, gn.name, gn.data, gn.source_id, gn.created_at + FROM neighbors nb + JOIN graph_nodes gn ON gn.id = nb.node_id + WHERE nb.node_id != @startId"; + + cmd.Parameters.AddWithValue("@startId", nodeId); + cmd.Parameters.AddWithValue("@hops", hops); + for (int i = 0; i < edgeTypes.Count; i++) + cmd.Parameters.AddWithValue($"@et{i}", edgeTypes[i]); + + var nodes = new List(); + using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + nodes.Add(ReadNode(reader)); + } + return nodes; + } + public async Task> FindPaths(string fromId, string toId, int maxDepth = 4) { // Recursive CTE to find all paths (directional: source→target only) diff --git a/src/DevBrain.Storage/SqliteGrowthStore.cs b/src/DevBrain.Storage/SqliteGrowthStore.cs new file mode 100644 index 0000000..1819585 --- /dev/null +++ b/src/DevBrain.Storage/SqliteGrowthStore.cs @@ -0,0 +1,229 @@ +namespace DevBrain.Storage; + +using System.Globalization; +using System.Text.Json; +using DevBrain.Core.Enums; +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; +using Microsoft.Data.Sqlite; + +public class SqliteGrowthStore : IGrowthStore +{ + private readonly SqliteConnection _connection; + + public SqliteGrowthStore(SqliteConnection connection) + { + _connection = connection; + } + + public async Task AddMetric(DeveloperMetric metric) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = """ + INSERT INTO developer_metrics (id, dimension, value, period_start, period_end, created_at) + VALUES (@id, @dimension, @value, @periodStart, @periodEnd, @createdAt) + """; + cmd.Parameters.AddWithValue("@id", metric.Id); + cmd.Parameters.AddWithValue("@dimension", metric.Dimension); + cmd.Parameters.AddWithValue("@value", metric.Value); + cmd.Parameters.AddWithValue("@periodStart", metric.PeriodStart.ToString("o")); + cmd.Parameters.AddWithValue("@periodEnd", metric.PeriodEnd.ToString("o")); + cmd.Parameters.AddWithValue("@createdAt", metric.CreatedAt.ToString("o")); + await cmd.ExecuteNonQueryAsync(); + return metric; + } + + public async Task> GetMetrics(string dimension, int weeks = 12) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = """ + SELECT * FROM developer_metrics + WHERE dimension = @dimension + ORDER BY period_start DESC LIMIT @limit + """; + cmd.Parameters.AddWithValue("@dimension", dimension); + cmd.Parameters.AddWithValue("@limit", weeks); + + var results = new List(); + using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + results.Add(MapMetric(reader)); + return results; + } + + public async Task> GetLatestMetrics() + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = """ + SELECT m.* FROM developer_metrics m + INNER JOIN ( + SELECT dimension, MAX(period_start) as max_start + FROM developer_metrics GROUP BY dimension + ) latest ON m.dimension = latest.dimension AND m.period_start = latest.max_start + ORDER BY m.dimension + """; + + var results = new List(); + using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + results.Add(MapMetric(reader)); + return results; + } + + public async Task AddMilestone(GrowthMilestone milestone) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = """ + INSERT INTO milestones (id, type, description, achieved_at, observation_id, created_at) + VALUES (@id, @type, @description, @achievedAt, @observationId, @createdAt) + """; + cmd.Parameters.AddWithValue("@id", milestone.Id); + cmd.Parameters.AddWithValue("@type", milestone.Type.ToString()); + cmd.Parameters.AddWithValue("@description", milestone.Description); + cmd.Parameters.AddWithValue("@achievedAt", milestone.AchievedAt.ToString("o")); + cmd.Parameters.AddWithValue("@observationId", (object?)milestone.ObservationId ?? DBNull.Value); + cmd.Parameters.AddWithValue("@createdAt", milestone.CreatedAt.ToString("o")); + await cmd.ExecuteNonQueryAsync(); + return milestone; + } + + public async Task> GetMilestones(int limit = 50) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = "SELECT * FROM milestones ORDER BY achieved_at DESC LIMIT @limit"; + cmd.Parameters.AddWithValue("@limit", limit); + + var results = new List(); + using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + results.Add(MapMilestone(reader)); + return results; + } + + public async Task AddReport(GrowthReport report) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = """ + INSERT INTO growth_reports (id, period_start, period_end, metrics, milestones, narrative, generated_at) + VALUES (@id, @periodStart, @periodEnd, @metrics, @milestones, @narrative, @generatedAt) + """; + cmd.Parameters.AddWithValue("@id", report.Id); + cmd.Parameters.AddWithValue("@periodStart", report.PeriodStart.ToString("o")); + cmd.Parameters.AddWithValue("@periodEnd", report.PeriodEnd.ToString("o")); + cmd.Parameters.AddWithValue("@metrics", JsonSerializer.Serialize(report.Metrics.Select(m => m.Id))); + cmd.Parameters.AddWithValue("@milestones", JsonSerializer.Serialize(report.Milestones.Select(m => m.Id))); + cmd.Parameters.AddWithValue("@narrative", (object?)report.Narrative ?? DBNull.Value); + cmd.Parameters.AddWithValue("@generatedAt", report.GeneratedAt.ToString("o")); + await cmd.ExecuteNonQueryAsync(); + return report; + } + + public async Task GetLatestReport() + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = "SELECT * FROM growth_reports ORDER BY generated_at DESC LIMIT 1"; + + using var reader = await cmd.ExecuteReaderAsync(); + if (await reader.ReadAsync()) + return await HydrateReport(MapReportShell(reader)); + return null; + } + + public async Task> GetReports(int limit = 12) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = "SELECT * FROM growth_reports ORDER BY generated_at DESC LIMIT @limit"; + cmd.Parameters.AddWithValue("@limit", limit); + + var results = new List(); + using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + results.Add(await HydrateReport(MapReportShell(reader))); + return results; + } + + private async Task HydrateReport( + (GrowthReport Report, List MetricIds, List MilestoneIds) shell) + { + var metrics = new List(); + foreach (var id in shell.MetricIds) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = "SELECT * FROM developer_metrics WHERE id = @id"; + cmd.Parameters.AddWithValue("@id", id); + using var reader = await cmd.ExecuteReaderAsync(); + if (await reader.ReadAsync()) + metrics.Add(MapMetric(reader)); + } + + var milestones = new List(); + foreach (var id in shell.MilestoneIds) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = "SELECT * FROM milestones WHERE id = @id"; + cmd.Parameters.AddWithValue("@id", id); + using var reader = await cmd.ExecuteReaderAsync(); + if (await reader.ReadAsync()) + milestones.Add(MapMilestone(reader)); + } + + return shell.Report with { Metrics = metrics, Milestones = milestones }; + } + + public async Task Clear() + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = "DELETE FROM developer_metrics; DELETE FROM milestones; DELETE FROM growth_reports;"; + await cmd.ExecuteNonQueryAsync(); + } + + private static DeveloperMetric MapMetric(SqliteDataReader reader) => new() + { + Id = reader.GetString(reader.GetOrdinal("id")), + Dimension = reader.GetString(reader.GetOrdinal("dimension")), + Value = reader.GetDouble(reader.GetOrdinal("value")), + PeriodStart = DateTime.Parse(reader.GetString(reader.GetOrdinal("period_start")), + CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind), + PeriodEnd = DateTime.Parse(reader.GetString(reader.GetOrdinal("period_end")), + CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind), + CreatedAt = DateTime.Parse(reader.GetString(reader.GetOrdinal("created_at")), + CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind) + }; + + private static GrowthMilestone MapMilestone(SqliteDataReader reader) => new() + { + Id = reader.GetString(reader.GetOrdinal("id")), + Type = Enum.Parse(reader.GetString(reader.GetOrdinal("type"))), + Description = reader.GetString(reader.GetOrdinal("description")), + AchievedAt = DateTime.Parse(reader.GetString(reader.GetOrdinal("achieved_at")), + CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind), + ObservationId = reader.IsDBNull(reader.GetOrdinal("observation_id")) + ? null : reader.GetString(reader.GetOrdinal("observation_id")), + CreatedAt = DateTime.Parse(reader.GetString(reader.GetOrdinal("created_at")), + CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind) + }; + + private static (GrowthReport Report, List MetricIds, List MilestoneIds) MapReportShell( + SqliteDataReader reader) + { + var metricIds = JsonSerializer.Deserialize>( + reader.GetString(reader.GetOrdinal("metrics"))) ?? []; + var milestoneIds = JsonSerializer.Deserialize>( + reader.GetString(reader.GetOrdinal("milestones"))) ?? []; + + var report = new GrowthReport + { + Id = reader.GetString(reader.GetOrdinal("id")), + PeriodStart = DateTime.Parse(reader.GetString(reader.GetOrdinal("period_start")), + CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind), + PeriodEnd = DateTime.Parse(reader.GetString(reader.GetOrdinal("period_end")), + CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind), + Narrative = reader.IsDBNull(reader.GetOrdinal("narrative")) + ? null : reader.GetString(reader.GetOrdinal("narrative")), + GeneratedAt = DateTime.Parse(reader.GetString(reader.GetOrdinal("generated_at")), + CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind) + }; + + return (report, metricIds, milestoneIds); + } +} diff --git a/src/DevBrain.Storage/SqliteObservationStore.cs b/src/DevBrain.Storage/SqliteObservationStore.cs index 408ad6e..eefebe9 100644 --- a/src/DevBrain.Storage/SqliteObservationStore.cs +++ b/src/DevBrain.Storage/SqliteObservationStore.cs @@ -192,6 +192,20 @@ public async Task DeleteBefore(DateTime before) await cmd.ExecuteNonQueryAsync(); } + public async Task> GetSessionObservations(string sessionId, int limit = 500) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = "SELECT * FROM observations WHERE session_id = @sessionId ORDER BY timestamp ASC LIMIT @limit"; + cmd.Parameters.AddWithValue("@sessionId", sessionId); + cmd.Parameters.AddWithValue("@limit", limit); + + var results = new List(); + using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + results.Add(MapObservation(reader)); + return results; + } + private static Observation MapObservation(SqliteDataReader reader) => new() { Id = reader.GetString(reader.GetOrdinal("id")), diff --git a/src/DevBrain.Storage/SqliteSessionStore.cs b/src/DevBrain.Storage/SqliteSessionStore.cs new file mode 100644 index 0000000..734a57b --- /dev/null +++ b/src/DevBrain.Storage/SqliteSessionStore.cs @@ -0,0 +1,110 @@ +namespace DevBrain.Storage; + +using System.Globalization; +using System.Text.Json; +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; +using Microsoft.Data.Sqlite; + +public class SqliteSessionStore : ISessionStore +{ + private readonly SqliteConnection _connection; + + public SqliteSessionStore(SqliteConnection connection) + { + _connection = connection; + } + + public async Task Add(SessionSummary summary) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = """ + INSERT OR IGNORE INTO session_summaries (id, session_id, narrative, outcome, + duration_seconds, observation_count, files_touched, dead_ends_hit, + phases, created_at) + VALUES (@id, @sessionId, @narrative, @outcome, + @durationSeconds, @observationCount, @filesTouched, @deadEndsHit, + @phases, @createdAt) + """; + + cmd.Parameters.AddWithValue("@id", summary.Id); + cmd.Parameters.AddWithValue("@sessionId", summary.SessionId); + cmd.Parameters.AddWithValue("@narrative", summary.Narrative); + cmd.Parameters.AddWithValue("@outcome", summary.Outcome); + cmd.Parameters.AddWithValue("@durationSeconds", (int)summary.Duration.TotalSeconds); + cmd.Parameters.AddWithValue("@observationCount", summary.ObservationCount); + cmd.Parameters.AddWithValue("@filesTouched", summary.FilesTouched); + cmd.Parameters.AddWithValue("@deadEndsHit", summary.DeadEndsHit); + cmd.Parameters.AddWithValue("@phases", JsonSerializer.Serialize(summary.Phases)); + cmd.Parameters.AddWithValue("@createdAt", summary.CreatedAt.ToString("o")); + + await cmd.ExecuteNonQueryAsync(); + return summary; + } + + public async Task GetBySessionId(string sessionId) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = "SELECT * FROM session_summaries WHERE session_id = @sessionId"; + cmd.Parameters.AddWithValue("@sessionId", sessionId); + + using var reader = await cmd.ExecuteReaderAsync(); + if (await reader.ReadAsync()) + return MapSummary(reader); + return null; + } + + public async Task> GetAll(int limit = 50) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = "SELECT * FROM session_summaries ORDER BY created_at DESC LIMIT @limit"; + cmd.Parameters.AddWithValue("@limit", limit); + + var results = new List(); + using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + results.Add(MapSummary(reader)); + return results; + } + + public async Task GetLatest() + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = "SELECT * FROM session_summaries ORDER BY created_at DESC LIMIT 1"; + + using var reader = await cmd.ExecuteReaderAsync(); + if (await reader.ReadAsync()) + return MapSummary(reader); + return null; + } + + public async Task> GetByDateRange(DateTime after, DateTime before) + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = "SELECT * FROM session_summaries WHERE created_at > @after AND created_at < @before ORDER BY created_at DESC"; + cmd.Parameters.AddWithValue("@after", after.ToString("o")); + cmd.Parameters.AddWithValue("@before", before.ToString("o")); + + var results = new List(); + using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + results.Add(MapSummary(reader)); + return results; + } + + private static SessionSummary MapSummary(SqliteDataReader reader) => new() + { + Id = reader.GetString(reader.GetOrdinal("id")), + SessionId = reader.GetString(reader.GetOrdinal("session_id")), + Narrative = reader.GetString(reader.GetOrdinal("narrative")), + Outcome = reader.GetString(reader.GetOrdinal("outcome")), + Duration = TimeSpan.FromSeconds(reader.GetInt32(reader.GetOrdinal("duration_seconds"))), + ObservationCount = reader.GetInt32(reader.GetOrdinal("observation_count")), + FilesTouched = reader.GetInt32(reader.GetOrdinal("files_touched")), + DeadEndsHit = reader.GetInt32(reader.GetOrdinal("dead_ends_hit")), + Phases = JsonSerializer.Deserialize>( + reader.GetString(reader.GetOrdinal("phases"))) ?? [], + CreatedAt = DateTime.Parse(reader.GetString(reader.GetOrdinal("created_at")), + CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind) + }; +} diff --git a/tests/DevBrain.Agents.Tests/DecisionChainAgentTests.cs b/tests/DevBrain.Agents.Tests/DecisionChainAgentTests.cs new file mode 100644 index 0000000..64b1be2 --- /dev/null +++ b/tests/DevBrain.Agents.Tests/DecisionChainAgentTests.cs @@ -0,0 +1,141 @@ +using DevBrain.Agents; +using DevBrain.Core; +using DevBrain.Core.Enums; +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; +using DevBrain.Storage; +using DevBrain.Storage.Schema; +using Microsoft.Data.Sqlite; + +namespace DevBrain.Agents.Tests; + +public class DecisionChainAgentTests : IAsyncLifetime +{ + private SqliteConnection _connection = null!; + private SqliteObservationStore _obsStore = null!; + private SqliteGraphStore _graphStore = null!; + private SqliteDeadEndStore _deadEndStore = null!; + private DecisionChainAgent _agent = null!; + + public async Task InitializeAsync() + { + _connection = new SqliteConnection("Data Source=:memory:"); + await _connection.OpenAsync(); + SchemaManager.Initialize(_connection); + _obsStore = new SqliteObservationStore(_connection); + _graphStore = new SqliteGraphStore(_connection); + _deadEndStore = new SqliteDeadEndStore(_connection); + _agent = new DecisionChainAgent(); + } + + public async Task DisposeAsync() + { + await _connection.DisposeAsync(); + } + + private AgentContext CreateContext(ILlmService? llm = null) + { + return new AgentContext( + Observations: _obsStore, + Graph: _graphStore, + Vectors: new NullVectorStore(), + Llm: llm ?? new ClassifyingLlmService("caused_by"), + Settings: new Settings(), + DeadEnds: _deadEndStore + ); + } + + [Fact] + public async Task Run_CreatesEdgeBetweenRelatedDecisions() + { + var obs1 = new Observation + { + Id = "obs-1", SessionId = "s1", Timestamp = DateTime.UtcNow.AddMinutes(-5), + Project = "proj", EventType = EventType.Decision, Source = CaptureSource.ClaudeCode, + RawContent = "Decided to use SQLite", Summary = "Use SQLite for storage", + FilesInvolved = ["src/Storage.cs"] + }; + var obs2 = new Observation + { + Id = "obs-2", SessionId = "s1", Timestamp = DateTime.UtcNow, + Project = "proj", EventType = EventType.Decision, Source = CaptureSource.ClaudeCode, + RawContent = "Added WAL mode", Summary = "Enable WAL mode for concurrency", + FilesInvolved = ["src/Storage.cs"] + }; + await _obsStore.Add(obs1); + await _obsStore.Add(obs2); + + // Pre-create the LinkerAgent's node for obs1 + await _graphStore.AddNode("Decision", "Use SQLite for storage", sourceId: "obs-1"); + // Also create the File node so GetRelatedToFile works + var fileNode = await _graphStore.AddNode("File", "src/Storage.cs"); + var decNode = (await _graphStore.GetNodesByType("Decision"))[0]; + await _graphStore.AddEdge(decNode.Id, fileNode.Id, "references"); + + var ctx = CreateContext(); + var results = await _agent.Run(ctx, CancellationToken.None); + + Assert.Contains(results, r => r.Type == AgentOutputType.DecisionChainBuilt); + + var decisionNodes = await _graphStore.GetNodesByType("Decision"); + Assert.True(decisionNodes.Count >= 2); + + var newNode = decisionNodes.FirstOrDefault(n => n.SourceId == "obs-2"); + Assert.NotNull(newNode); + + var neighbors = await _graphStore.GetNeighbors(newNode!.Id, hops: 1, edgeTypes: ["caused_by"]); + Assert.Single(neighbors); + } + + [Fact] + public async Task Run_SkipsWhenLlmClassifiesAsUnrelated() + { + var obs1 = new Observation + { + Id = "obs-1", SessionId = "s1", Timestamp = DateTime.UtcNow.AddMinutes(-5), + Project = "proj", EventType = EventType.Decision, Source = CaptureSource.ClaudeCode, + RawContent = "Use Redis for cache", Summary = "Redis caching", + FilesInvolved = ["src/Cache.cs"] + }; + var obs2 = new Observation + { + Id = "obs-2", SessionId = "s1", Timestamp = DateTime.UtcNow, + Project = "proj", EventType = EventType.Decision, Source = CaptureSource.ClaudeCode, + RawContent = "Add logging", Summary = "Structured logging", + FilesInvolved = ["src/Cache.cs"] + }; + await _obsStore.Add(obs1); + await _obsStore.Add(obs2); + + await _graphStore.AddNode("Decision", "Redis caching", sourceId: "obs-1"); + var fileNode = await _graphStore.AddNode("File", "src/Cache.cs"); + var decNode = (await _graphStore.GetNodesByType("Decision"))[0]; + await _graphStore.AddEdge(decNode.Id, fileNode.Id, "references"); + + var ctx = CreateContext(new ClassifyingLlmService("unrelated")); + var results = await _agent.Run(ctx, CancellationToken.None); + + var decisionNodes = await _graphStore.GetNodesByType("Decision"); + var newNode = decisionNodes.FirstOrDefault(n => n.SourceId == "obs-2"); + if (newNode is not null) + { + var neighbors = await _graphStore.GetNeighbors(newNode.Id, hops: 1, edgeTypes: ["caused_by", "supersedes", "resolved_by"]); + Assert.Empty(neighbors); + } + } + + private class ClassifyingLlmService : ILlmService + { + private readonly string _response; + public ClassifyingLlmService(string response) => _response = response; + public bool IsLocalAvailable => true; + public bool IsCloudAvailable => false; + public int CloudRequestsToday => 0; + public int QueueDepth => 0; + public Task Submit(LlmTask task, CancellationToken ct = default) + => Task.FromResult(new LlmResult { TaskId = task.Id, Success = true, Content = _response }); + public Task Embed(string text, CancellationToken ct = default) + => Task.FromResult(Array.Empty()); + } + +} diff --git a/tests/DevBrain.Agents.Tests/DejaVuAgentTests.cs b/tests/DevBrain.Agents.Tests/DejaVuAgentTests.cs new file mode 100644 index 0000000..ddf84b2 --- /dev/null +++ b/tests/DevBrain.Agents.Tests/DejaVuAgentTests.cs @@ -0,0 +1,233 @@ +using DevBrain.Agents; +using DevBrain.Core.Enums; +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; +using DevBrain.Storage; +using DevBrain.Storage.Schema; +using Microsoft.Data.Sqlite; + +namespace DevBrain.Agents.Tests; + +public class DejaVuAgentTests : IAsyncLifetime +{ + private SqliteConnection _connection = null!; + private SqliteObservationStore _obsStore = null!; + private SqliteGraphStore _graphStore = null!; + private SqliteDeadEndStore _deadEndStore = null!; + private SqliteAlertStore _alertStore = null!; + private DejaVuAgent _agent = null!; + + public async Task InitializeAsync() + { + _connection = new SqliteConnection("Data Source=:memory:"); + await _connection.OpenAsync(); + SchemaManager.Initialize(_connection); + _obsStore = new SqliteObservationStore(_connection); + _graphStore = new SqliteGraphStore(_connection); + _deadEndStore = new SqliteDeadEndStore(_connection); + _alertStore = new SqliteAlertStore(_connection); + _agent = new DejaVuAgent(_alertStore); + } + + public async Task DisposeAsync() + { + await _connection.DisposeAsync(); + } + + private AgentContext CreateContext() + { + return new AgentContext( + Observations: _obsStore, + Graph: _graphStore, + Vectors: new NullVectorStore(), + Llm: new NullLlmService(), + Settings: new Settings(), + DeadEnds: _deadEndStore + ); + } + + [Fact] + public async Task Run_FiresAlertOnFileOverlapMatch() + { + await _deadEndStore.Add(new DeadEnd + { + Id = "de-1", Project = "proj", + Description = "FTS doesn't support CJK", + Approach = "Used default tokenizer", Reason = "No CJK support", + FilesInvolved = ["src/Search.cs", "src/Index.cs"], + DetectedAt = DateTime.UtcNow.AddDays(-1) + }); + + await _obsStore.Add(new Observation + { + Id = "obs-1", SessionId = "s1", ThreadId = "t1", + Timestamp = DateTime.UtcNow, Project = "proj", + EventType = EventType.FileChange, Source = CaptureSource.ClaudeCode, + RawContent = "Editing search", FilesInvolved = ["src/Search.cs"] + }); + + var ctx = CreateContext(); + var results = await _agent.Run(ctx, CancellationToken.None); + + Assert.Single(results); + Assert.Equal(AgentOutputType.AlertFired, results[0].Type); + + var active = await _alertStore.GetActive(); + Assert.Single(active); + Assert.Equal("de-1", active[0].MatchedDeadEndId); + Assert.Equal(MatchStrategy.FileOverlap, active[0].Strategy); + Assert.True(active[0].Confidence >= 0.5); + } + + [Fact] + public async Task Run_DoesNotFireWhenOverlapBelowThreshold() + { + await _deadEndStore.Add(new DeadEnd + { + Id = "de-1", Project = "proj", + Description = "Complex issue", Approach = "approach", Reason = "reason", + FilesInvolved = ["src/A.cs", "src/B.cs", "src/C.cs"], + DetectedAt = DateTime.UtcNow.AddDays(-1) + }); + + await _obsStore.Add(new Observation + { + Id = "obs-1", SessionId = "s1", ThreadId = "t1", + Timestamp = DateTime.UtcNow, Project = "proj", + EventType = EventType.FileChange, Source = CaptureSource.ClaudeCode, + RawContent = "Editing A", FilesInvolved = ["src/A.cs"] + }); + + var ctx = CreateContext(); + var results = await _agent.Run(ctx, CancellationToken.None); + + Assert.Empty(results); + Assert.Empty(await _alertStore.GetActive()); + } + + [Fact] + public async Task Run_DeduplicatesAlerts() + { + await _deadEndStore.Add(new DeadEnd + { + Id = "de-1", Project = "proj", + Description = "Known issue", Approach = "approach", Reason = "reason", + FilesInvolved = ["src/A.cs"], + DetectedAt = DateTime.UtcNow.AddDays(-1) + }); + + await _obsStore.Add(new Observation + { + Id = "obs-1", SessionId = "s1", ThreadId = "t1", + Timestamp = DateTime.UtcNow.AddMinutes(-5), Project = "proj", + EventType = EventType.FileChange, Source = CaptureSource.ClaudeCode, + RawContent = "First edit", FilesInvolved = ["src/A.cs"] + }); + + var ctx = CreateContext(); + await _agent.Run(ctx, CancellationToken.None); + + await _obsStore.Add(new Observation + { + Id = "obs-2", SessionId = "s1", ThreadId = "t1", + Timestamp = DateTime.UtcNow, Project = "proj", + EventType = EventType.FileChange, Source = CaptureSource.ClaudeCode, + RawContent = "Second edit", FilesInvolved = ["src/A.cs"] + }); + + var results2 = await _agent.Run(ctx, CancellationToken.None); + + Assert.Empty(results2); + Assert.Single(await _alertStore.GetActive()); + } + + [Fact] + public async Task Run_PushesToAlertSinkWhenProvided() + { + var sink = new CapturingAlertSink(); + var agentWithSink = new DejaVuAgent(_alertStore, sink); + + await _deadEndStore.Add(new DeadEnd + { + Id = "de-1", Project = "proj", + Description = "Known issue", Approach = "approach", Reason = "reason", + FilesInvolved = ["src/A.cs"], + DetectedAt = DateTime.UtcNow.AddDays(-1) + }); + + await _obsStore.Add(new Observation + { + Id = "obs-1", SessionId = "s1", ThreadId = "t1", + Timestamp = DateTime.UtcNow, Project = "proj", + EventType = EventType.FileChange, Source = CaptureSource.ClaudeCode, + RawContent = "Editing A", FilesInvolved = ["src/A.cs"] + }); + + var ctx = CreateContext(); + await agentWithSink.Run(ctx, CancellationToken.None); + + Assert.Single(sink.SentAlerts); + Assert.Equal("de-1", sink.SentAlerts[0].MatchedDeadEndId); + } + + [Fact] + public async Task Run_SkipsDeadEndsWithEmptyFiles() + { + await _deadEndStore.Add(new DeadEnd + { + Id = "de-empty", Project = "proj", + Description = "Empty files", Approach = "approach", Reason = "reason", + FilesInvolved = [], + DetectedAt = DateTime.UtcNow.AddDays(-1) + }); + + await _obsStore.Add(new Observation + { + Id = "obs-1", SessionId = "s1", ThreadId = "t1", + Timestamp = DateTime.UtcNow, Project = "proj", + EventType = EventType.FileChange, Source = CaptureSource.ClaudeCode, + RawContent = "Editing", FilesInvolved = ["src/A.cs"] + }); + + var ctx = CreateContext(); + var results = await _agent.Run(ctx, CancellationToken.None); + + Assert.Empty(results); + } + + [Fact] + public async Task Run_SkipsObservationsWithoutThreadId() + { + await _deadEndStore.Add(new DeadEnd + { + Id = "de-1", Project = "proj", + Description = "Issue", Approach = "approach", Reason = "reason", + FilesInvolved = ["src/A.cs"], + DetectedAt = DateTime.UtcNow.AddDays(-1) + }); + + await _obsStore.Add(new Observation + { + Id = "obs-1", SessionId = "s1", ThreadId = null, + Timestamp = DateTime.UtcNow, Project = "proj", + EventType = EventType.FileChange, Source = CaptureSource.ClaudeCode, + RawContent = "No thread", FilesInvolved = ["src/A.cs"] + }); + + var ctx = CreateContext(); + var results = await _agent.Run(ctx, CancellationToken.None); + + Assert.Empty(results); + } + + private class CapturingAlertSink : IAlertSink + { + public List SentAlerts { get; } = []; + + public Task Send(DejaVuAlert alert, CancellationToken ct = default) + { + SentAlerts.Add(alert); + return Task.CompletedTask; + } + } +} diff --git a/tests/DevBrain.Agents.Tests/GrowthAgentTests.cs b/tests/DevBrain.Agents.Tests/GrowthAgentTests.cs new file mode 100644 index 0000000..7f59293 --- /dev/null +++ b/tests/DevBrain.Agents.Tests/GrowthAgentTests.cs @@ -0,0 +1,285 @@ +using DevBrain.Agents; +using DevBrain.Core.Enums; +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; +using DevBrain.Storage; +using DevBrain.Storage.Schema; +using Microsoft.Data.Sqlite; + +namespace DevBrain.Agents.Tests; + +public class GrowthAgentTests : IAsyncLifetime +{ + private SqliteConnection _connection = null!; + private SqliteObservationStore _obsStore = null!; + private SqliteGraphStore _graphStore = null!; + private SqliteDeadEndStore _deadEndStore = null!; + private SqliteGrowthStore _growthStore = null!; + private GrowthAgent _agent = null!; + + public async Task InitializeAsync() + { + _connection = new SqliteConnection("Data Source=:memory:"); + await _connection.OpenAsync(); + SchemaManager.Initialize(_connection); + _obsStore = new SqliteObservationStore(_connection); + _graphStore = new SqliteGraphStore(_connection); + _deadEndStore = new SqliteDeadEndStore(_connection); + _growthStore = new SqliteGrowthStore(_connection); + _agent = new GrowthAgent(_growthStore); + } + + public async Task DisposeAsync() + { + await _connection.DisposeAsync(); + } + + private AgentContext CreateContext() + { + return new AgentContext( + Observations: _obsStore, + Graph: _graphStore, + Vectors: new NullVectorStore(), + Llm: new NullLlmService(), + Settings: new Settings(), + DeadEnds: _deadEndStore + ); + } + + [Fact] + public async Task Run_GeneratesMetricsAndReport() + { + var now = DateTime.UtcNow; + for (int i = 0; i < 10; i++) + { + await _obsStore.Add(new Observation + { + Id = $"obs-{i}", SessionId = "s1", ThreadId = "t1", + Timestamp = now.AddMinutes(-60 + i * 5), Project = "proj", + EventType = i % 3 == 0 ? EventType.Error : EventType.FileChange, + Source = CaptureSource.ClaudeCode, + RawContent = $"Activity {i}", + FilesInvolved = [$"src/File{i % 3}.cs"] + }); + } + + var ctx = CreateContext(); + var results = await _agent.Run(ctx, CancellationToken.None); + + Assert.Contains(results, r => r.Type == AgentOutputType.GrowthReportGenerated); + + var report = await _growthStore.GetLatestReport(); + Assert.NotNull(report); + + var metrics = await _growthStore.GetLatestMetrics(); + Assert.True(metrics.Count >= 6); + } + + [Fact] + public async Task Run_SkipsWhenNoObservations() + { + var ctx = CreateContext(); + var results = await _agent.Run(ctx, CancellationToken.None); + + Assert.Empty(results); + Assert.Null(await _growthStore.GetLatestReport()); + } + + [Fact] + public void ComputeDebuggingSpeed_MeasuresErrorToResolution() + { + var now = DateTime.UtcNow; + var obs = new List + { + MakeObs("1", "t1", now, EventType.Error), + MakeObs("2", "t1", now.AddMinutes(10), EventType.Error), + MakeObs("3", "t1", now.AddMinutes(15), EventType.FileChange), + }; + + var speed = GrowthAgent.ComputeDebuggingSpeed(obs); + // First error at t=0, resolution (FileChange after last error) at t=15 + Assert.Equal(15.0, speed); + } + + [Fact] + public void ComputeDecisionVelocity_MeasuresFileChangeToDecision() + { + var now = DateTime.UtcNow; + var obs = new List + { + MakeObs("1", "t1", now, EventType.FileChange), + MakeObs("2", "t1", now.AddMinutes(20), EventType.Decision), + }; + + var velocity = GrowthAgent.ComputeDecisionVelocity(obs); + Assert.Equal(20.0, velocity); + } + + [Fact] + public void ComputeRetryRate_DetectsRepeatedEdits() + { + var now = DateTime.UtcNow; + var obs = new List + { + MakeObs("1", "t1", now, EventType.FileChange, files: ["src/A.cs"]), + MakeObs("2", "t1", now.AddMinutes(1), EventType.FileChange, files: ["src/A.cs"]), + MakeObs("3", "t1", now.AddMinutes(2), EventType.FileChange, files: ["src/A.cs"]), + }; + + var rate = GrowthAgent.ComputeRetryRate(obs); + Assert.Equal(1.0, rate); // 100% of sessions have retries + } + + [Fact] + public void ComputeHeuristicComplexity_ReturnsNormalizedScore() + { + var now = DateTime.UtcNow; + var obs = new List + { + MakeObs("1", "t1", now, EventType.FileChange, files: ["a.cs", "b.cs", "c.cs"]), + MakeObs("2", "t1", now.AddHours(2), EventType.Decision), + MakeObs("3", "t1", now.AddHours(3), EventType.FileChange, files: ["d.cs"]), + }; + + var complexity = GrowthAgent.ComputeHeuristicComplexity(obs); + Assert.InRange(complexity, 1.0, 5.0); + } + + [Fact] + public async Task Run_DetectsFirstProjectMilestone() + { + var now = DateTime.UtcNow; + await _obsStore.Add(new Observation + { + Id = "obs-new", SessionId = "s1", ThreadId = "t1", + Timestamp = now.AddMinutes(-10), Project = "brand-new-project", + EventType = EventType.FileChange, Source = CaptureSource.ClaudeCode, + RawContent = "First work on new project", + FilesInvolved = ["src/App.cs"] + }); + + var ctx = CreateContext(); + var results = await _agent.Run(ctx, CancellationToken.None); + + Assert.Contains(results, r => + r.Type == AgentOutputType.MilestoneAchieved && + r.Content.Contains("brand-new-project")); + + var milestones = await _growthStore.GetMilestones(); + Assert.Contains(milestones, m => + m.Type == MilestoneType.First && + m.Description.Contains("brand-new-project")); + } + + [Fact] + public void ComputeDebuggingSpeed_MeasuresFirstErrorToResolution() + { + var now = DateTime.UtcNow; + var obs = new List + { + MakeObs("1", "t1", now, EventType.Error), + MakeObs("2", "t1", now.AddMinutes(10), EventType.Error), + MakeObs("3", "t1", now.AddMinutes(30), EventType.FileChange), // resolution + }; + + var speed = GrowthAgent.ComputeDebuggingSpeed(obs); + // Should measure from first error (t=0) to resolution (t=30), not error span (t=10) + Assert.Equal(30.0, speed); + } + + [Fact] + public void ComputeDebuggingSpeed_ZeroErrors_ReturnsZero() + { + var obs = new List + { + MakeObs("1", "t1", DateTime.UtcNow, EventType.FileChange), + }; + + Assert.Equal(0, GrowthAgent.ComputeDebuggingSpeed(obs)); + } + + [Fact] + public void ComputeHeuristicComplexity_VariesAcrossThreadSizes() + { + var now = DateTime.UtcNow; + var simpleObs = new List + { + MakeObs("1", "t1", now, EventType.FileChange, files: ["a.cs"]), + }; + var complexObs = new List + { + MakeObs("1", "t1", now, EventType.FileChange, files: ["a.cs", "b.cs", "c.cs", "d.cs", "e.cs"]), + MakeObs("2", "t1", now.AddHours(2), EventType.Decision), + MakeObs("3", "t1", now.AddHours(3), EventType.Decision), + MakeObs("4", "t1", now.AddHours(4), EventType.FileChange, files: ["f.cs", "g.cs"]), + }; + + var simple = GrowthAgent.ComputeHeuristicComplexity(simpleObs); + var complex = GrowthAgent.ComputeHeuristicComplexity(complexObs); + + Assert.True(complex > simple, + $"Complex ({complex}) should be > Simple ({simple})"); + Assert.InRange(simple, 1.0, 5.0); + Assert.InRange(complex, 1.0, 5.0); + } + + [Fact] + public async Task Run_ReportRoundTrips_WithHydratedMetrics() + { + var now = DateTime.UtcNow; + for (int i = 0; i < 5; i++) + { + await _obsStore.Add(new Observation + { + Id = $"rt-{i}", SessionId = "s1", ThreadId = "t1", + Timestamp = now.AddMinutes(-30 + i * 5), Project = "proj", + EventType = EventType.FileChange, Source = CaptureSource.ClaudeCode, + RawContent = $"Activity {i}", FilesInvolved = [$"src/F{i}.cs"] + }); + } + + var ctx = CreateContext(); + await _agent.Run(ctx, CancellationToken.None); + + var report = await _growthStore.GetLatestReport(); + Assert.NotNull(report); + Assert.Equal(8, report.Metrics.Count); // all 8 dimensions + Assert.All(report.Metrics, m => Assert.NotEmpty(m.Dimension)); + } + + [Fact] + public void ComputeRetryRate_MultipleSessions() + { + var now = DateTime.UtcNow; + var obs = new List + { + // Session 1: has retries (3+ edits to same file) + new() { Id = "1", SessionId = "s1", ThreadId = "t1", Timestamp = now, + Project = "proj", EventType = EventType.FileChange, Source = CaptureSource.ClaudeCode, + RawContent = "edit", FilesInvolved = ["a.cs"] }, + new() { Id = "2", SessionId = "s1", ThreadId = "t1", Timestamp = now.AddMinutes(1), + Project = "proj", EventType = EventType.FileChange, Source = CaptureSource.ClaudeCode, + RawContent = "edit", FilesInvolved = ["a.cs"] }, + new() { Id = "3", SessionId = "s1", ThreadId = "t1", Timestamp = now.AddMinutes(2), + Project = "proj", EventType = EventType.FileChange, Source = CaptureSource.ClaudeCode, + RawContent = "edit", FilesInvolved = ["a.cs"] }, + // Session 2: no retries + new() { Id = "4", SessionId = "s2", ThreadId = "t2", Timestamp = now, + Project = "proj", EventType = EventType.FileChange, Source = CaptureSource.ClaudeCode, + RawContent = "edit", FilesInvolved = ["b.cs"] }, + }; + + var rate = GrowthAgent.ComputeRetryRate(obs); + Assert.Equal(0.5, rate); // 1 of 2 sessions has retries + } + + private static Observation MakeObs(string id, string threadId, DateTime timestamp, + EventType type, string[]? files = null) => new() + { + Id = id, SessionId = "s1", ThreadId = threadId, + Timestamp = timestamp, Project = "proj", + EventType = type, Source = CaptureSource.ClaudeCode, + RawContent = $"Content for {id}", + FilesInvolved = files ?? [] + }; +} diff --git a/tests/DevBrain.Agents.Tests/LinkerAgentTests.cs b/tests/DevBrain.Agents.Tests/LinkerAgentTests.cs index f3fb4bf..2f0156c 100644 --- a/tests/DevBrain.Agents.Tests/LinkerAgentTests.cs +++ b/tests/DevBrain.Agents.Tests/LinkerAgentTests.cs @@ -37,7 +37,8 @@ private AgentContext CreateContext() Graph: _graphStore, Vectors: new NullVectorStore(), Llm: new NullLlmService(), - Settings: new Settings() + Settings: new Settings(), + DeadEnds: new NullDeadEndStore() ); } @@ -64,13 +65,11 @@ public async Task Run_CreatesFileNodesAndEdgesForObservationsWithFiles() Assert.Equal(2, results.Count); Assert.All(results, r => Assert.Equal(AgentOutputType.EdgeCreated, r.Type)); - // Verify file nodes were created var fileNodes = await _graphStore.GetNodesByType("File"); Assert.Equal(2, fileNodes.Count); Assert.Contains(fileNodes, n => n.Name == "src/Program.cs"); Assert.Contains(fileNodes, n => n.Name == "src/Startup.cs"); - // Verify observation node was created var decisionNodes = await _graphStore.GetNodesByType("Decision"); Assert.Single(decisionNodes); Assert.Equal("Architecture decision", decisionNodes[0].Name); @@ -80,7 +79,6 @@ public async Task Run_CreatesFileNodesAndEdgesForObservationsWithFiles() [Fact] public async Task Run_DoesNotDuplicateExistingFileNodes() { - // Pre-create a file node await _graphStore.AddNode("File", "src/Program.cs"); var obs1 = new Observation @@ -116,12 +114,10 @@ public async Task Run_DoesNotDuplicateExistingFileNodes() Assert.Equal(2, results.Count); - // Verify only one File node exists (no duplicate) var fileNodes = await _graphStore.GetNodesByType("File"); Assert.Single(fileNodes); Assert.Equal("src/Program.cs", fileNodes[0].Name); - // Verify both observation node types were created var decisionNodes = await _graphStore.GetNodesByType("Decision"); Assert.Single(decisionNodes); @@ -153,26 +149,4 @@ public async Task Run_SkipsObservationsWithoutFiles() var fileNodes = await _graphStore.GetNodesByType("File"); Assert.Empty(fileNodes); } - - private class NullVectorStore : IVectorStore - { - public Task Index(string id, string text, VectorCategory category) => Task.CompletedTask; - public Task> Search(string query, int topK = 20, VectorCategory? filter = null) - => Task.FromResult>(Array.Empty()); - public Task Remove(string id) => Task.CompletedTask; - public Task Rebuild() => Task.CompletedTask; - public Task GetSizeBytes() => Task.FromResult(0L); - } - - private class NullLlmService : ILlmService - { - public bool IsLocalAvailable => false; - public bool IsCloudAvailable => false; - public int CloudRequestsToday => 0; - public int QueueDepth => 0; - public Task Submit(LlmTask task, CancellationToken ct = default) - => Task.FromResult(new LlmResult { TaskId = task.Id, Success = false }); - public Task Embed(string text, CancellationToken ct = default) - => Task.FromResult(Array.Empty()); - } } diff --git a/tests/DevBrain.Agents.Tests/StorytellerAgentTests.cs b/tests/DevBrain.Agents.Tests/StorytellerAgentTests.cs new file mode 100644 index 0000000..905b981 --- /dev/null +++ b/tests/DevBrain.Agents.Tests/StorytellerAgentTests.cs @@ -0,0 +1,266 @@ +using DevBrain.Agents; +using DevBrain.Core.Enums; +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; +using DevBrain.Storage; +using DevBrain.Storage.Schema; +using Microsoft.Data.Sqlite; + +namespace DevBrain.Agents.Tests; + +public class StorytellerAgentTests : IAsyncLifetime +{ + private SqliteConnection _connection = null!; + private SqliteObservationStore _obsStore = null!; + private SqliteGraphStore _graphStore = null!; + private SqliteDeadEndStore _deadEndStore = null!; + private SqliteSessionStore _sessionStore = null!; + private StorytellerAgent _agent = null!; + + public async Task InitializeAsync() + { + _connection = new SqliteConnection("Data Source=:memory:"); + await _connection.OpenAsync(); + SchemaManager.Initialize(_connection); + _obsStore = new SqliteObservationStore(_connection); + _graphStore = new SqliteGraphStore(_connection); + _deadEndStore = new SqliteDeadEndStore(_connection); + _sessionStore = new SqliteSessionStore(_connection); + _agent = new StorytellerAgent(_sessionStore); + } + + public async Task DisposeAsync() + { + await _connection.DisposeAsync(); + } + + private AgentContext CreateContext(ILlmService? llm = null) + { + return new AgentContext( + Observations: _obsStore, + Graph: _graphStore, + Vectors: new NullVectorStore(), + Llm: llm ?? new StoryLlmService(), + Settings: new Settings(), + DeadEnds: _deadEndStore + ); + } + + [Fact] + public async Task Run_GeneratesStoryForSessionWithEnoughObservations() + { + var now = DateTime.UtcNow; + for (int i = 0; i < 5; i++) + { + await _obsStore.Add(new Observation + { + Id = $"obs-{i}", SessionId = "session-1", ThreadId = "t1", + Timestamp = now.AddMinutes(-30 + i * 5), Project = "proj", + EventType = i < 3 ? EventType.FileChange : EventType.Decision, + Source = CaptureSource.ClaudeCode, + RawContent = $"Activity {i}", Summary = $"Step {i}", + FilesInvolved = [$"src/File{i}.cs"] + }); + } + + var ctx = CreateContext(); + var results = await _agent.Run(ctx, CancellationToken.None); + + Assert.Single(results); + Assert.Equal(AgentOutputType.StoryGenerated, results[0].Type); + + var story = await _sessionStore.GetBySessionId("session-1"); + Assert.NotNull(story); + Assert.Equal(5, story.ObservationCount); + Assert.True(story.FilesTouched >= 5); + } + + [Fact] + public async Task Run_SkipsSessionWithTooFewObservations() + { + await _obsStore.Add(new Observation + { + Id = "obs-1", SessionId = "session-short", ThreadId = "t1", + Timestamp = DateTime.UtcNow, Project = "proj", + EventType = EventType.Conversation, Source = CaptureSource.ClaudeCode, + RawContent = "Just one observation" + }); + + var ctx = CreateContext(); + var results = await _agent.Run(ctx, CancellationToken.None); + + Assert.Empty(results); + Assert.Null(await _sessionStore.GetBySessionId("session-short")); + } + + [Fact] + public async Task Run_SkipsAlreadyGeneratedSession() + { + var now = DateTime.UtcNow; + for (int i = 0; i < 4; i++) + { + await _obsStore.Add(new Observation + { + Id = $"obs-{i}", SessionId = "session-done", ThreadId = "t1", + Timestamp = now.AddMinutes(-20 + i * 5), Project = "proj", + EventType = EventType.FileChange, Source = CaptureSource.ClaudeCode, + RawContent = $"Activity {i}" + }); + } + + // Pre-create a story + await _sessionStore.Add(new SessionSummary + { + Id = "ss-existing", SessionId = "session-done", + Narrative = "Already generated", Outcome = "Done", + Duration = TimeSpan.FromMinutes(15), ObservationCount = 4, + FilesTouched = 0, DeadEndsHit = 0, Phases = [] + }); + + var ctx = CreateContext(); + var results = await _agent.Run(ctx, CancellationToken.None); + + Assert.Empty(results); + } + + [Fact] + public void DetectPhases_IdentifiesPhaseTransitions() + { + var now = DateTime.UtcNow; + var observations = new List + { + MakeObs("1", now, EventType.Conversation), + MakeObs("2", now.AddMinutes(5), EventType.Conversation), + MakeObs("3", now.AddMinutes(15), EventType.FileChange), + MakeObs("4", now.AddMinutes(20), EventType.FileChange), + MakeObs("5", now.AddMinutes(25), EventType.Error), + MakeObs("6", now.AddMinutes(30), EventType.FileChange), + }; + + var phases = StorytellerAgent.DetectPhases(observations); + + Assert.Contains("Exploration", phases); + Assert.True(phases.Count >= 2); + } + + [Fact] + public void DetectPhases_SingleWindowSession() + { + var now = DateTime.UtcNow; + var observations = new List + { + MakeObs("1", now, EventType.FileChange), + MakeObs("2", now.AddMinutes(2), EventType.FileChange), + MakeObs("3", now.AddMinutes(5), EventType.Error), + }; + + var phases = StorytellerAgent.DetectPhases(observations); + + Assert.NotEmpty(phases); + } + + [Fact] + public void DetectPhases_SameTimestamp_ReturnsOnePhase() + { + var now = DateTime.UtcNow; + var observations = new List + { + MakeObs("1", now, EventType.Conversation), + MakeObs("2", now, EventType.Conversation), + MakeObs("3", now, EventType.Conversation), + }; + + var phases = StorytellerAgent.DetectPhases(observations); + + Assert.Single(phases); + Assert.Equal("Exploration", phases[0]); + } + + [Fact] + public void DetectPhases_EmptyObservations_ReturnsEmpty() + { + var phases = StorytellerAgent.DetectPhases([]); + Assert.Empty(phases); + } + + [Fact] + public async Task Run_SkipsWhenLlmFails() + { + var now = DateTime.UtcNow; + for (int i = 0; i < 4; i++) + { + await _obsStore.Add(new Observation + { + Id = $"obs-{i}", SessionId = "session-fail", ThreadId = "t1", + Timestamp = now.AddMinutes(-20 + i * 5), Project = "proj", + EventType = EventType.FileChange, Source = CaptureSource.ClaudeCode, + RawContent = $"Activity {i}" + }); + } + + var ctx = CreateContext(new FailingLlmService()); + var results = await _agent.Run(ctx, CancellationToken.None); + + Assert.Empty(results); + Assert.Null(await _sessionStore.GetBySessionId("session-fail")); + } + + [Fact] + public void DetectTurningPoints_FindsDecisionsAndResolutions() + { + var now = DateTime.UtcNow; + var observations = new List + { + MakeObs("1", now, EventType.Error), + MakeObs("2", now.AddMinutes(15), EventType.Decision, + summary: "Decided to use mutex"), + MakeObs("3", now.AddMinutes(20), EventType.FileChange), + }; + + var points = StorytellerAgent.DetectTurningPoints(observations); + + Assert.Contains(points, p => p.Contains("Decision")); + Assert.Contains(points, p => p.Contains("no recurrence")); + } + + private static Observation MakeObs(string id, DateTime timestamp, EventType type, + string? summary = null) => new() + { + Id = id, SessionId = "s1", ThreadId = "t1", + Timestamp = timestamp, Project = "proj", + EventType = type, Source = CaptureSource.ClaudeCode, + RawContent = $"Content for {id}", + Summary = summary + }; + + private class StoryLlmService : ILlmService + { + public bool IsLocalAvailable => true; + public bool IsCloudAvailable => true; + public int CloudRequestsToday => 0; + public int QueueDepth => 0; + public Task Submit(LlmTask task, CancellationToken ct = default) + => Task.FromResult(new LlmResult + { + TaskId = task.Id, + Success = true, + Content = "The developer started by exploring the codebase.\n" + + "They implemented several changes across multiple files.\n" + + "Session outcome: Successfully completed the task." + }); + public Task Embed(string text, CancellationToken ct = default) + => Task.FromResult(Array.Empty()); + } + + private class FailingLlmService : ILlmService + { + public bool IsLocalAvailable => false; + public bool IsCloudAvailable => false; + public int CloudRequestsToday => 0; + public int QueueDepth => 0; + public Task Submit(LlmTask task, CancellationToken ct = default) + => Task.FromResult(new LlmResult { TaskId = task.Id, Success = false, Content = null }); + public Task Embed(string text, CancellationToken ct = default) + => Task.FromResult(Array.Empty()); + } +} diff --git a/tests/DevBrain.Agents.Tests/TestHelpers.cs b/tests/DevBrain.Agents.Tests/TestHelpers.cs new file mode 100644 index 0000000..5673185 --- /dev/null +++ b/tests/DevBrain.Agents.Tests/TestHelpers.cs @@ -0,0 +1,38 @@ +namespace DevBrain.Agents.Tests; + +using DevBrain.Core.Enums; +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; + +public class NullVectorStore : IVectorStore +{ + public Task Index(string id, string text, VectorCategory category) => Task.CompletedTask; + public Task> Search(string query, int topK = 20, VectorCategory? filter = null) + => Task.FromResult>(Array.Empty()); + public Task Remove(string id) => Task.CompletedTask; + public Task Rebuild() => Task.CompletedTask; + public Task GetSizeBytes() => Task.FromResult(0L); +} + +public class NullLlmService : ILlmService +{ + public bool IsLocalAvailable => false; + public bool IsCloudAvailable => false; + public int CloudRequestsToday => 0; + public int QueueDepth => 0; + public Task Submit(LlmTask task, CancellationToken ct = default) + => Task.FromResult(new LlmResult { TaskId = task.Id, Success = false }); + public Task Embed(string text, CancellationToken ct = default) + => Task.FromResult(Array.Empty()); +} + +public class NullDeadEndStore : IDeadEndStore +{ + public Task Add(DeadEnd deadEnd) => Task.FromResult(deadEnd); + public Task> Query(DeadEndFilter filter) + => Task.FromResult>(Array.Empty()); + public Task> FindByFiles(IReadOnlyList filePaths) + => Task.FromResult>(Array.Empty()); + public Task> FindSimilar(string description, int limit = 5) + => Task.FromResult>(Array.Empty()); +} diff --git a/tests/DevBrain.Storage.Tests/BlastRadiusCalculatorTests.cs b/tests/DevBrain.Storage.Tests/BlastRadiusCalculatorTests.cs new file mode 100644 index 0000000..988c193 --- /dev/null +++ b/tests/DevBrain.Storage.Tests/BlastRadiusCalculatorTests.cs @@ -0,0 +1,180 @@ +using DevBrain.Core.Enums; +using DevBrain.Core.Models; +using DevBrain.Storage; +using DevBrain.Storage.Schema; +using Microsoft.Data.Sqlite; + +namespace DevBrain.Storage.Tests; + +public class BlastRadiusCalculatorTests : IAsyncLifetime +{ + private SqliteConnection _connection = null!; + private SqliteObservationStore _obsStore = null!; + private SqliteGraphStore _graphStore = null!; + private SqliteDeadEndStore _deadEndStore = null!; + private BlastRadiusCalculator _calculator = null!; + + public async Task InitializeAsync() + { + _connection = new SqliteConnection("Data Source=:memory:"); + await _connection.OpenAsync(); + SchemaManager.Initialize(_connection); + _obsStore = new SqliteObservationStore(_connection); + _graphStore = new SqliteGraphStore(_connection); + _deadEndStore = new SqliteDeadEndStore(_connection); + var chainBuilder = new DecisionChainBuilder(_graphStore, _obsStore); + _calculator = new BlastRadiusCalculator(_graphStore, _deadEndStore, chainBuilder); + } + + public async Task DisposeAsync() + { + await _connection.DisposeAsync(); + } + + [Fact] + public async Task Calculate_FindsAffectedFiles() + { + // Source file -> decision -> references another file + var sourceFile = await _graphStore.AddNode("File", "src/Auth.cs"); + var decision = await _graphStore.AddNode("Decision", "Add JWT auth"); + var affectedFile = await _graphStore.AddNode("File", "src/Config.cs"); + + await _graphStore.AddEdge(decision.Id, sourceFile.Id, "references"); + await _graphStore.AddEdge(decision.Id, affectedFile.Id, "references"); + + var result = await _calculator.Calculate("src/Auth.cs"); + + Assert.Equal("src/Auth.cs", result.SourceFile); + Assert.Single(result.AffectedFiles); + Assert.Equal("src/Config.cs", result.AffectedFiles[0].FilePath); + Assert.True(result.AffectedFiles[0].RiskScore > 0); + } + + [Fact] + public async Task Calculate_ExcludesSourceFileFromAffected() + { + var sourceFile = await _graphStore.AddNode("File", "src/App.cs"); + var decision = await _graphStore.AddNode("Decision", "Refactor"); + + await _graphStore.AddEdge(decision.Id, sourceFile.Id, "references"); + + var result = await _calculator.Calculate("src/App.cs"); + + Assert.Empty(result.AffectedFiles); + } + + [Fact] + public async Task Calculate_IncludesDeadEndsAtRisk() + { + var sourceFile = await _graphStore.AddNode("File", "src/Search.cs"); + var decision = await _graphStore.AddNode("Decision", "Use FTS"); + + await _graphStore.AddEdge(decision.Id, sourceFile.Id, "references"); + + await _deadEndStore.Add(new DeadEnd + { + Id = "de-1", Project = "proj", + Description = "FTS tokenizer issue", + Approach = "Default tokenizer", Reason = "No CJK support", + FilesInvolved = ["src/Search.cs"], + DetectedAt = DateTime.UtcNow.AddDays(-5) + }); + + var result = await _calculator.Calculate("src/Search.cs"); + + Assert.Single(result.DeadEndsAtRisk); + Assert.Equal("de-1", result.DeadEndsAtRisk[0]); + } + + [Fact] + public async Task Calculate_ReturnsEmptyForUnknownFile() + { + var result = await _calculator.Calculate("src/Unknown.cs"); + + Assert.Equal("src/Unknown.cs", result.SourceFile); + Assert.Empty(result.AffectedFiles); + Assert.Empty(result.DeadEndsAtRisk); + } + + [Fact] + public async Task Calculate_FollowsCausalChainToFindDistantFiles() + { + // src/A.cs -> dec1 --caused_by--> dec2 -> src/B.cs + var fileA = await _graphStore.AddNode("File", "src/A.cs"); + var fileB = await _graphStore.AddNode("File", "src/B.cs"); + var dec1 = await _graphStore.AddNode("Decision", "Decision about A"); + var dec2 = await _graphStore.AddNode("Decision", "Decision about B"); + + await _graphStore.AddEdge(dec1.Id, fileA.Id, "references"); + await _graphStore.AddEdge(dec2.Id, fileB.Id, "references"); + await _graphStore.AddEdge(dec2.Id, dec1.Id, "caused_by"); + + var result = await _calculator.Calculate("src/A.cs"); + + Assert.Single(result.AffectedFiles); + Assert.Equal("src/B.cs", result.AffectedFiles[0].FilePath); + } + + [Fact] + public async Task Calculate_CloserFilesGetHigherRiskThanDistant() + { + // src/Root.cs -> dec1 --caused_by--> dec2 --caused_by--> dec3 + // dec1 references src/Close.cs (1 hop), dec3 references src/Far.cs (2 hops) + var rootFile = await _graphStore.AddNode("File", "src/Root.cs"); + var closeFile = await _graphStore.AddNode("File", "src/Close.cs"); + var farFile = await _graphStore.AddNode("File", "src/Far.cs"); + var dec1 = await _graphStore.AddNode("Decision", "Root decision"); + var dec2 = await _graphStore.AddNode("Decision", "Middle decision"); + var dec3 = await _graphStore.AddNode("Decision", "Far decision"); + + await _graphStore.AddEdge(dec1.Id, rootFile.Id, "references"); + await _graphStore.AddEdge(dec1.Id, closeFile.Id, "references"); + await _graphStore.AddEdge(dec3.Id, farFile.Id, "references"); + await _graphStore.AddEdge(dec2.Id, dec1.Id, "caused_by"); + await _graphStore.AddEdge(dec3.Id, dec2.Id, "caused_by"); + + var result = await _calculator.Calculate("src/Root.cs"); + + Assert.Equal(2, result.AffectedFiles.Count); + + var closeEntry = result.AffectedFiles.First(f => f.FilePath == "src/Close.cs"); + var farEntry = result.AffectedFiles.First(f => f.FilePath == "src/Far.cs"); + Assert.True(closeEntry.RiskScore >= farEntry.RiskScore, + $"Close ({closeEntry.RiskScore}) should be >= Far ({farEntry.RiskScore})"); + } + + [Fact] + public void ComputeRiskScore_ClampedToOneMax() + { + // With deadEnds and short chain, raw score would exceed 1.0 + var score = BlastRadiusCalculator.ComputeRiskScore(1, 5, 1.0); + Assert.True(score <= 1.0, $"Score {score} should be <= 1.0"); + } + + [Fact] + public void ComputeRiskScore_ShortChainHigherRisk() + { + var shortChain = BlastRadiusCalculator.ComputeRiskScore(1, 0, 1.0); + var longChain = BlastRadiusCalculator.ComputeRiskScore(5, 0, 1.0); + + Assert.True(shortChain > longChain); + } + + [Fact] + public void ComputeRiskScore_DeadEndsAmplifyRisk() + { + var noDeadEnds = BlastRadiusCalculator.ComputeRiskScore(2, 0, 1.0); + var withDeadEnds = BlastRadiusCalculator.ComputeRiskScore(2, 3, 1.0); + + Assert.True(withDeadEnds > noDeadEnds); + } + + [Fact] + public void ComputeRiskScore_RecencyDecayReducesRisk() + { + var recent = BlastRadiusCalculator.ComputeRiskScore(2, 0, 1.0); + var stale = BlastRadiusCalculator.ComputeRiskScore(2, 0, 0.2); + + Assert.True(recent > stale); + } +} diff --git a/tests/DevBrain.Storage.Tests/DecisionChainBuilderTests.cs b/tests/DevBrain.Storage.Tests/DecisionChainBuilderTests.cs new file mode 100644 index 0000000..d464e63 --- /dev/null +++ b/tests/DevBrain.Storage.Tests/DecisionChainBuilderTests.cs @@ -0,0 +1,220 @@ +using DevBrain.Core.Enums; +using DevBrain.Core.Models; +using DevBrain.Storage; +using DevBrain.Storage.Schema; +using Microsoft.Data.Sqlite; + +namespace DevBrain.Storage.Tests; + +public class DecisionChainBuilderTests : IAsyncLifetime +{ + private SqliteConnection _connection = null!; + private SqliteObservationStore _obsStore = null!; + private SqliteGraphStore _graphStore = null!; + private DecisionChainBuilder _builder = null!; + + public async Task InitializeAsync() + { + _connection = new SqliteConnection("Data Source=:memory:"); + await _connection.OpenAsync(); + SchemaManager.Initialize(_connection); + _obsStore = new SqliteObservationStore(_connection); + _graphStore = new SqliteGraphStore(_connection); + _builder = new DecisionChainBuilder(_graphStore, _obsStore); + } + + public async Task DisposeAsync() + { + await _connection.DisposeAsync(); + } + + [Fact] + public async Task BuildForFile_ReturnsChainWithDecisions() + { + // Create file node + two decision nodes linked by causal edge + var fileNode = await _graphStore.AddNode("File", "src/Storage.cs"); + var dec1 = await _graphStore.AddNode("Decision", "Use SQLite", sourceId: "obs-1"); + var dec2 = await _graphStore.AddNode("Decision", "Add WAL mode", sourceId: "obs-2"); + + await _graphStore.AddEdge(dec1.Id, fileNode.Id, "references"); + await _graphStore.AddEdge(dec2.Id, fileNode.Id, "references"); + await _graphStore.AddEdge(dec2.Id, dec1.Id, "caused_by"); + + // Create backing observations + await _obsStore.Add(new Observation + { + Id = "obs-1", SessionId = "s1", Timestamp = DateTime.UtcNow.AddHours(-2), + Project = "proj", EventType = EventType.Decision, Source = CaptureSource.ClaudeCode, + RawContent = "Decided to use SQLite", FilesInvolved = ["src/Storage.cs"] + }); + await _obsStore.Add(new Observation + { + Id = "obs-2", SessionId = "s1", Timestamp = DateTime.UtcNow.AddHours(-1), + Project = "proj", EventType = EventType.Decision, Source = CaptureSource.ClaudeCode, + RawContent = "Added WAL mode", FilesInvolved = ["src/Storage.cs"] + }); + + var chain = await _builder.BuildForFile("src/Storage.cs"); + + Assert.NotNull(chain); + Assert.Equal(2, chain.Steps.Count); + Assert.Equal("Use SQLite", chain.Steps[0].Summary); // chronological order + Assert.Equal("Add WAL mode", chain.Steps[1].Summary); + Assert.All(chain.Steps, s => Assert.Equal(DecisionStepType.Decision, s.StepType)); + } + + [Fact] + public async Task BuildForFile_IncludesDeadEndNodes() + { + var fileNode = await _graphStore.AddNode("File", "src/Search.cs"); + var dec = await _graphStore.AddNode("Decision", "Use FTS5", sourceId: "obs-1"); + var bug = await _graphStore.AddNode("Bug", "FTS tokenizer issue", sourceId: "de-1"); + + await _graphStore.AddEdge(dec.Id, fileNode.Id, "references"); + await _graphStore.AddEdge(bug.Id, fileNode.Id, "references"); + await _graphStore.AddEdge(dec.Id, bug.Id, "resolved_by"); + + await _obsStore.Add(new Observation + { + Id = "obs-1", SessionId = "s1", Timestamp = DateTime.UtcNow, + Project = "proj", EventType = EventType.Decision, Source = CaptureSource.ClaudeCode, + RawContent = "Use FTS5", FilesInvolved = ["src/Search.cs"] + }); + + var chain = await _builder.BuildForFile("src/Search.cs"); + + Assert.NotNull(chain); + Assert.Contains(chain.Steps, s => s.StepType == DecisionStepType.DeadEnd); + Assert.Contains(chain.Steps, s => s.StepType == DecisionStepType.Decision); + } + + [Fact] + public async Task BuildForFile_ReturnsNullWhenNoDecisions() + { + var fileNode = await _graphStore.AddNode("File", "src/Empty.cs"); + + var chain = await _builder.BuildForFile("src/Empty.cs"); + + Assert.Null(chain); + } + + [Fact] + public async Task BuildForDecision_TraversesCausalChain() + { + var dec1 = await _graphStore.AddNode("Decision", "Choose SQLite", sourceId: "obs-1"); + var dec2 = await _graphStore.AddNode("Decision", "Add WAL", sourceId: "obs-2"); + var dec3 = await _graphStore.AddNode("Decision", "Add connection pooling", sourceId: "obs-3"); + + await _graphStore.AddEdge(dec2.Id, dec1.Id, "caused_by"); + await _graphStore.AddEdge(dec3.Id, dec2.Id, "caused_by"); + + await _obsStore.Add(new Observation + { + Id = "obs-1", SessionId = "s1", Timestamp = DateTime.UtcNow.AddHours(-3), + Project = "proj", EventType = EventType.Decision, Source = CaptureSource.ClaudeCode, + RawContent = "Choose SQLite" + }); + await _obsStore.Add(new Observation + { + Id = "obs-2", SessionId = "s1", Timestamp = DateTime.UtcNow.AddHours(-2), + Project = "proj", EventType = EventType.Decision, Source = CaptureSource.ClaudeCode, + RawContent = "Add WAL" + }); + await _obsStore.Add(new Observation + { + Id = "obs-3", SessionId = "s1", Timestamp = DateTime.UtcNow.AddHours(-1), + Project = "proj", EventType = EventType.Decision, Source = CaptureSource.ClaudeCode, + RawContent = "Add connection pooling" + }); + + var chain = await _builder.BuildForDecision(dec3.Id); + + Assert.NotNull(chain); + Assert.Equal(3, chain.Steps.Count); + Assert.Equal("Choose SQLite", chain.Steps[0].Summary); + Assert.Equal("Add connection pooling", chain.Steps[2].Summary); + } + + [Fact] + public async Task BuildForDecision_ReturnsNullForNonexistentNode() + { + var chain = await _builder.BuildForDecision("nonexistent-id"); + Assert.Null(chain); + } + + [Fact] + public async Task BuildForDecision_RejectsNonDecisionNodeType() + { + var fileNode = await _graphStore.AddNode("File", "src/Program.cs"); + + var chain = await _builder.BuildForDecision(fileNode.Id); + + Assert.Null(chain); + } + + [Fact] + public async Task BuildForFile_RootNodeIdIsChronologicallyEarliest() + { + var fileNode = await _graphStore.AddNode("File", "src/App.cs"); + var dec1 = await _graphStore.AddNode("Decision", "Early decision", sourceId: "obs-early"); + var dec2 = await _graphStore.AddNode("Decision", "Late decision", sourceId: "obs-late"); + + await _graphStore.AddEdge(dec1.Id, fileNode.Id, "references"); + await _graphStore.AddEdge(dec2.Id, fileNode.Id, "references"); + + await _obsStore.Add(new Observation + { + Id = "obs-early", SessionId = "s1", Timestamp = DateTime.UtcNow.AddHours(-5), + Project = "proj", EventType = EventType.Decision, Source = CaptureSource.ClaudeCode, + RawContent = "Early", FilesInvolved = ["src/App.cs"] + }); + await _obsStore.Add(new Observation + { + Id = "obs-late", SessionId = "s1", Timestamp = DateTime.UtcNow.AddHours(-1), + Project = "proj", EventType = EventType.Decision, Source = CaptureSource.ClaudeCode, + RawContent = "Late", FilesInvolved = ["src/App.cs"] + }); + + var chain = await _builder.BuildForFile("src/App.cs"); + + Assert.NotNull(chain); + Assert.Equal("obs-early", chain.RootNodeId); + } + + [Fact] + public async Task BuildForDecision_HopsLimitsTraversalDepth() + { + var dec1 = await _graphStore.AddNode("Decision", "Root", sourceId: "obs-1"); + var dec2 = await _graphStore.AddNode("Decision", "Hop 1", sourceId: "obs-2"); + var dec3 = await _graphStore.AddNode("Decision", "Hop 2", sourceId: "obs-3"); + + await _graphStore.AddEdge(dec2.Id, dec1.Id, "caused_by"); + await _graphStore.AddEdge(dec3.Id, dec2.Id, "caused_by"); + + await _obsStore.Add(new Observation + { + Id = "obs-1", SessionId = "s1", Timestamp = DateTime.UtcNow.AddHours(-3), + Project = "proj", EventType = EventType.Decision, Source = CaptureSource.ClaudeCode, + RawContent = "Root" + }); + await _obsStore.Add(new Observation + { + Id = "obs-2", SessionId = "s1", Timestamp = DateTime.UtcNow.AddHours(-2), + Project = "proj", EventType = EventType.Decision, Source = CaptureSource.ClaudeCode, + RawContent = "Hop 1" + }); + await _obsStore.Add(new Observation + { + Id = "obs-3", SessionId = "s1", Timestamp = DateTime.UtcNow.AddHours(-1), + Project = "proj", EventType = EventType.Decision, Source = CaptureSource.ClaudeCode, + RawContent = "Hop 2" + }); + + // With hops=1, should only reach dec1 (direct neighbor) + var chain = await _builder.BuildForDecision(dec3.Id, maxHops: 1); + + Assert.NotNull(chain); + // dec3 + dec2 (1 hop away) but NOT dec1 (2 hops away) + Assert.Equal(2, chain.Steps.Count); + } +} diff --git a/tests/DevBrain.Storage.Tests/SqliteAlertStoreTests.cs b/tests/DevBrain.Storage.Tests/SqliteAlertStoreTests.cs new file mode 100644 index 0000000..d8f8f81 --- /dev/null +++ b/tests/DevBrain.Storage.Tests/SqliteAlertStoreTests.cs @@ -0,0 +1,120 @@ +using DevBrain.Core.Enums; +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; +using DevBrain.Storage; +using DevBrain.Storage.Schema; +using Microsoft.Data.Sqlite; + +namespace DevBrain.Storage.Tests; + +public class SqliteAlertStoreTests : IAsyncLifetime +{ + private SqliteConnection _connection = null!; + private SqliteAlertStore _store = null!; + + public async Task InitializeAsync() + { + _connection = new SqliteConnection("Data Source=:memory:"); + await _connection.OpenAsync(); + SchemaManager.Initialize(_connection); + _store = new SqliteAlertStore(_connection); + } + + public async Task DisposeAsync() + { + await _connection.DisposeAsync(); + } + + [Fact] + public async Task Add_And_GetActive_RoundTrips() + { + var alert = new DejaVuAlert + { + Id = "alert-1", + ThreadId = "t1", + MatchedDeadEndId = "de-1", + Confidence = 0.75, + Message = "You tried this before!", + Strategy = MatchStrategy.FileOverlap + }; + + await _store.Add(alert); + + var active = await _store.GetActive(); + Assert.Single(active); + Assert.Equal("alert-1", active[0].Id); + Assert.Equal(0.75, active[0].Confidence); + Assert.Equal(MatchStrategy.FileOverlap, active[0].Strategy); + Assert.False(active[0].Dismissed); + } + + [Fact] + public async Task Dismiss_RemovesFromActive() + { + var alert = new DejaVuAlert + { + Id = "alert-1", + ThreadId = "t1", + MatchedDeadEndId = "de-1", + Confidence = 0.8, + Message = "Warning", + Strategy = MatchStrategy.FileOverlap + }; + + await _store.Add(alert); + await _store.Dismiss("alert-1"); + + var active = await _store.GetActive(); + Assert.Empty(active); + + var all = await _store.GetAll(); + Assert.Single(all); + Assert.True(all[0].Dismissed); + } + + [Fact] + public async Task Exists_DetectsDuplicates() + { + await _store.Add(new DejaVuAlert + { + Id = "alert-1", ThreadId = "t1", MatchedDeadEndId = "de-1", + Confidence = 0.6, Message = "Dup check", Strategy = MatchStrategy.FileOverlap + }); + + Assert.True(await _store.Exists("t1", "de-1")); + Assert.False(await _store.Exists("t1", "de-999")); + Assert.False(await _store.Exists("t-other", "de-1")); + } + + [Fact] + public async Task Exists_IgnoresDismissedAlerts() + { + await _store.Add(new DejaVuAlert + { + Id = "alert-1", ThreadId = "t1", MatchedDeadEndId = "de-1", + Confidence = 0.7, Message = "Will dismiss", Strategy = MatchStrategy.FileOverlap + }); + + await _store.Dismiss("alert-1"); + Assert.False(await _store.Exists("t1", "de-1")); + } + + [Fact] + public async Task GetAll_ReturnsAllIncludingDismissed() + { + await _store.Add(new DejaVuAlert + { + Id = "a1", ThreadId = "t1", MatchedDeadEndId = "de-1", + Confidence = 0.5, Message = "First", Strategy = MatchStrategy.FileOverlap + }); + await _store.Add(new DejaVuAlert + { + Id = "a2", ThreadId = "t2", MatchedDeadEndId = "de-2", + Confidence = 0.9, Message = "Second", Strategy = MatchStrategy.Semantic + }); + await _store.Dismiss("a1"); + + var all = await _store.GetAll(); + Assert.Equal(2, all.Count); + } +} diff --git a/tests/DevBrain.Storage.Tests/SqliteDeadEndStoreTests.cs b/tests/DevBrain.Storage.Tests/SqliteDeadEndStoreTests.cs new file mode 100644 index 0000000..43c7833 --- /dev/null +++ b/tests/DevBrain.Storage.Tests/SqliteDeadEndStoreTests.cs @@ -0,0 +1,112 @@ +using DevBrain.Core.Interfaces; +using DevBrain.Core.Models; +using DevBrain.Storage; +using DevBrain.Storage.Schema; +using Microsoft.Data.Sqlite; + +namespace DevBrain.Storage.Tests; + +public class SqliteDeadEndStoreTests : IAsyncLifetime +{ + private SqliteConnection _connection = null!; + private SqliteDeadEndStore _store = null!; + + public async Task InitializeAsync() + { + _connection = new SqliteConnection("Data Source=:memory:"); + await _connection.OpenAsync(); + SchemaManager.Initialize(_connection); + _store = new SqliteDeadEndStore(_connection); + } + + public async Task DisposeAsync() + { + await _connection.DisposeAsync(); + } + + [Fact] + public async Task Add_And_Query_RoundTrips() + { + var deadEnd = new DeadEnd + { + Id = "de-1", + Project = "test-project", + Description = "SQLite FTS doesn't support CJK", + Approach = "Tried FTS5 with default tokenizer", + Reason = "Default tokenizer can't segment CJK characters", + FilesInvolved = ["src/Search.cs", "src/Index.cs"], + DetectedAt = DateTime.UtcNow + }; + + await _store.Add(deadEnd); + + var results = await _store.Query(new DeadEndFilter { Project = "test-project" }); + Assert.Single(results); + Assert.Equal("de-1", results[0].Id); + Assert.Equal("SQLite FTS doesn't support CJK", results[0].Description); + Assert.Equal(2, results[0].FilesInvolved.Count); + } + + [Fact] + public async Task FindByFiles_MatchesOverlappingFiles() + { + var de1 = new DeadEnd + { + Id = "de-1", Project = "proj", + Description = "Dead end 1", Approach = "approach", Reason = "reason", + FilesInvolved = ["src/A.cs", "src/B.cs"], DetectedAt = DateTime.UtcNow + }; + var de2 = new DeadEnd + { + Id = "de-2", Project = "proj", + Description = "Dead end 2", Approach = "approach", Reason = "reason", + FilesInvolved = ["src/C.cs", "src/D.cs"], DetectedAt = DateTime.UtcNow + }; + + await _store.Add(de1); + await _store.Add(de2); + + var results = await _store.FindByFiles(["src/A.cs", "src/X.cs"]); + Assert.Single(results); + Assert.Equal("de-1", results[0].Id); + } + + [Fact] + public async Task FindByFiles_ReturnsEmptyWhenNoMatch() + { + var de = new DeadEnd + { + Id = "de-1", Project = "proj", + Description = "Dead end", Approach = "approach", Reason = "reason", + FilesInvolved = ["src/A.cs"], DetectedAt = DateTime.UtcNow + }; + await _store.Add(de); + + var results = await _store.FindByFiles(["src/Z.cs"]); + Assert.Empty(results); + } + + [Fact] + public async Task Query_FiltersByDateRange() + { + var old = new DeadEnd + { + Id = "de-old", Project = "proj", + Description = "Old dead end", Approach = "approach", Reason = "reason", + FilesInvolved = [], DetectedAt = DateTime.UtcNow.AddDays(-10) + }; + var recent = new DeadEnd + { + Id = "de-recent", Project = "proj", + Description = "Recent dead end", Approach = "approach", Reason = "reason", + FilesInvolved = [], DetectedAt = DateTime.UtcNow + }; + + await _store.Add(old); + await _store.Add(recent); + + var results = await _store.Query(new DeadEndFilter { After = DateTime.UtcNow.AddDays(-1) }); + Assert.Single(results); + Assert.Equal("de-recent", results[0].Id); + } +} diff --git a/tests/DevBrain.Storage.Tests/SqliteGraphStoreMultiEdgeTests.cs b/tests/DevBrain.Storage.Tests/SqliteGraphStoreMultiEdgeTests.cs new file mode 100644 index 0000000..c4dac61 --- /dev/null +++ b/tests/DevBrain.Storage.Tests/SqliteGraphStoreMultiEdgeTests.cs @@ -0,0 +1,75 @@ +using DevBrain.Storage; +using DevBrain.Storage.Schema; +using Microsoft.Data.Sqlite; + +namespace DevBrain.Storage.Tests; + +public class SqliteGraphStoreMultiEdgeTests : IAsyncLifetime +{ + private SqliteConnection _connection = null!; + private SqliteGraphStore _store = null!; + + public async Task InitializeAsync() + { + _connection = new SqliteConnection("Data Source=:memory:"); + await _connection.OpenAsync(); + SchemaManager.Initialize(_connection); + _store = new SqliteGraphStore(_connection); + } + + public async Task DisposeAsync() + { + await _connection.DisposeAsync(); + } + + [Fact] + public async Task GetNeighbors_MultiEdgeTypes_FiltersCorrectly() + { + var a = await _store.AddNode("Decision", "A"); + var b = await _store.AddNode("Decision", "B"); + var c = await _store.AddNode("Decision", "C"); + var d = await _store.AddNode("Decision", "D"); + + await _store.AddEdge(a.Id, b.Id, "caused_by"); + await _store.AddEdge(a.Id, c.Id, "supersedes"); + await _store.AddEdge(a.Id, d.Id, "references"); + + var neighbors = await _store.GetNeighbors(a.Id, hops: 1, edgeTypes: ["caused_by", "supersedes"]); + + Assert.Equal(2, neighbors.Count); + var names = neighbors.Select(n => n.Name).OrderBy(n => n).ToList(); + Assert.Equal(new[] { "B", "C" }, names); + } + + [Fact] + public async Task GetNeighbors_MultiEdgeTypes_MultiHop() + { + var a = await _store.AddNode("Decision", "A"); + var b = await _store.AddNode("Decision", "B"); + var c = await _store.AddNode("Decision", "C"); + + await _store.AddEdge(a.Id, b.Id, "caused_by"); + await _store.AddEdge(b.Id, c.Id, "resolved_by"); + + var neighbors = await _store.GetNeighbors(a.Id, hops: 2, edgeTypes: ["caused_by", "resolved_by"]); + + Assert.Equal(2, neighbors.Count); + var names = neighbors.Select(n => n.Name).OrderBy(n => n).ToList(); + Assert.Equal(new[] { "B", "C" }, names); + } + + [Fact] + public async Task GetNeighbors_MultiEdgeTypes_EmptyList_ReturnsAll() + { + var a = await _store.AddNode("Decision", "A"); + var b = await _store.AddNode("Decision", "B"); + var c = await _store.AddNode("Decision", "C"); + + await _store.AddEdge(a.Id, b.Id, "caused_by"); + await _store.AddEdge(a.Id, c.Id, "references"); + + var neighbors = await _store.GetNeighbors(a.Id, hops: 1, edgeTypes: []); + + Assert.Equal(2, neighbors.Count); + } +} diff --git a/tests/DevBrain.Storage.Tests/SqliteGrowthStoreTests.cs b/tests/DevBrain.Storage.Tests/SqliteGrowthStoreTests.cs new file mode 100644 index 0000000..f526f9a --- /dev/null +++ b/tests/DevBrain.Storage.Tests/SqliteGrowthStoreTests.cs @@ -0,0 +1,121 @@ +using DevBrain.Core.Enums; +using DevBrain.Core.Models; +using DevBrain.Storage; +using DevBrain.Storage.Schema; +using Microsoft.Data.Sqlite; + +namespace DevBrain.Storage.Tests; + +public class SqliteGrowthStoreTests : IAsyncLifetime +{ + private SqliteConnection _connection = null!; + private SqliteGrowthStore _store = null!; + + public async Task InitializeAsync() + { + _connection = new SqliteConnection("Data Source=:memory:"); + await _connection.OpenAsync(); + SchemaManager.Initialize(_connection); + _store = new SqliteGrowthStore(_connection); + } + + public async Task DisposeAsync() + { + await _connection.DisposeAsync(); + } + + [Fact] + public async Task AddMetric_And_GetMetrics_RoundTrips() + { + var now = DateTime.UtcNow; + await _store.AddMetric(new DeveloperMetric + { + Id = "m1", Dimension = "debugging_speed", Value = 12.5, + PeriodStart = now.AddDays(-7), PeriodEnd = now + }); + + var metrics = await _store.GetMetrics("debugging_speed"); + Assert.Single(metrics); + Assert.Equal(12.5, metrics[0].Value); + } + + [Fact] + public async Task GetLatestMetrics_ReturnsOnePerDimension() + { + var now = DateTime.UtcNow; + await _store.AddMetric(new DeveloperMetric + { + Id = "m1", Dimension = "debugging_speed", Value = 10, + PeriodStart = now.AddDays(-14), PeriodEnd = now.AddDays(-7) + }); + await _store.AddMetric(new DeveloperMetric + { + Id = "m2", Dimension = "debugging_speed", Value = 8, + PeriodStart = now.AddDays(-7), PeriodEnd = now + }); + await _store.AddMetric(new DeveloperMetric + { + Id = "m3", Dimension = "dead_end_rate", Value = 0.5, + PeriodStart = now.AddDays(-7), PeriodEnd = now + }); + + var latest = await _store.GetLatestMetrics(); + Assert.Equal(2, latest.Count); + Assert.Contains(latest, m => m.Dimension == "debugging_speed" && m.Value == 8); + Assert.Contains(latest, m => m.Dimension == "dead_end_rate"); + } + + [Fact] + public async Task AddMilestone_And_GetMilestones_RoundTrips() + { + await _store.AddMilestone(new GrowthMilestone + { + Id = "ms1", Type = MilestoneType.First, + Description = "First time using CTE queries", + AchievedAt = DateTime.UtcNow + }); + + var milestones = await _store.GetMilestones(); + Assert.Single(milestones); + Assert.Equal(MilestoneType.First, milestones[0].Type); + Assert.Contains("CTE", milestones[0].Description); + } + + [Fact] + public async Task AddReport_And_GetLatest_RoundTrips() + { + await _store.AddReport(new GrowthReport + { + Id = "r1", + PeriodStart = DateTime.UtcNow.AddDays(-7), + PeriodEnd = DateTime.UtcNow, + Narrative = "Great week — debugging speed improved 20%" + }); + + var latest = await _store.GetLatestReport(); + Assert.NotNull(latest); + Assert.Equal("r1", latest.Id); + Assert.Contains("debugging speed", latest.Narrative); + } + + [Fact] + public async Task Clear_RemovesAllData() + { + await _store.AddMetric(new DeveloperMetric + { + Id = "m1", Dimension = "test", Value = 1, + PeriodStart = DateTime.UtcNow, PeriodEnd = DateTime.UtcNow + }); + await _store.AddMilestone(new GrowthMilestone + { + Id = "ms1", Type = MilestoneType.First, + Description = "test", AchievedAt = DateTime.UtcNow + }); + + await _store.Clear(); + + Assert.Empty(await _store.GetLatestMetrics()); + Assert.Empty(await _store.GetMilestones()); + Assert.Null(await _store.GetLatestReport()); + } +} diff --git a/tests/DevBrain.Storage.Tests/SqliteSessionStoreTests.cs b/tests/DevBrain.Storage.Tests/SqliteSessionStoreTests.cs new file mode 100644 index 0000000..d20e02a --- /dev/null +++ b/tests/DevBrain.Storage.Tests/SqliteSessionStoreTests.cs @@ -0,0 +1,108 @@ +using DevBrain.Core.Models; +using DevBrain.Storage; +using DevBrain.Storage.Schema; +using Microsoft.Data.Sqlite; + +namespace DevBrain.Storage.Tests; + +public class SqliteSessionStoreTests : IAsyncLifetime +{ + private SqliteConnection _connection = null!; + private SqliteSessionStore _store = null!; + + public async Task InitializeAsync() + { + _connection = new SqliteConnection("Data Source=:memory:"); + await _connection.OpenAsync(); + SchemaManager.Initialize(_connection); + _store = new SqliteSessionStore(_connection); + } + + public async Task DisposeAsync() + { + await _connection.DisposeAsync(); + } + + [Fact] + public async Task Add_And_GetBySessionId_RoundTrips() + { + var summary = new SessionSummary + { + Id = "ss-1", + SessionId = "session-abc", + Narrative = "The developer started by investigating a bug...", + Outcome = "Fixed the race condition in auth middleware", + Duration = TimeSpan.FromMinutes(47), + ObservationCount = 23, + FilesTouched = 5, + DeadEndsHit = 1, + Phases = ["Exploration", "Debugging", "Implementation"] + }; + + await _store.Add(summary); + + var fetched = await _store.GetBySessionId("session-abc"); + Assert.NotNull(fetched); + Assert.Equal("ss-1", fetched.Id); + Assert.Equal("session-abc", fetched.SessionId); + Assert.Equal(TimeSpan.FromMinutes(47), fetched.Duration); + Assert.Equal(23, fetched.ObservationCount); + Assert.Equal(3, fetched.Phases.Count); + Assert.Contains("Debugging", fetched.Phases); + } + + [Fact] + public async Task GetLatest_ReturnsNewest() + { + await _store.Add(new SessionSummary + { + Id = "ss-old", SessionId = "s-old", + Narrative = "Old session", Outcome = "Done", + Duration = TimeSpan.FromMinutes(10), ObservationCount = 5, + FilesTouched = 2, DeadEndsHit = 0, Phases = [], + CreatedAt = DateTime.UtcNow.AddHours(-2) + }); + await _store.Add(new SessionSummary + { + Id = "ss-new", SessionId = "s-new", + Narrative = "New session", Outcome = "Also done", + Duration = TimeSpan.FromMinutes(30), ObservationCount = 15, + FilesTouched = 8, DeadEndsHit = 2, Phases = ["Implementation"] + }); + + var latest = await _store.GetLatest(); + Assert.NotNull(latest); + Assert.Equal("ss-new", latest.Id); + } + + [Fact] + public async Task GetBySessionId_ReturnsNullWhenNotFound() + { + var result = await _store.GetBySessionId("nonexistent"); + Assert.Null(result); + } + + [Fact] + public async Task GetAll_ReturnsAllSorted() + { + await _store.Add(new SessionSummary + { + Id = "ss-1", SessionId = "s1", + Narrative = "First", Outcome = "Done", + Duration = TimeSpan.FromMinutes(10), ObservationCount = 5, + FilesTouched = 2, DeadEndsHit = 0, Phases = [], + CreatedAt = DateTime.UtcNow.AddHours(-1) + }); + await _store.Add(new SessionSummary + { + Id = "ss-2", SessionId = "s2", + Narrative = "Second", Outcome = "Done", + Duration = TimeSpan.FromMinutes(20), ObservationCount = 10, + FilesTouched = 4, DeadEndsHit = 1, Phases = [] + }); + + var all = await _store.GetAll(); + Assert.Equal(2, all.Count); + Assert.Equal("ss-2", all[0].Id); // newest first + } +}