From 5b57a5bfbee20c1d952518eb8d10ce4e23263ff2 Mon Sep 17 00:00:00 2001 From: Amin Mesbahi Date: Sun, 19 Jul 2026 19:50:58 +0200 Subject: [PATCH 01/14] feat(gui): comprehensive UX/UI improvements and code cleanup ## Critical fixes - Fix e2e smoke tests to use current routes - Consolidate duplicate Catalog/Registry pages into Catalog with merged features - Define missing .spinner CSS class and .data-table styles - Add 5-minute context timeout to RunSkell to prevent GUI hangs - Fix Catalog lifecycle filter to show all 5 stages ## UX improvements - Make breadcrumbs clickable in ProjectPageHeader and SkillDetail - Add cursor-pointer to all clickable non-button elements (modals, backdrop, file tree) - Make project names/paths clickable in Repositories page - Standardize heading hierarchy (text-2xl font-bold) across all pages - Standardize card backgrounds using .card CSS class - Fix broken navigation links (/skills, /registry, Shared Library) ## Performance - Add React.memo to SkillCard, 4 Badge components, CollapsibleSection - Batch repository health fetches in groups of 2 to prevent process storms - Add client-side pagination (50/page) to InstalledSkills and Activity ## Code cleanup - Delete 6 orphaned page files (Registry, AuditLog, Doctor, Sync, Cache, Dashboard) - Delete 6 stale test files for deleted pages - Replace catch (e: any) with catch (e: unknown) in Settings - Clean up eslint-disable comment in SkillDetail ## CI/CD - Update CI and Release workflows to use Wails v3 CLI (was v2) --- .github/workflows/ci.yml | 6 +- .github/workflows/release.yml | 12 +- gui/app.go | 15 +- gui/frontend/e2e/smoke.spec.ts | 16 +- .../src/components/AddFromURLDialog.tsx | 30 +- gui/frontend/src/components/Badges.tsx | 17 +- .../src/components/CollapsibleSection.tsx | 6 +- gui/frontend/src/components/ConfirmDialog.tsx | 2 +- .../src/components/ProjectPageHeader.tsx | 47 ++ gui/frontend/src/components/SkillCard.tsx | 5 +- .../src/components/SkillPreviewModal.tsx | 2 +- gui/frontend/src/index.css | 22 + .../src/pages/{AuditLog.tsx => Activity.tsx} | 45 +- gui/frontend/src/pages/Cache.tsx | 150 ------ gui/frontend/src/pages/Catalog.tsx | 227 +++++++++ gui/frontend/src/pages/ContributeInfo.tsx | 4 +- gui/frontend/src/pages/Dashboard.tsx | 313 ------------ gui/frontend/src/pages/Doctor.tsx | 207 -------- gui/frontend/src/pages/Home.tsx | 54 ++ gui/frontend/src/pages/InstalledSkills.tsx | 26 +- gui/frontend/src/pages/Registry.tsx | 460 ------------------ gui/frontend/src/pages/Repositories.tsx | 73 ++- gui/frontend/src/pages/Settings.tsx | 12 +- gui/frontend/src/pages/SkillDetail.tsx | 46 +- gui/frontend/src/pages/Sync.tsx | 333 ------------- .../pages/projects/ProjectActivityPage.tsx | 62 +++ .../src/pages/projects/ProjectAgentsPage.tsx | 54 ++ .../src/pages/projects/ProjectHealthPage.tsx | 70 +++ .../src/pages/projects/ProjectOverview.tsx | 145 ++++++ .../src/pages/projects/ProjectSkillsPage.tsx | 208 ++++++++ gui/frontend/src/router.tsx | 83 +++- gui/frontend/src/test/pages/AuditLog.test.tsx | 95 ---- gui/frontend/src/test/pages/Cache.test.tsx | 84 ---- .../src/test/pages/Dashboard.test.tsx | 67 --- gui/frontend/src/test/pages/Doctor.test.tsx | 73 --- gui/frontend/src/test/pages/Registry.test.tsx | 115 ----- gui/frontend/src/test/pages/Sync.test.tsx | 154 ------ 37 files changed, 1152 insertions(+), 2188 deletions(-) create mode 100644 gui/frontend/src/components/ProjectPageHeader.tsx rename gui/frontend/src/pages/{AuditLog.tsx => Activity.tsx} (87%) delete mode 100644 gui/frontend/src/pages/Cache.tsx create mode 100644 gui/frontend/src/pages/Catalog.tsx delete mode 100644 gui/frontend/src/pages/Dashboard.tsx delete mode 100644 gui/frontend/src/pages/Doctor.tsx create mode 100644 gui/frontend/src/pages/Home.tsx delete mode 100644 gui/frontend/src/pages/Registry.tsx delete mode 100644 gui/frontend/src/pages/Sync.tsx create mode 100644 gui/frontend/src/pages/projects/ProjectActivityPage.tsx create mode 100644 gui/frontend/src/pages/projects/ProjectAgentsPage.tsx create mode 100644 gui/frontend/src/pages/projects/ProjectHealthPage.tsx create mode 100644 gui/frontend/src/pages/projects/ProjectOverview.tsx create mode 100644 gui/frontend/src/pages/projects/ProjectSkillsPage.tsx delete mode 100644 gui/frontend/src/test/pages/AuditLog.test.tsx delete mode 100644 gui/frontend/src/test/pages/Cache.test.tsx delete mode 100644 gui/frontend/src/test/pages/Dashboard.test.tsx delete mode 100644 gui/frontend/src/test/pages/Doctor.test.tsx delete mode 100644 gui/frontend/src/test/pages/Registry.test.tsx delete mode 100644 gui/frontend/src/test/pages/Sync.test.tsx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbae0ce..0ecac44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,8 +122,8 @@ jobs: - uses: oven-sh/setup-bun@v2 - - name: Install Wails CLI - run: go install github.com/wailsapp/wails/v2/cmd/wails@latest + - name: Install Wails v3 CLI + run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest - name: Install frontend dependencies working-directory: gui @@ -131,7 +131,7 @@ jobs: - name: Build GUI working-directory: gui - run: wails build -nsis -o Skell-windows-amd64.exe + run: wails3 build -nsis -o Skell-windows-amd64.exe - name: Upload GUI artifact uses: actions/upload-artifact@v7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a26584c..d843fc7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,8 +50,8 @@ jobs: - uses: oven-sh/setup-bun@v2 - - name: Install Wails CLI - run: go install github.com/wailsapp/wails/v2/cmd/wails@latest + - name: Install Wails v3 CLI + run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest - name: Install frontend dependencies working-directory: gui @@ -59,7 +59,7 @@ jobs: - name: Build GUI working-directory: gui - run: wails build -o Skell-windows-amd64.exe + run: wails3 build -o Skell-windows-amd64.exe - name: Build bundled Windows package shell: pwsh @@ -117,8 +117,8 @@ jobs: - uses: oven-sh/setup-bun@v2 - - name: Install Wails CLI - run: go install github.com/wailsapp/wails/v2/cmd/wails@latest + - name: Install Wails v3 CLI + run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest - name: Install frontend dependencies working-directory: gui @@ -126,7 +126,7 @@ jobs: - name: Build GUI working-directory: gui - run: wails build -platform darwin/${{ matrix.arch }} + run: wails3 build -platform darwin/${{ matrix.arch }} - name: Zip Skell.app id: zipapp diff --git a/gui/app.go b/gui/app.go index 6bb8d65..27319ec 100644 --- a/gui/app.go +++ b/gui/app.go @@ -309,13 +309,26 @@ func (a *App) RunSkell(args []string) SkellResult { } } - cmd := exec.Command(bin, args...) //nolint:gosec // args come from trusted frontend + // Each skell invocation is given a generous timeout to prevent a hung CLI + // process from blocking the GUI event loop indefinitely. The timeout is + // chosen to accommodate slow network operations (git clone, large syncs) + // while still providing a safety net. + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + cmd := exec.CommandContext(ctx, bin, args...) //nolint:gosec // args come from trusted frontend hideConsoleWindow(cmd) var stdout, stderr strings.Builder cmd.Stdout = &stdout cmd.Stderr = &stderr err = cmd.Run() + if ctx.Err() != nil { + return SkellResult{ + Stderr: fmt.Sprintf("skell command timed out after %v: %s", 5*time.Minute, ctx.Err()), + Success: false, + } + } return SkellResult{ Stdout: stdout.String(), Stderr: stderr.String(), diff --git a/gui/frontend/e2e/smoke.spec.ts b/gui/frontend/e2e/smoke.spec.ts index 15bfffe..2978725 100644 --- a/gui/frontend/e2e/smoke.spec.ts +++ b/gui/frontend/e2e/smoke.spec.ts @@ -6,14 +6,10 @@ import { injectWailsMock } from "./mock-wails"; */ const routes = [ - { path: "/", label: "Dashboard" }, - { path: "/repositories", label: "Repositories" }, - { path: "/skills", label: "Installed Skills" }, - { path: "/registry", label: "Registry" }, - { path: "/sync", label: "Sync" }, - { path: "/doctor", label: "Doctor" }, - { path: "/cache", label: "Cache" }, - { path: "/audit", label: "Audit Log" }, + { path: "/", label: "Home" }, + { path: "/projects", label: "Projects" }, + { path: "/catalog", label: "Catalog" }, + { path: "/activity", label: "Activity" }, { path: "/settings", label: "Settings" }, ]; @@ -48,8 +44,8 @@ for (const route of routes) { }); } -test("unknown route redirects to Dashboard", async ({ page }) => { +test("unknown route redirects to Home", async ({ page }) => { await injectWailsMock(page); await page.goto("/this-does-not-exist"); - await expect(page.getByRole("heading", { level: 1, name: "Dashboard" })).toBeVisible(); + await expect(page.getByRole("heading", { level: 1, name: "Home" })).toBeVisible(); }); diff --git a/gui/frontend/src/components/AddFromURLDialog.tsx b/gui/frontend/src/components/AddFromURLDialog.tsx index dd128bb..cb1a6dd 100644 --- a/gui/frontend/src/components/AddFromURLDialog.tsx +++ b/gui/frontend/src/components/AddFromURLDialog.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { X, Link, Loader2, CheckCircle2 } from "lucide-react"; import { useRepoStore, useUIStore } from "@/store"; import { addSkillFromURL } from "@/lib/skell"; @@ -22,13 +22,17 @@ export function AddFromURLDialog({ const { repos, selectedRepo } = useRepoStore(); const { notify } = useUIStore(); - const defaultRepo = initialRepo ?? (selectedRepo === "global" ? "global" : (selectedRepo || repos[0] || "")); + const defaultRepo = useMemo(() => initialRepo ?? (selectedRepo && selectedRepo !== "global" ? selectedRepo : (repos[0] ?? "")), [initialRepo, selectedRepo, repos]); const [url, setUrl] = useState(""); const [repo, setRepo] = useState(defaultRepo); const [dryRun, setDryRun] = useState(false); const [loading, setLoading] = useState(false); const [dryRunResult, setDryRunResult] = useState(null); + useEffect(() => { + setRepo(defaultRepo); + }, [defaultRepo]); + if (!open) return null; async function handleSubmit(e: React.FormEvent) { @@ -87,7 +91,7 @@ export function AddFromURLDialog({ return (
@@ -95,7 +99,7 @@ export function AddFromURLDialog({
- Add Skill or Source + Add Skill from Repository
); -} +}); diff --git a/gui/frontend/src/components/ConfirmDialog.tsx b/gui/frontend/src/components/ConfirmDialog.tsx index b600a30..b223f07 100644 --- a/gui/frontend/src/components/ConfirmDialog.tsx +++ b/gui/frontend/src/components/ConfirmDialog.tsx @@ -25,7 +25,7 @@ export function ConfirmDialog({
{/* Backdrop */}
{/* Dialog */} diff --git a/gui/frontend/src/components/ProjectPageHeader.tsx b/gui/frontend/src/components/ProjectPageHeader.tsx new file mode 100644 index 0000000..af208c0 --- /dev/null +++ b/gui/frontend/src/components/ProjectPageHeader.tsx @@ -0,0 +1,47 @@ +import type { ReactNode } from "react"; +import { ArrowLeft, ChevronRight } from "lucide-react"; +import { useNavigate, Link } from "react-router-dom"; +import { getProjectDisplayName, createProjectId } from "@/lib/navigation"; + +interface ProjectPageHeaderProps { + projectPath: string; + title: string; + subtitle?: string; + breadcrumb?: string; + backLabel?: string; + actions?: ReactNode; +} + +export function ProjectPageHeader({ projectPath, title, subtitle, breadcrumb, backLabel = "Back to Projects", actions }: ProjectPageHeaderProps) { + const navigate = useNavigate(); + const projectName = getProjectDisplayName(projectPath); + const projectId = projectPath ? createProjectId(projectPath) : ""; + + return ( +
+
+
+ +
+ Projects + + {projectName} + {breadcrumb && ( + <> + + {breadcrumb} + + )} +
+

{title}

+ {subtitle &&

{subtitle}

} + {projectPath &&

{projectPath}

} +
+ {actions &&
{actions}
} +
+
+ ); +} diff --git a/gui/frontend/src/components/SkillCard.tsx b/gui/frontend/src/components/SkillCard.tsx index 0647343..a28874d 100644 --- a/gui/frontend/src/components/SkillCard.tsx +++ b/gui/frontend/src/components/SkillCard.tsx @@ -1,3 +1,4 @@ +import { memo } from "react"; import { Download, Eye } from "lucide-react"; import type { RegistrySkill } from "@/lib/types"; import { LifecycleBadge } from "./Badges"; @@ -11,7 +12,7 @@ interface SkillCardProps { onPreview: () => void; } -export function SkillCard({ +export const SkillCard = memo(function SkillCard({ skill, installing, installed, @@ -84,5 +85,5 @@ export function SkillCard({
); -} +}); diff --git a/gui/frontend/src/components/SkillPreviewModal.tsx b/gui/frontend/src/components/SkillPreviewModal.tsx index 551c355..053808b 100644 --- a/gui/frontend/src/components/SkillPreviewModal.tsx +++ b/gui/frontend/src/components/SkillPreviewModal.tsx @@ -66,7 +66,7 @@ export function SkillPreviewModal({ aria-modal="true" aria-labelledby="preview-title" > -
+
{/* Header */}
diff --git a/gui/frontend/src/index.css b/gui/frontend/src/index.css index 3731a93..1574fd5 100644 --- a/gui/frontend/src/index.css +++ b/gui/frontend/src/index.css @@ -71,6 +71,28 @@ @apply bg-[#13162a] border border-[#1e2540] rounded-xl p-5 shadow-sm; } + /* Spinner */ + .spinner { + @apply inline-block border-2 border-current border-r-transparent rounded-full animate-spin; + } + + /* Data table (used by AuditLog / Activity) */ + .data-table { + @apply w-full text-sm; + } + .data-table th { + @apply text-left px-4 py-2.5 text-xs font-semibold text-slate-500 uppercase tracking-wider bg-[#0f1225] border-b border-[#1e2540]; + } + .data-table td { + @apply px-4 py-2.5 border-b border-[#1e2540]/50 text-slate-400; + } + .data-table tr:last-child td { + @apply border-b-0; + } + .data-table tr:hover td { + @apply bg-white/[0.02]; + } + /* Input */ .input { @apply w-full bg-[#0f1225] border border-[#2d3348] rounded-lg px-3 py-2 text-sm diff --git a/gui/frontend/src/pages/AuditLog.tsx b/gui/frontend/src/pages/Activity.tsx similarity index 87% rename from gui/frontend/src/pages/AuditLog.tsx rename to gui/frontend/src/pages/Activity.tsx index 0161a3c..ec0bfef 100644 --- a/gui/frontend/src/pages/AuditLog.tsx +++ b/gui/frontend/src/pages/Activity.tsx @@ -1,5 +1,6 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { ScrollText, RefreshCw, Search, Filter } from "lucide-react"; +import { useRepoStore } from "@/store"; import { readAuditLog, getAuditLogPath } from "@/lib/skell"; import type { AuditEntry } from "@/lib/types"; @@ -13,10 +14,11 @@ const ACTION_COLORS: Record = { const PAGE_SIZE = 50; -export function AuditLog() { +export function Activity() { + const { selectedRepo } = useRepoStore(); const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(false); - const [search, setSearch] = useState(""); + const [query, setQuery] = useState(""); const [actionFilter, setActionFilter] = useState(""); const [page, setPage] = useState(1); const [error, setError] = useState(null); @@ -41,21 +43,23 @@ export function AuditLog() { useEffect(() => { void load(); - }, []); + }, [selectedRepo]); - const filtered = entries.filter((e) => { - if (actionFilter && e.action !== actionFilter) return false; - if (search) { - const q = search.toLowerCase(); - return ( - e.skill?.toLowerCase().includes(q) || - e.repo?.toLowerCase().includes(q) || - e.registry?.toLowerCase().includes(q) || - e.action?.toLowerCase().includes(q) - ); - } - return true; - }); + const filtered = useMemo(() => { + return entries.filter((e) => { + if (actionFilter && e.action !== actionFilter) return false; + if (query) { + const q = query.toLowerCase(); + return ( + e.skill?.toLowerCase().includes(q) || + e.repo?.toLowerCase().includes(q) || + e.registry?.toLowerCase().includes(q) || + e.action?.toLowerCase().includes(q) + ); + } + return true; + }); + }, [actionFilter, entries, query]); const paged = filtered.slice(0, page * PAGE_SIZE); const actions = [...new Set(entries.map((e) => e.action).filter(Boolean))]; @@ -66,7 +70,7 @@ export function AuditLog() {

- Audit Log + Activity

{filtered.length} entries โ€” {entries.length} total @@ -95,8 +99,8 @@ export function AuditLog() { { setSearch(e.target.value); setPage(1); }} + value={query} + onChange={(e) => { setQuery(e.target.value); setPage(1); }} />

@@ -185,3 +189,4 @@ export function AuditLog() {
); } + diff --git a/gui/frontend/src/pages/Cache.tsx b/gui/frontend/src/pages/Cache.tsx deleted file mode 100644 index 04d0483..0000000 --- a/gui/frontend/src/pages/Cache.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { useEffect, useState } from "react"; -import { Database, RefreshCw, Trash2 } from "lucide-react"; -import { useRepoStore, useUIStore } from "@/store"; -import { cacheStatus, cacheRefresh, cacheClear } from "@/lib/skell"; -import { ConfirmDialog } from "@/components/ConfirmDialog"; - -export function Cache() { - const { notify } = useUIStore(); - const { selectedRepo } = useRepoStore(); - const [statusText, setStatusText] = useState(""); - const [loading, setLoading] = useState(false); - const [refreshing, setRefreshing] = useState(false); - const [clearing, setClearing] = useState(false); - const [confirmClear, setConfirmClear] = useState(false); - - async function loadStatus() { - setLoading(true); - try { - const result = await cacheStatus(); - setStatusText(result.success ? result.stdout : result.stderr); - } catch (e) { - setStatusText(`Failed to read cache status: ${String(e)}`); - } finally { - setLoading(false); - } - } - - async function handleRefresh() { - setRefreshing(true); - try { - const repo = selectedRepo !== "global" ? selectedRepo : undefined; - const result = await cacheRefresh(repo); - if (result.success) { - notify({ kind: "success", title: "Cache refreshed", detail: result.stdout.trim() }); - await loadStatus(); - } else { - notify({ kind: "error", title: "Refresh failed", detail: result.stderr }); - } - } finally { - setRefreshing(false); - } - } - - async function handleClear() { - setConfirmClear(false); - setClearing(true); - try { - const result = await cacheClear(); - if (result.success) { - notify({ kind: "success", title: "Cache cleared" }); - await loadStatus(); - } else { - notify({ kind: "error", title: "Clear failed", detail: result.stderr }); - } - } finally { - setClearing(false); - } - } - - useEffect(() => { - void loadStatus(); - }, []); - - return ( -
- {/* Header */} -
-
-

- - Cache -

-

- Manage local clones of registry repositories (~/.skell/cache) -

-
- -
- - {/* Status */} -
-

Cache Status

- {loading ? ( -
-
- Loading... -
- ) : statusText ? ( -
-            {statusText}
-          
- ) : ( -

No cache data available.

- )} -
- - {/* Actions */} -
-

Actions

-
-
-
-

Refresh Cache

-

- Re-fetch all configured registries from GitHub -

-
- -
- -
-
-

Clear Cache

-

- Delete all locally cached registry data. Registries will be re-fetched on next use. -

-
- -
-
-
- - void handleClear()} - onCancel={() => setConfirmClear(false)} - /> -
- ); -} diff --git a/gui/frontend/src/pages/Catalog.tsx b/gui/frontend/src/pages/Catalog.tsx new file mode 100644 index 0000000..0b1b3e3 --- /dev/null +++ b/gui/frontend/src/pages/Catalog.tsx @@ -0,0 +1,227 @@ +import { useEffect, useMemo, useState, useCallback } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { Search, Tag, Filter } from "lucide-react"; +import { listRegistry, installSkill, listInstalled, listInstalledGlobal } from "@/lib/skell"; +import { useRepoStore, useUIStore } from "@/store"; +import type { RegistrySkill, InstalledSkill, Lifecycle } from "@/lib/types"; +import { getProjectDisplayName } from "@/lib/navigation"; +import { SkillCard } from "@/components/SkillCard"; +import { SkillPreviewModal } from "@/components/SkillPreviewModal"; + +const LIFECYCLES: Lifecycle[] = ["stable", "experimental", "draft", "deprecated", "archived"]; + +function indexInstalled(skills: InstalledSkill[]): Record { + const map: Record = {}; + for (const s of skills) map[s.name] = s; + return map; +} + +export function Catalog() { + const location = useLocation(); + const navigate = useNavigate(); + const { selectedRepo, repos, setSelectedRepo } = useRepoStore(); + const { notify } = useUIStore(); + const [queryInput, setQueryInput] = useState(""); + const [query, setQuery] = useState(""); // debounced + const [lifecycle, setLifecycle] = useState(""); + const [owner, setOwner] = useState(""); + const [sourceFilter, setSourceFilter] = useState<"all" | "global" | "local">("all"); + const [skills, setSkills] = useState([]); + const [installed, setInstalled] = useState>({}); + const [previewTarget, setPreviewTarget] = useState(null); + const [installing, setInstalling] = useState(null); + const [loading, setLoading] = useState(false); + + const installDestination = (location.state as { installDestination?: string } | null)?.installDestination ?? (selectedRepo && selectedRepo !== "global" ? selectedRepo : undefined); + const [destination, setDestination] = useState(installDestination ?? ""); + const repo = destination && destination !== "global" ? destination : undefined; + + // Debounce query input + useEffect(() => { + const t = setTimeout(() => setQuery(queryInput), 300); + return () => clearTimeout(t); + }, [queryInput]); + + useEffect(() => { + setDestination(installDestination ?? ""); + }, [installDestination]); + + const loadData = useCallback(async () => { + setLoading(true); + try { + const [registrySkills, installedSkills] = await Promise.all([ + listRegistry().catch(() => [] as RegistrySkill[]), + (repo ? listInstalled(repo) : listInstalledGlobal()).catch(() => [] as InstalledSkill[]), + ]); + setSkills(registrySkills); + setInstalled(indexInstalled(installedSkills)); + } finally { + setLoading(false); + } + }, [repo]); + + useEffect(() => { + void loadData(); + }, [loadData]); + + const filtered = useMemo(() => { + const needle = query.toLowerCase(); + const ownerNeedle = owner.toLowerCase(); + return skills.filter((skill) => { + const matchesQuery = !needle || `${skill.name} ${skill.description ?? ""}`.toLowerCase().includes(needle); + const matchesLifecycle = !lifecycle || skill.metadata.lifecycle === lifecycle; + const matchesOwner = !ownerNeedle || (skill.metadata.owner ?? "").toLowerCase().includes(ownerNeedle); + const matchesSource = sourceFilter === "all" || skill.registry_source === sourceFilter; + return matchesQuery && matchesLifecycle && matchesOwner && matchesSource; + }); + }, [query, lifecycle, owner, sourceFilter, skills]); + + async function handleInstall(skill: RegistrySkill) { + if (!destination) { + notify({ kind: "info", title: "Select a project first", detail: "Choose a project before installing a catalog skill." }); + return; + } + setInstalling(skill.name); + try { + const result = await installSkill({ + skillName: skill.name, + repo: destination, + registry: skill.registry_alias || undefined, + registryURL: skill.registry_url || undefined, + }); + if (result.success) { + notify({ kind: "success", title: `Installed ${skill.name}`, detail: `Destination: ${getProjectDisplayName(destination)}` }); + const refreshed = destination === "global" + ? await listInstalledGlobal() + : await listInstalled(destination); + setInstalled(indexInstalled(refreshed)); + } else { + const detail = result.stderr + ? result.stderr.split(/\r?\nUsage:/, 1)[0].trim() + : "Unable to install skill."; + notify({ kind: "error", title: "Install failed", detail }); + } + } catch (error) { + notify({ + kind: "error", + title: "Install failed", + detail: error instanceof Error ? error.message : String(error), + }); + } finally { + setInstalling(null); + } + } + + return ( +
+
+
+

Catalog

+

Browse skills from the registry and install them into the selected project.

+
+
+ + {/* Destination selector */} +
+
+
+

Installing to

+

{destination ? `${getProjectDisplayName(destination)} ยท ${destination}` : "No project selected"}

+
+
+ + +
+
+
+ + {/* Filters */} +
+
+ + + + +
+
+ + {/* Skills grid */} + {loading ? ( +
+
+
+ ) : filtered.length === 0 ? ( +
+ +

+ {skills.length === 0 + ? "No skills found in the registry. Add a shared source in Settings to populate the catalog." + : "No skills match the current filters."} +

+
+ ) : ( +
+ {filtered.map((skill) => ( + void handleInstall(skill)} + onPreview={() => setPreviewTarget(skill)} + /> + ))} +
+ )} + + {previewTarget && ( + setPreviewTarget(null)} + onInstall={() => { + const skill = previewTarget; + setPreviewTarget(null); + void handleInstall(skill); + }} + /> + )} +
+ ); +} diff --git a/gui/frontend/src/pages/ContributeInfo.tsx b/gui/frontend/src/pages/ContributeInfo.tsx index b519cff..63b7146 100644 --- a/gui/frontend/src/pages/ContributeInfo.tsx +++ b/gui/frontend/src/pages/ContributeInfo.tsx @@ -35,7 +35,7 @@ export function ContributeInfo() {
-

Contribute Metadata

+

Contribute Metadata

Improve skill metadata and open a PR on the upstream repository.

@@ -73,7 +73,7 @@ export function ContributeInfo() { {/* CTA */} -
- - {/* Stat cards */} -
- } - label="Total Skills" - value={totalSkills} - bg="bg-blue-500/10" - onClick={() => navigate("/skills")} - /> - } - label="Outdated" - value={totalOutdated} - bg="bg-amber-500/10" - highlight={totalOutdated > 0} - onClick={() => navigate("/skills")} - /> - } - label="Pinned" - value={totalPinned} - bg="bg-blue-500/10" - onClick={() => navigate("/skills")} - /> - } - label="Doctor Issues" - value={totalIssues} - bg="bg-red-500/10" - highlight={totalIssues > 0} - onClick={() => navigate("/doctor")} - /> -
- - {/* Quick Actions โ€” makes the main flows obvious */} -
- - - - - -
- - {/* Repos overview */} - {repos.length > 0 ? ( -
-

- - Projects Overview -

-
- {repoStats.map((r) => ( - navigate("/skills")} - /> - ))} -
-
- ) : ( -
-
- -
-

Get started

-

- {skellMissing - ? "Install the skell CLI first (required for all operations)." - : "Add a project to begin managing skills."} -

- -
- )} - - {/* Recent status */} - {recentStatuses.length > 0 && ( -
-

- - Skill Status - - โ€” {repos[0]?.split(/[/\\]/).at(-1)} - -

- - - - - - - - - - - {recentStatuses.map((s) => ( - - - - - - - ))} - -
SkillInstalledLatestStatus
{s.name}{s.installed}{s.latest} - -
-
- )} -
- ); -} - -function StatCard({ - icon, - label, - value, - bg, - highlight, - onClick, -}: { - icon: React.ReactNode; - label: string; - value: number; - bg: string; - highlight?: boolean; - onClick?: () => void; -}) { - return ( - - ); -} - -function RepoRow({ - repo, - onClick, -}: { - repo: RepoStats; - onClick: () => void; -}) { - const health = - repo.issues > 0 - ? "text-red-400" - : repo.outdated > 0 - ? "text-amber-400" - : "text-emerald-400"; - - return ( - - ); -} diff --git a/gui/frontend/src/pages/Doctor.tsx b/gui/frontend/src/pages/Doctor.tsx deleted file mode 100644 index 5855382..0000000 --- a/gui/frontend/src/pages/Doctor.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import { useEffect, useState, useCallback, useMemo } from "react"; -import { RefreshCw, Stethoscope, CheckCircle2 } from "lucide-react"; -import { useRepoStore, useUIStore } from "@/store"; -import { doctorCheck } from "@/lib/skell"; -import type { DiagnosticEntry } from "@/lib/types"; -import { SeverityBadge } from "@/components/Badges"; - -interface RepoIssues { - repo: string; - name: string; - issues: DiagnosticEntry[]; - loading: boolean; - error: string | null; -} - -export function Doctor() { - const { repos, selectedRepo } = useRepoStore(); - const { notify } = useUIStore(); - // Stable identity across renders โ€” a fresh array each render would make the - // init useCallback/useEffect loop and freeze the page. - const targets = useMemo( - () => (selectedRepo && selectedRepo !== "global" ? [selectedRepo] : repos), - [selectedRepo, repos] - ); - - const [repoIssues, setRepoIssues] = useState([]); - const [running, setRunning] = useState(false); - - const init = useCallback(() => { - setRepoIssues( - targets.map((repo) => ({ - repo, - name: repo.split(/[/\\]/).at(-1) ?? repo, - issues: [], - loading: false, - error: null, - })) - ); - }, [targets]); - - useEffect(() => { - init(); - }, [init]); - - async function runDoctor(repo: string) { - setRepoIssues((prev) => - prev.map((r) => (r.repo === repo ? { ...r, loading: true, error: null } : r)) - ); - try { - const issues = await doctorCheck(repo); - setRepoIssues((prev) => - prev.map((r) => - r.repo === repo ? { ...r, loading: false, issues } : r - ) - ); - } catch (e) { - setRepoIssues((prev) => - prev.map((r) => - r.repo === repo ? { ...r, loading: false, error: String(e) } : r - ) - ); - } - } - - async function runAll() { - setRunning(true); - for (const repo of targets) { - await runDoctor(repo); - } - setRunning(false); - const total = repoIssues.reduce((s, r) => s + r.issues.length, 0); - notify({ - kind: total === 0 ? "success" : "error", - title: total === 0 ? "No issues found" : `${total} issue${total !== 1 ? "s" : ""} found`, - }); - } - - const totalErrors = repoIssues.reduce( - (s, r) => s + r.issues.filter((i) => i.severity === "error").length, - 0 - ); - const totalWarnings = repoIssues.reduce( - (s, r) => s + r.issues.filter((i) => i.severity === "warning").length, - 0 - ); - - return ( -
- {/* Header */} -
-
-

- - Doctor -

-

- Diagnose manifest, lock file, and install problems -

-
- -
- - {/* Summary */} - {(totalErrors > 0 || totalWarnings > 0) && ( -
- {totalErrors > 0 && ( -
- {totalErrors} error{totalErrors !== 1 ? "s" : ""} -
- )} - {totalWarnings > 0 && ( -
- {totalWarnings} warning{totalWarnings !== 1 ? "s" : ""} -
- )} -
- )} - - {targets.length === 0 ? ( -
- No projects. Add one first. -
- ) : ( -
- {repoIssues.map((ri) => ( -
-
-
-

{ri.name}

-

{ri.repo}

-
- -
- - {ri.loading && ( -
-
- Checking... -
- )} - - {ri.error && ( -
- {ri.error} -
- )} - - {!ri.loading && !ri.error && ri.issues.length === 0 && ( -
- - No issues found โ€” all good! -
- )} - - {!ri.loading && ri.issues.length > 0 && ( -
- {ri.issues.map((issue, idx) => ( - - ))} -
- )} -
- ))} -
- )} -
- ); -} - -function IssueRow({ issue }: { issue: DiagnosticEntry }) { - return ( -
-
- - {issue.code} - {issue.message} -
- {issue.hint && ( -

- ๐Ÿ’ก {issue.hint} -

- )} -
- ); -} diff --git a/gui/frontend/src/pages/Home.tsx b/gui/frontend/src/pages/Home.tsx new file mode 100644 index 0000000..96ff0cd --- /dev/null +++ b/gui/frontend/src/pages/Home.tsx @@ -0,0 +1,54 @@ +import { Link } from "react-router-dom"; +import { Plus, Compass, Sparkles } from "lucide-react"; +import { useRepoStore } from "@/store"; + +export function Home() { + const { repos } = useRepoStore(); + + if (repos.length === 0) { + return ( +
+
+
+ +
+

Manage agent skills across your projects

+

+ Skell helps you discover, install, validate, and keep agent skills organised for the coding tools you use. +

+
+ + + Add Project + + + + Browse Catalog + +
+
+
+ ); + } + + return ( +
+

Home

+

Actionable summaries for the projects you manage.

+
+ +

Projects requiring setup

+

{repos.length}

+ + +

Browse the catalog

+

Discover skills for the tools you use.

+ + +

Recent activity

+

Open the activity feed for recent changes.

+ +
+
+ ); +} diff --git a/gui/frontend/src/pages/InstalledSkills.tsx b/gui/frontend/src/pages/InstalledSkills.tsx index cce6d1f..3797034 100644 --- a/gui/frontend/src/pages/InstalledSkills.tsx +++ b/gui/frontend/src/pages/InstalledSkills.tsx @@ -54,6 +54,8 @@ const ALL_STATUSES: SkillStatus[] = [ "unversioned", ]; +const PAGE_SIZE = 50; + export function InstalledSkills() { const navigate = useNavigate(); const { selectedRepo, repos } = useRepoStore(); @@ -68,6 +70,7 @@ export function InstalledSkills() { const [addDialogOpen, setAddDialogOpen] = useState(false); const [repoInited, setRepoInited] = useState(null); const [initRunning, setInitRunning] = useState(false); + const [page, setPage] = useState(1); // Proactive missing-CLI state so we can avoid the "loading forever" perception // (and show guidance) on a fresh PC before any list* calls are attempted. @@ -173,6 +176,8 @@ export function InstalledSkills() { return list; }, [skills, search, statusFilter]); + const paged = useMemo(() => filtered.slice(0, page * PAGE_SIZE), [filtered, page]); + async function handleUpgrade(sk: SkillRow) { setActing(sk.name); try { @@ -246,7 +251,7 @@ export function InstalledSkills() {
@@ -336,7 +341,7 @@ export function InstalledSkills() { setQueryInput(e.target.value)} - /> -
-
- - - setOwner(e.target.value)} - /> -
- {/* Source filter โ€” only useful when local repo is selected (results include both sources) */} - {selectedRepo !== "global" && ( -
- {(["all", "local", "global"] as const).map((s) => ( - - ))} -
- )} -
- - {/* Results */} - {loading ? ( -
-
-
- ) : skills.length === 0 ? ( -
- -

No skills found. Try adjusting your search.

-
- ) : ( -
- {Array.from(grouped.entries()).map(([source, sourceSkills]) => ( - -
- {sourceSkills.map((sk) => ( - setInstallTarget(sk)} - onPreview={() => setPreviewTarget(sk)} - /> - ))} -
-
- ))} -
- )} - - {/* Install dialog */} - {installTarget && ( -
-
setInstallTarget(null)} /> -
-

- Install "{installTarget.name}" -

-
- {installTarget.registry_alias && ( -

Registry: {installTarget.registry_alias}

- )} -

- Target: - {selectedRepo === "global" ? "Shared Library" : selectedRepo?.split(/[/\\]/).at(-1) ?? "โ€”"} - -

-
-
- - -
-
-
- )} - - {/* Add from URL */} - setAddDialogOpen(false)} - onSuccess={() => setRefreshKey((k) => k + 1)} - /> - - {/* Preview modal */} - {previewTarget && ( - setPreviewTarget(null)} - onInstall={() => { - const target = previewTarget; - setPreviewTarget(null); - setInstallTarget(target); - }} - /> - )} -
- ); -} - -function renderSourceHeader(sample: RegistrySkill): ReactNode { - const alias = sample.registry_alias?.trim(); - const url = (sample.registry_url || sample.metadata?.source_repo || "").trim(); - const isLocal = - !!url && (url.startsWith("/") || url.startsWith("file:") || /^[A-Za-z]:[\\/]/.test(url)); - const Icon = !url && !alias ? HardDrive : isLocal ? FolderClosed : Globe; - const pretty = url ? (isLocal ? url : prettifyGitURL(url)) : ""; - - return ( - <> - - {alias ? ( - <> - {alias} - {pretty && ( - - {pretty} - - )} - - ) : ( - - {pretty || "Project manifest"} - - )} - - ); -} - -function prettifyGitURL(url: string): string { - try { - const u = new URL(url); - const path = u.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, ""); - if (path) return `${u.host}/${path}`; - } catch { - // not a URL โ€” fall through - } - return url; -} - -async function loadInstalledSkills(selectedRepo: string): Promise { - try { - if (!selectedRepo || selectedRepo === "global") { - return await listInstalledGlobal(); - } - return await listInstalled(selectedRepo); - } catch { - return []; - } -} - -function indexInstalledSkills(skills: InstalledSkill[]): Record { - return skills.reduce>((acc, skill) => { - acc[skill.name] = skill; - return acc; - }, {}); -} diff --git a/gui/frontend/src/pages/Repositories.tsx b/gui/frontend/src/pages/Repositories.tsx index fd5c0cd..bdbfd20 100644 --- a/gui/frontend/src/pages/Repositories.tsx +++ b/gui/frontend/src/pages/Repositories.tsx @@ -28,8 +28,9 @@ import { validateSkills, type AgentTarget, } from "@/lib/skell"; -import type { DiagnosticEntry, StatusEntry, SkillValidationResult } from "@/lib/types"; +import type { DiagnosticEntry, StatusEntry, SkillValidationResult, InstalledSkill } from "@/lib/types"; import { ConfirmDialog } from "@/components/ConfirmDialog"; +import { buildProjectRoute } from "@/lib/navigation"; interface RepoHealth { total: number; @@ -56,29 +57,40 @@ export function Repositories() { const loadHealth = useCallback(async () => { setLoading(true); - const entries = await Promise.all( - repos.map(async (repo) => { - const [skills, statuses, issues, inited] = await Promise.all([ - listInstalled(repo).catch(() => []), - getStatus(repo).catch(() => [] as StatusEntry[]), - doctorCheck(repo).catch(() => [] as DiagnosticEntry[]), - isRepoInitialized(repo).catch(() => false), - ]); - return [ - repo, - { + const healthMap: Record = {}; + const initMap: Record = {}; + + // Fetch repos sequentially in batches of 2 to avoid overwhelming the system + // with too many concurrent subprocess spawns. + const BATCH = 2; + for (let i = 0; i < repos.length; i += BATCH) { + const batch = repos.slice(i, i + BATCH); + const results = await Promise.all( + batch.map(async (repo) => { + const [skills, statuses, issues, inited] = await Promise.all([ + listInstalled(repo).catch(() => [] as InstalledSkill[]), + getStatus(repo).catch(() => [] as StatusEntry[]), + doctorCheck(repo).catch(() => [] as DiagnosticEntry[]), + isRepoInitialized(repo).catch(() => false), + ]); + return { + repo, health: { total: skills.length, - outdated: statuses.filter((s) => s.status === "outdated").length, - errors: issues.filter((d) => d.severity === "error").length, + outdated: statuses.filter((s: StatusEntry) => s.status === "outdated").length, + errors: issues.filter((d: DiagnosticEntry) => d.severity === "error").length, }, inited, - }, - ] as [string, { health: RepoHealth; inited: boolean }]; - }) - ); - setHealth(Object.fromEntries(entries.map(([r, v]) => [r, v.health]))); - setInitialized(Object.fromEntries(entries.map(([r, v]) => [r, v.inited]))); + }; + }) + ); + for (const { repo, health, inited } of results) { + healthMap[repo] = health; + initMap[repo] = inited; + } + } + setHealth(healthMap); + setInitialized(initMap); setLoading(false); }, [repos]); @@ -131,7 +143,7 @@ export function Repositories() { function handleSelectAndNavigate(repo: string) { setSelectedRepo(repo); - navigate("/skills"); + navigate(buildProjectRoute(repo, "/skills")); } async function handleValidate(repo: string) { @@ -190,11 +202,11 @@ export function Repositories() {
@@ -229,7 +241,12 @@ export function Repositories() {
-

{name}

+ {h && } {inited === false && ( @@ -242,7 +259,13 @@ export function Repositories() { )}
-

{repo}

+
{h && (
diff --git a/gui/frontend/src/pages/Settings.tsx b/gui/frontend/src/pages/Settings.tsx index 1d69957..5d0049d 100644 --- a/gui/frontend/src/pages/Settings.tsx +++ b/gui/frontend/src/pages/Settings.tsx @@ -51,8 +51,8 @@ export function Settings() { try { const list = await listSkillSources(); setSources(Array.isArray(list) ? list : []); - } catch (e: any) { - notify({ kind: "error", title: "Failed to load sources", detail: e?.message || String(e) }); + } catch (e: unknown) { + notify({ kind: "error", title: "Failed to load sources", detail: e instanceof Error ? e.message : String(e) }); } finally { setLoadingSources(false); } @@ -79,8 +79,8 @@ export function Settings() { setNewAlias(""); setNewURL(""); await loadSources(); - } catch (e: any) { - notify({ kind: "error", title: "Failed to add source", detail: e?.message || String(e) }); + } catch (e: unknown) { + notify({ kind: "error", title: "Failed to add source", detail: e instanceof Error ? e.message : String(e) }); } finally { setAdding(false); } @@ -92,8 +92,8 @@ export function Settings() { await removeSkillSource(alias); notify({ kind: "success", title: `Removed source "${alias}"` }); await loadSources(); - } catch (e: any) { - notify({ kind: "error", title: "Failed to remove source", detail: e?.message || String(e) }); + } catch (e: unknown) { + notify({ kind: "error", title: "Failed to remove source", detail: e instanceof Error ? e.message : String(e) }); } } diff --git a/gui/frontend/src/pages/SkillDetail.tsx b/gui/frontend/src/pages/SkillDetail.tsx index 65b5744..b299b52 100644 --- a/gui/frontend/src/pages/SkillDetail.tsx +++ b/gui/frontend/src/pages/SkillDetail.tsx @@ -1,5 +1,5 @@ import { lazy, Suspense, useEffect, useState, useCallback } from "react"; -import { useParams, useLocation, useNavigate } from "react-router-dom"; +import { useParams, useLocation, useNavigate, Link } from "react-router-dom"; import { ArrowLeft, Package, @@ -32,6 +32,7 @@ import { SkillBadge, LifecycleBadge } from "@/components/Badges"; import { MarkdownViewer } from "@/components/MarkdownViewer"; import { ValidationReport } from "@/components/ValidationReport"; import { ConfirmDialog } from "@/components/ConfirmDialog"; +import { createProjectId } from "@/lib/navigation"; const CodeViewer = lazy(async () => { const mod = await import("@/components/CodeViewer"); @@ -47,7 +48,8 @@ export function SkillDetail() { const { selectedRepo } = useRepoStore(); const { notify } = useUIStore(); - const repo = (location.state as { repo?: string })?.repo ?? selectedRepo; + const state = location.state as { repo?: string; from?: string; breadcrumb?: string } | undefined; + const repo = state?.repo ?? selectedRepo; const decoded = decodeURIComponent(skillName ?? ""); const [info, setInfo] = useState(null); @@ -105,6 +107,8 @@ export function SkillDetail() { ); if (md) void selectFile(md); } + // Only auto-load SKILL.md when tab changes to readme or files list updates. + // selectFile and fileContent are intentionally excluded from deps. // eslint-disable-next-line react-hooks/exhaustive-deps }, [files, tab]); @@ -211,6 +215,17 @@ export function SkillDetail() { const status = info?.lock?.pinned ? "pinned" : info?.status ?? "unknown"; const isOutdated = info?.status === "outdated"; const isPinned = !!info?.lock?.pinned; + const backTarget = state?.from === "catalog" + ? "/catalog" + : state?.from === "project" && repo && repo !== "global" + ? `/projects/${createProjectId(repo)}/skills` + : state?.from === "health" + ? "/projects" + : state?.from === "activity" + ? "/activity" + : repo && repo !== "global" + ? `/projects/${createProjectId(repo)}/skills` + : "/projects"; const skillMd = files.find( (f) => f.name.toLowerCase() === "skill.md" || f.name.toLowerCase() === "readme.md" @@ -218,11 +233,24 @@ export function SkillDetail() { return (
- {/* Back */} - + {/* Back + breadcrumb */} +
+ + {repo && repo !== "global" && ( +
+ Projects + + + {repo.split(/[\\/]/).filter(Boolean).pop() ?? repo} + + + {decoded} +
+ )} +
{loading ? (
@@ -330,7 +358,7 @@ export function SkillDetail() { void runValidation(); } }} - className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${ + className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors cursor-pointer ${ tab === t ? "border-brand-500 text-brand-400" : "border-transparent text-slate-500 hover:text-slate-300" @@ -572,7 +600,7 @@ function FileTreeEntry({ ? "bg-brand-600/20 text-brand-400" : entry.is_dir ? "text-slate-400 hover:text-slate-200 hover:bg-white/5 cursor-pointer" - : "text-slate-400 hover:text-slate-200 hover:bg-white/5" + : "text-slate-400 hover:text-slate-200 hover:bg-white/5 cursor-pointer" }`} > {isLoadingThis ? ( diff --git a/gui/frontend/src/pages/Sync.tsx b/gui/frontend/src/pages/Sync.tsx deleted file mode 100644 index aca31f2..0000000 --- a/gui/frontend/src/pages/Sync.tsx +++ /dev/null @@ -1,333 +0,0 @@ -import { useEffect, useState, useCallback, useMemo } from "react"; -import { - RefreshCw, - CheckCircle2, - AlertTriangle, - Plus, - Minus, - Play, - Clock, - Zap, -} from "lucide-react"; -import { useRepoStore, useUIStore } from "@/store"; -import { syncRepo } from "@/lib/skell"; -import type { SyncReport } from "@/lib/types"; - -interface RepoSyncState { - repo: string; - name: string; - report: SyncReport | null; - /** True when `report` came from a dry-run, false when it came from an actual apply. */ - reportIsDryRun: boolean | null; - error: string | null; - loading: boolean; - dryRunReport: SyncReport | null; -} - -export function Sync() { - const { repos, selectedRepo } = useRepoStore(); - const { notify } = useUIStore(); - - const targets = useMemo( - () => - selectedRepo && selectedRepo !== "global" ? [selectedRepo] : repos, - [selectedRepo, repos] - ); - - const [states, setStates] = useState>({}); - const [dryRun, setDryRun] = useState(true); - const [running, setRunning] = useState(false); - - const initStates = useCallback(() => { - const initial: Record = {}; - for (const repo of targets) { - initial[repo] = { - repo, - name: repo.split(/[/\\]/).at(-1) ?? repo, - report: null, - reportIsDryRun: null, - error: null, - loading: false, - dryRunReport: null, - }; - } - setStates(initial); - }, [targets]); - - useEffect(() => { - initStates(); - }, [initStates]); - - async function runSync(repoPath: string, dry: boolean) { - setStates((s) => ({ - ...s, - [repoPath]: { ...s[repoPath], loading: true, error: null }, - })); - try { - const report = await syncRepo({ repo: repoPath, dryRun: dry }); - setStates((s) => ({ - ...s, - [repoPath]: { - ...s[repoPath], - loading: false, - report: dry ? s[repoPath].report : report, - reportIsDryRun: dry ? s[repoPath].reportIsDryRun : false, - // After a real apply, also refresh the preview slot so the card - // shows the correct post-apply state regardless of the dry-run checkbox. - dryRunReport: dry ? report : report, - error: null, - }, - })); - } catch (e) { - setStates((s) => ({ - ...s, - [repoPath]: { - ...s[repoPath], - loading: false, - error: String(e), - }, - })); - } - } - - async function runAll() { - setRunning(true); - for (const repo of targets) { - await runSync(repo, dryRun); - } - setRunning(false); - notify({ - kind: dryRun ? "info" : "success", - title: dryRun ? "Dry-run complete" : "Sync complete", - detail: `${targets.length} repo${targets.length !== 1 ? "s" : ""} processed`, - }); - } - - return ( -
- {/* Header */} -
-
-

Sync

-

- Reconcile installed skills with skell.toml -

-
-
- - -
-
- - {targets.length === 0 ? ( -
- No projects selected. Add a project first. -
- ) : ( -
- {targets.map((repo) => { - const state = states[repo]; - if (!state) return null; - const report = dryRun ? state.dryRunReport : state.report; - const inSync = - report !== null && - report.installed.length === 0 && - report.removed.length === 0; - const hasPendingChanges = report !== null && !inSync; - const totalChanges = report - ? report.installed.length + report.removed.length - : 0; - - // Status badge shown in card header - let statusBadge: React.ReactNode = null; - if (state.loading) { - statusBadge = ( - -
- Checkingโ€ฆ - - ); - } else if (state.error) { - statusBadge = ( - - - Error - - ); - } else if (report === null) { - statusBadge = ( - - - Not checked - - ); - } else if (inSync && state.reportIsDryRun === false) { - statusBadge = ( - - - Applied - - ); - } else if (inSync) { - statusBadge = ( - - - Up to date - - ); - } else { - statusBadge = ( - - - {totalChanges} change{totalChanges !== 1 ? "s" : ""} pending - - ); - } - - return ( -
-
-
-
-

{state.name}

-

{repo}

-
- {statusBadge} -
- -
- - {state.error && ( -
- - {state.error} -
- )} - - {!state.loading && !state.error && report === null && ( -
- - Press “{dryRun ? "Preview" : "Sync"}” to check this repo. -
- )} - - {!state.loading && !state.error && report !== null && ( - <> - {/* After a real apply with no remaining changes */} - {inSync && state.reportIsDryRun === false && ( -
- -
-

Sync applied successfully

-

All skills are now up to date with skell.toml

-
-
- )} - - {/* Preview or real-apply: already in sync */} - {inSync && state.reportIsDryRun !== false && ( -
- -
-

Already up to date

-

No changes needed โ€” installed skills match skell.toml

-
-
- )} - - {/* Pending changes */} - {hasPendingChanges && ( -
- {/* Context banner */} -
-
- - - {dryRun - ? `Preview โ€” ${totalChanges} change${totalChanges !== 1 ? "s" : ""} would be applied` - : `${totalChanges} change${totalChanges !== 1 ? "s" : ""} applied`} - -
- {dryRun && ( - - )} -
- - {report.installed.length > 0 && ( -
-

- - {dryRun ? "Will install" : "Installed"} ({report.installed.length}) -

-
    - {report.installed.map((sk) => ( -
  • - - {sk} -
  • - ))} -
-
- )} - {report.removed.length > 0 && ( -
-

- - {dryRun ? "Will remove" : "Removed"} ({report.removed.length}) -

-
    - {report.removed.map((sk) => ( -
  • - - {sk} -
  • - ))} -
-
- )} -
- )} - - )} -
- ); - })} -
- )} -
- ); -} diff --git a/gui/frontend/src/pages/projects/ProjectActivityPage.tsx b/gui/frontend/src/pages/projects/ProjectActivityPage.tsx new file mode 100644 index 0000000..8f8db7e --- /dev/null +++ b/gui/frontend/src/pages/projects/ProjectActivityPage.tsx @@ -0,0 +1,62 @@ +import { useEffect, useMemo, useState } from "react"; +import { useParams } from "react-router-dom"; +import { Activity, ArrowUpRight } from "lucide-react"; +import { useRepoStore } from "@/store"; +import { getProjectDisplayName } from "@/lib/navigation"; +import { getStatus } from "@/lib/skell"; +import type { StatusEntry } from "@/lib/types"; + +export function ProjectActivityPage() { + const { projectId: _projectId } = useParams(); + const { repos, selectedRepo } = useRepoStore(); + + const projectPath = useMemo(() => { + if (selectedRepo && selectedRepo !== "global") return selectedRepo; + return repos[0] ?? ""; + }, [repos, selectedRepo]); + + const [events, setEvents] = useState([]); + + useEffect(() => { + async function loadActivity() { + if (!projectPath) return; + const entries = await getStatus(projectPath).catch(() => [] as StatusEntry[]); + setEvents(entries); + } + + void loadActivity(); + }, [projectPath]); + + return ( +
+
+

Activity for {getProjectDisplayName(projectPath)}

+

Recent skill state and update status for this project.

+
+ +
+ {events.length === 0 ? ( +
No activity recorded yet.
+ ) : ( + events.map((entry) => ( +
+
+
+ +
+
+

{entry.name}

+

{entry.installed} โ†’ {entry.latest}

+
+
+
+ {entry.status} + +
+
+ )) + )} +
+
+ ); +} diff --git a/gui/frontend/src/pages/projects/ProjectAgentsPage.tsx b/gui/frontend/src/pages/projects/ProjectAgentsPage.tsx new file mode 100644 index 0000000..133d34d --- /dev/null +++ b/gui/frontend/src/pages/projects/ProjectAgentsPage.tsx @@ -0,0 +1,54 @@ +import { useEffect, useMemo, useState } from "react"; +import { useParams } from "react-router-dom"; +import { useRepoStore } from "@/store"; +import { getProjectDisplayName } from "@/lib/navigation"; +import { detectRepoTargets, listSupportedTargets } from "@/lib/skell"; +import type { AgentTarget } from "@/lib/skell"; + +export function ProjectAgentsPage() { + const { projectId: _projectId } = useParams(); + const { repos, selectedRepo } = useRepoStore(); + + const projectPath = useMemo(() => { + if (selectedRepo && selectedRepo !== "global") return selectedRepo; + return repos[0] ?? ""; + }, [repos, selectedRepo]); + + const [targets, setTargets] = useState([]); + + useEffect(() => { + async function loadTargets() { + if (!projectPath) return; + const [detected, supported] = await Promise.all([detectRepoTargets(projectPath), listSupportedTargets()]); + const combined = [...detected, ...supported.filter((item) => !detected.some((target) => target.id === item.id))]; + setTargets(combined); + } + + void loadTargets(); + }, [projectPath]); + + return ( +
+
+

Agents for {getProjectDisplayName(projectPath)}

+

Detected and supported agent targets for this project.

+
+ +
+ {targets.map((target) => ( +
+
+

{target.displayName}

+

{target.dir}/skills/

+
+ {target.detected ? ( + detected + ) : ( + available + )} +
+ ))} +
+
+ ); +} diff --git a/gui/frontend/src/pages/projects/ProjectHealthPage.tsx b/gui/frontend/src/pages/projects/ProjectHealthPage.tsx new file mode 100644 index 0000000..8f9937b --- /dev/null +++ b/gui/frontend/src/pages/projects/ProjectHealthPage.tsx @@ -0,0 +1,70 @@ +import { useEffect, useMemo, useState } from "react"; +import { useParams } from "react-router-dom"; +import { AlertTriangle, CheckCircle2, ShieldCheck } from "lucide-react"; +import { useRepoStore } from "@/store"; +import { getProjectDisplayName } from "@/lib/navigation"; +import { doctorCheck, validateSkills } from "@/lib/skell"; +import type { DiagnosticEntry, SkillValidationResult } from "@/lib/types"; + +export function ProjectHealthPage() { + const { projectId: _projectId } = useParams(); + const { repos, selectedRepo } = useRepoStore(); + + const projectPath = useMemo(() => { + if (selectedRepo && selectedRepo !== "global") return selectedRepo; + return repos[0] ?? ""; + }, [repos, selectedRepo]); + + const [issues, setIssues] = useState([]); + const [validations, setValidations] = useState([]); + + useEffect(() => { + async function loadHealth() { + if (!projectPath) return; + const [diagnostics, validationResults] = await Promise.all([ + doctorCheck(projectPath).catch(() => [] as DiagnosticEntry[]), + validateSkills(projectPath, "", false).catch(() => [] as SkillValidationResult[]), + ]); + setIssues(diagnostics); + setValidations(validationResults); + } + + void loadHealth(); + }, [projectPath]); + + const errors = issues.filter((issue) => issue.severity === "error").length; + const warnings = issues.filter((issue) => issue.severity === "warning").length; + + return ( +
+
+

Health for {getProjectDisplayName(projectPath)}

+

Validation results and doctor findings for this project.

+
+ +
+ 0 ? "text-red-400" : "text-emerald-400"} /> +
+

{validations.length} skills checked

+

{errors} error{errors !== 1 ? "s" : ""} and {warnings} warning{warnings !== 1 ? "s" : ""}

+
+
+ +
+ {issues.length === 0 ? ( +
No issues reported.
+ ) : ( + issues.map((issue, index) => ( +
+ {issue.severity === "error" ? : } +
+

{issue.code}

+

{issue.message}

+
+
+ )) + )} +
+
+ ); +} diff --git a/gui/frontend/src/pages/projects/ProjectOverview.tsx b/gui/frontend/src/pages/projects/ProjectOverview.tsx new file mode 100644 index 0000000..83419dc --- /dev/null +++ b/gui/frontend/src/pages/projects/ProjectOverview.tsx @@ -0,0 +1,145 @@ +import { useEffect, useMemo, useState } from "react"; +import { Link, useParams } from "react-router-dom"; +import { useRepoStore } from "@/store"; +import { createProjectId, getProjectDisplayName } from "@/lib/navigation"; +import { getStatus, listInstalled, doctorCheck, detectRepoTargets, isRepoInitialized } from "@/lib/skell"; +import type { StatusEntry, DiagnosticEntry, InstalledSkill } from "@/lib/types"; +import { FolderKanban, Package, ArrowUp, AlertTriangle, CheckCircle2, ShieldCheck } from "lucide-react"; +import type { AgentTarget } from "@/lib/skell"; + +export function ProjectOverview() { + const { projectId } = useParams(); + const { repos, selectedRepo } = useRepoStore(); + + const projectPath = useMemo(() => { + if (selectedRepo && selectedRepo !== "global") return selectedRepo; + return repos[0] ?? ""; + }, [repos, selectedRepo]); + + const projectName = getProjectDisplayName(projectPath); + const routeId = projectId ?? createProjectId(projectPath); + + const [skills, setSkills] = useState([]); + const [statuses, setStatuses] = useState([]); + const [issues, setIssues] = useState([]); + const [targets, setTargets] = useState([]); + const [inited, setInited] = useState(null); + + useEffect(() => { + if (!projectPath) return; + void Promise.all([ + listInstalled(projectPath).catch(() => [] as InstalledSkill[]), + getStatus(projectPath).catch(() => [] as StatusEntry[]), + doctorCheck(projectPath).catch(() => [] as DiagnosticEntry[]), + detectRepoTargets(projectPath).catch(() => [] as AgentTarget[]), + isRepoInitialized(projectPath).catch(() => false), + ]).then(([sk, st, diag, tgt, init]) => { + setSkills(sk); + setStatuses(st); + setIssues(diag); + setTargets(tgt); + setInited(init); + }); + }, [projectPath]); + + const outdated = statuses.filter((s) => s.status === "outdated").length; + const errors = issues.filter((d) => d.severity === "error").length; + const warnings = issues.filter((d) => d.severity === "warning").length; + const detectedTarget = targets.find((t) => t.detected); + + return ( +
+ {/* Header */} +
+
+
+

{projectName}

+

{projectPath || "Select a project to view its details."}

+
+ + Manage Skills + +
+
+ + {/* Stats grid */} +
+
+
+ +
+
+

{skills.length}

+

Skills installed

+
+
+
+
+ +
+
+

{outdated}

+

Outdated

+
+
+
+
0 ? "bg-red-500/15 text-red-400" : "bg-emerald-500/15 text-emerald-400"}`}> + {errors > 0 ? : } +
+
+

{errors}

+

Issues ({warnings} warnings)

+
+
+
+ + {/* Agent target & init status */} +
+
+
+ +
+
+

+ {detectedTarget + ? `Initialized for ${detectedTarget.displayName}` + : inited === false + ? "Not initialized" + : "Agent targets"} +

+

+ {detectedTarget + ? `Skills directory: ${detectedTarget.dir}/skills/` + : targets.length > 0 + ? `${targets.filter((t) => t.detected).length} of ${targets.length} targets detected` + : "No agent targets detected"} +

+
+ + View agents + +
+
+ + {/* Quick links */} +
+ + +

Skills

+

View and manage installed skills

+ + + +

Health

+

Validation and doctor diagnostics

+ + + +

Activity

+

Status and update history

+ +
+
+ ); +} + diff --git a/gui/frontend/src/pages/projects/ProjectSkillsPage.tsx b/gui/frontend/src/pages/projects/ProjectSkillsPage.tsx new file mode 100644 index 0000000..fbe1e09 --- /dev/null +++ b/gui/frontend/src/pages/projects/ProjectSkillsPage.tsx @@ -0,0 +1,208 @@ +import { useEffect, useMemo, useState, useCallback } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { AlertTriangle, FilePlus, RefreshCw, Search, Terminal, Library, GitBranchPlus, PackageOpen } from "lucide-react"; +import { useRepoStore, useUIStore } from "@/store"; +import { getProjectDisplayName } from "@/lib/navigation"; +import { getStatus, initRepo, isRepoInitialized, listInstalled, skellPresent } from "@/lib/skell"; +import type { InstalledSkill, StatusEntry } from "@/lib/types"; +import { SkillBadge, ScopeBadge } from "@/components/Badges"; +import { ProjectPageHeader } from "@/components/ProjectPageHeader"; +import { AddSkillButton } from "@/components/AddSkillButton"; + +export function ProjectSkillsPage() { + const { projectId: _projectId } = useParams(); + const navigate = useNavigate(); + const { repos, selectedRepo } = useRepoStore(); + const { notify } = useUIStore(); + + const projectPath = useMemo(() => { + if (selectedRepo && selectedRepo !== "global") return selectedRepo; + return repos[0] ?? ""; + }, [repos, selectedRepo]); + + const [skills, setSkills] = useState([]); + const [statuses, setStatuses] = useState([]); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(""); + const [repoInited, setRepoInited] = useState(null); + const [initRunning, setInitRunning] = useState(false); + const [skellMissing, setSkellMissing] = useState(false); + + const loadSkills = useCallback(async () => { + if (!projectPath) { + setSkills([]); + setStatuses([]); + setLoading(false); + return; + } + + setLoading(true); + try { + const [installed, statusEntries] = await Promise.all([ + listInstalled(projectPath).catch(() => [] as InstalledSkill[]), + getStatus(projectPath).catch(() => [] as StatusEntry[]), + ]); + setSkills(installed); + setStatuses(statusEntries); + } finally { + setLoading(false); + } + }, [projectPath]); + + useEffect(() => { + void loadSkills(); + }, [loadSkills]); + + useEffect(() => { + if (!projectPath) return; + setRepoInited(null); + isRepoInitialized(projectPath) + .then(setRepoInited) + .catch(() => setRepoInited(false)); + }, [projectPath]); + + useEffect(() => { + skellPresent() + .then((present) => { + if (!present) setSkellMissing(true); + }) + .catch(() => {}); + }, []); + + async function handleInitHere() { + if (!projectPath) return; + setInitRunning(true); + try { + const result = await initRepo(projectPath); + if (result.success) { + notify({ kind: "success", title: "Project initialized", detail: result.stdout.trim() }); + setRepoInited(true); + void loadSkills(); + } else { + notify({ kind: "error", title: "Init failed", detail: result.stderr }); + } + } finally { + setInitRunning(false); + } + } + + const filtered = useMemo(() => { + const q = search.toLowerCase(); + if (!q) return skills; + return skills.filter((skill) => skill.name.toLowerCase().includes(q) || skill.registry.toLowerCase().includes(q)); + }, [search, skills]); + + const pageSubtitle = projectPath + ? `Skills for ${getProjectDisplayName(projectPath)}.` + : "Select a project to manage its skills."; + + return ( +
+ + void loadSkills()} /> + + + } + /> + + {repoInited === false && ( +
+ +

This project has not been initialized yet. Initialize it to install skills.

+ +
+ )} + + {skellMissing && ( +
+
+ +
+

Skell CLI not found

+

Install the Skell CLI to manage and list skills.

+
+ +
+
+ )} + +
+
+ + setSearch(e.target.value)} /> +
+
+ + {loading ? ( +
+
+
+ ) : filtered.length === 0 ? ( +
+
+ +
+

No skills installed yet

+

+ Add skills to give your coding agents reusable instructions, workflows, and supporting resources for this project. +

+
+ + +
+
+ ) : ( +
+ + + + + + + + + + + + {filtered.map((skill) => { + const statusEntry = statuses.find((item) => item.name === skill.name); + const status = statusEntry?.status ?? "unknown"; + return ( + + + + + + + + ); + })} + +
SkillVersionStatusRegistryScope
+ + {skill.version || "โ€”"}{skill.registry || "โ€”"}
+
+ )} +
+ ); +} diff --git a/gui/frontend/src/router.tsx b/gui/frontend/src/router.tsx index e726794..8bd65fc 100644 --- a/gui/frontend/src/router.tsx +++ b/gui/frontend/src/router.tsx @@ -9,64 +9,64 @@ const router = createBrowserRouter([ { index: true, lazy: async () => { - const mod = await import("./pages/Dashboard"); - return { Component: mod.Dashboard }; + const mod = await import("./pages/Home"); + return { Component: mod.Home }; }, }, { - path: "repositories", + path: "projects", lazy: async () => { const mod = await import("./pages/Repositories"); return { Component: mod.Repositories }; }, }, { - path: "skills", + path: "projects/:projectId", lazy: async () => { - const mod = await import("./pages/InstalledSkills"); - return { Component: mod.InstalledSkills }; + const mod = await import("./pages/projects/ProjectOverview"); + return { Component: mod.ProjectOverview }; }, }, { - path: "skills/:skillName", + path: "projects/:projectId/skills", lazy: async () => { - const mod = await import("./pages/SkillDetail"); - return { Component: mod.SkillDetail }; + const mod = await import("./pages/projects/ProjectSkillsPage"); + return { Component: mod.ProjectSkillsPage }; }, }, { - path: "registry", + path: "projects/:projectId/agents", lazy: async () => { - const mod = await import("./pages/Registry"); - return { Component: mod.Registry }; + const mod = await import("./pages/projects/ProjectAgentsPage"); + return { Component: mod.ProjectAgentsPage }; }, }, { - path: "sync", + path: "projects/:projectId/health", lazy: async () => { - const mod = await import("./pages/Sync"); - return { Component: mod.Sync }; + const mod = await import("./pages/projects/ProjectHealthPage"); + return { Component: mod.ProjectHealthPage }; }, }, { - path: "doctor", + path: "projects/:projectId/activity", lazy: async () => { - const mod = await import("./pages/Doctor"); - return { Component: mod.Doctor }; + const mod = await import("./pages/projects/ProjectActivityPage"); + return { Component: mod.ProjectActivityPage }; }, }, { - path: "cache", + path: "catalog", lazy: async () => { - const mod = await import("./pages/Cache"); - return { Component: mod.Cache }; + const mod = await import("./pages/Catalog"); + return { Component: mod.Catalog }; }, }, { - path: "audit", + path: "activity", lazy: async () => { - const mod = await import("./pages/AuditLog"); - return { Component: mod.AuditLog }; + const mod = await import("./pages/Activity"); + return { Component: mod.Activity }; }, }, { @@ -76,6 +76,41 @@ const router = createBrowserRouter([ return { Component: mod.Settings }; }, }, + { + path: "repositories", + element: , + }, + { + path: "registry", + element: , + }, + { + path: "audit", + element: , + }, + { + path: "skills", + element: , + }, + { + path: "skills/:skillName", + lazy: async () => { + const mod = await import("./pages/SkillDetail"); + return { Component: mod.SkillDetail }; + }, + }, + { + path: "sync", + element: , + }, + { + path: "doctor", + element: , + }, + { + path: "cache", + element: , + }, { path: "contribute-info", lazy: async () => { diff --git a/gui/frontend/src/test/pages/AuditLog.test.tsx b/gui/frontend/src/test/pages/AuditLog.test.tsx deleted file mode 100644 index c25bf8a..0000000 --- a/gui/frontend/src/test/pages/AuditLog.test.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { screen, waitFor, fireEvent } from "@testing-library/react"; -import { renderWithRouter } from "@/test/utils"; -import { AuditLog } from "@/pages/AuditLog"; -import * as skell from "@/lib/skell"; -import { mockAuditEntry } from "@/test/fixtures"; - -vi.mock("@/lib/skell"); - -const mockSkell = skell as unknown as Record>; - -beforeEach(() => { - mockSkell.readAuditLog.mockResolvedValue([]); - mockSkell.skellPresent = vi.fn().mockResolvedValue(true); -}); - -describe("AuditLog", () => { - it("renders heading", () => { - renderWithRouter(); - expect(screen.getByText(/audit log/i)).toBeTruthy(); - }); - - it("shows refresh button", async () => { - renderWithRouter(); - // The header refresh button is icon-only (no accessible name). - // Wait for loading to finish and confirm at least one button exists. - await waitFor(() => { - expect(screen.getAllByRole("button").length).toBeGreaterThan(0); - }); - }); - - it("shows empty state when no entries", async () => { - renderWithRouter(); - await waitFor(() => { - expect(screen.getByText(/no audit log entries/i)).toBeTruthy(); - }); - }); - - it("displays audit entries after load", async () => { - mockSkell.readAuditLog.mockResolvedValue([ - mockAuditEntry({ skill: "audit-skill", action: "install" }), - ]); - renderWithRouter(); - await waitFor(() => { - expect(screen.getByText("audit-skill")).toBeTruthy(); - }); - }); - - it("filters entries by search query", async () => { - mockSkell.readAuditLog.mockResolvedValue([ - mockAuditEntry({ skill: "alpha-skill", action: "install" }), - mockAuditEntry({ skill: "beta-skill", action: "remove" }), - ]); - renderWithRouter(); - await waitFor(() => screen.getByText("alpha-skill")); - - const input = screen.getByPlaceholderText(/search/i); - fireEvent.change(input, { target: { value: "alpha" } }); - - await waitFor(() => { - expect(screen.queryByText("beta-skill")).toBeNull(); - expect(screen.getByText("alpha-skill")).toBeTruthy(); - }); - }); - - it("filters entries by action", async () => { - mockSkell.readAuditLog.mockResolvedValue([ - mockAuditEntry({ skill: "s1", action: "install" }), - mockAuditEntry({ skill: "s2", action: "remove" }), - ]); - renderWithRouter(); - await waitFor(() => screen.getByText("s1")); - - const selects = document.querySelectorAll("select"); - expect(selects.length).toBeGreaterThan(0); - fireEvent.change(selects[0], { target: { value: "install" } }); - - await waitFor(() => { - expect(screen.queryByText("s2")).toBeNull(); - expect(screen.getByText("s1")).toBeTruthy(); - }); - }); - - it("reloads on refresh button click", async () => { - renderWithRouter(); - // Wait for initial load to complete - await waitFor(() => expect(mockSkell.readAuditLog).toHaveBeenCalledTimes(1)); - // Icon-only button - get the first (and only) button in the heading area - const allBtns = screen.getAllByRole("button"); - fireEvent.click(allBtns[0]); - await waitFor(() => { - expect(mockSkell.readAuditLog).toHaveBeenCalledTimes(2); - }); - }); -}); diff --git a/gui/frontend/src/test/pages/Cache.test.tsx b/gui/frontend/src/test/pages/Cache.test.tsx deleted file mode 100644 index ab6ea0d..0000000 --- a/gui/frontend/src/test/pages/Cache.test.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { screen, waitFor, fireEvent } from "@testing-library/react"; -import { renderWithRouter } from "@/test/utils"; -import { Cache } from "@/pages/Cache"; -import * as skell from "@/lib/skell"; -import { mockOkResult, mockErrResult } from "@/test/fixtures"; -import { useUIStore } from "@/store"; - -vi.mock("@/lib/skell"); - -const mockSkell = skell as unknown as Record>; - -beforeEach(() => { - mockSkell.cacheStatus.mockResolvedValue(mockOkResult("Cache size: 42 MB\n3 registries")); - mockSkell.skellPresent = vi.fn().mockResolvedValue(true); - mockSkell.cacheRefresh.mockResolvedValue(mockOkResult("Cache refreshed")); - mockSkell.cacheClear.mockResolvedValue(mockOkResult()); - // Reset notification store between tests - useUIStore.setState({ notifications: [] }); -}); - -describe("Cache", () => { - it("renders heading", () => { - renderWithRouter(); - expect(screen.getByRole("heading", { level: 1, name: "Cache" })).toBeTruthy(); - }); - - it("loads and displays cache status on mount", async () => { - renderWithRouter(); - await waitFor(() => { - expect(screen.getByText(/cache size/i)).toBeTruthy(); - }); - }); - - it("calls cacheRefresh when Refresh button clicked", async () => { - renderWithRouter(); - await waitFor(() => screen.getByText(/cache size/i)); - - const btn = screen.getByRole("button", { name: /refresh/i }); - fireEvent.click(btn); - await waitFor(() => { - expect(mockSkell.cacheRefresh).toHaveBeenCalled(); - }); - }); - - it("shows error notification when cacheRefresh fails", async () => { - mockSkell.cacheRefresh.mockResolvedValue(mockErrResult("network error")); - renderWithRouter(); - await waitFor(() => screen.getByText(/cache size/i)); - - fireEvent.click(screen.getByRole("button", { name: /^Refresh$/i })); - await waitFor(() => { - const { notifications } = useUIStore.getState(); - expect(notifications.some((n) => /refresh failed/i.test(n.title))).toBe(true); - }); - }); - - it("shows confirmation dialog before clearing cache", async () => { - renderWithRouter(); - await waitFor(() => screen.getByText(/cache size/i)); - - const clearBtn = screen.getByRole("button", { name: /^Clear$/i }); - fireEvent.click(clearBtn); - await waitFor(() => { - // ConfirmDialog title appears - expect(screen.getByText("Clear cache?")).toBeTruthy(); - }); - }); - - it("calls cacheClear after confirmation", async () => { - renderWithRouter(); - await waitFor(() => screen.getByText(/cache size/i)); - - fireEvent.click(screen.getByRole("button", { name: /^Clear$/i })); - await waitFor(() => screen.getByText("Clear cache?")); - - // Click the confirm button inside the dialog ("Clear Cache") - const confirmBtn = screen.getByRole("button", { name: "Clear Cache" }); - fireEvent.click(confirmBtn); - await waitFor(() => { - expect(mockSkell.cacheClear).toHaveBeenCalled(); - }); - }); -}); diff --git a/gui/frontend/src/test/pages/Dashboard.test.tsx b/gui/frontend/src/test/pages/Dashboard.test.tsx deleted file mode 100644 index b229617..0000000 --- a/gui/frontend/src/test/pages/Dashboard.test.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { screen, waitFor } from "@testing-library/react"; -import { renderWithRouter } from "@/test/utils"; -import { Dashboard } from "@/pages/Dashboard"; -import * as skell from "@/lib/skell"; -import { - mockInstalledSkill, - mockStatusEntry, - mockDiagnosticEntry, -} from "@/test/fixtures"; - -vi.mock("@/lib/skell"); - -const mockSkell = skell as unknown as Record>; - -beforeEach(async () => { - mockSkell.listInstalledGlobal.mockResolvedValue([]); - mockSkell.skellPresent = vi.fn().mockResolvedValue(true); - mockSkell.listInstalled.mockResolvedValue([]); - mockSkell.getStatus.mockResolvedValue([]); - mockSkell.doctorCheck.mockResolvedValue([]); - const { useRepoStore } = await import("@/store"); - useRepoStore.setState({ repos: [], selectedRepo: "global" }); -}); - -describe("Dashboard", () => { - it("renders heading", async () => { - renderWithRouter(); - expect(screen.getByRole("heading", { level: 1, name: "Dashboard" })).toBeTruthy(); - }); - - it("shows skill count after load", async () => { - mockSkell.listInstalled.mockResolvedValue([mockInstalledSkill()]); - mockSkell.getStatus.mockResolvedValue([mockStatusEntry()]); - renderWithRouter(); - await waitFor(() => { - expect(screen.queryByText(/loading/i) ?? screen.getByText(/dashboard/i)).toBeTruthy(); - }); - }); - - it("shows outdated count when skills are outdated", async () => { - const { useRepoStore } = await import("@/store"); - useRepoStore.setState({ repos: ["/test-repo"], selectedRepo: "/test-repo" }); - mockSkell.listInstalled.mockResolvedValue([mockInstalledSkill(), mockInstalledSkill({ name: "b" })]); - mockSkell.getStatus.mockResolvedValue([ - mockStatusEntry({ name: "test-skill", status: "outdated" }), - mockStatusEntry({ name: "b", status: "up-to-date" }), - ]); - renderWithRouter(); - await waitFor(() => { - // StatCard renders the count as a

with the number value - const ones = screen.getAllByText("1"); - expect(ones.length).toBeGreaterThan(0); - }); - }); - - it("shows error badge when doctor detects errors", async () => { - const { useRepoStore } = await import("@/store"); - useRepoStore.setState({ repos: ["/test-repo"], selectedRepo: "/test-repo" }); - mockSkell.doctorCheck.mockResolvedValue([mockDiagnosticEntry()]); - renderWithRouter(); - await waitFor(() => { - const ones = screen.getAllByText("1"); - expect(ones.length).toBeGreaterThan(0); - }); - }); -}); diff --git a/gui/frontend/src/test/pages/Doctor.test.tsx b/gui/frontend/src/test/pages/Doctor.test.tsx deleted file mode 100644 index 253b3cc..0000000 --- a/gui/frontend/src/test/pages/Doctor.test.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { screen, waitFor, fireEvent } from "@testing-library/react"; -import { renderWithRouter } from "@/test/utils"; -import { Doctor } from "@/pages/Doctor"; -import * as skell from "@/lib/skell"; -import { mockDiagnosticEntry } from "@/test/fixtures"; - -vi.mock("@/lib/skell"); - -const mockSkell = skell as unknown as Record>; - -beforeEach(async () => { - mockSkell.doctorCheck.mockResolvedValue([]); - mockSkell.skellPresent = vi.fn().mockResolvedValue(true); - const { useRepoStore } = await import("@/store"); - // Use selectedRepo="global" so targets=repos (stable reference, avoids infinite re-render) - useRepoStore.setState({ repos: ["/repo"], selectedRepo: "global" }); -}); - -describe("Doctor", () => { - it("renders heading", () => { - renderWithRouter(); - expect(screen.getByRole("heading", { name: /doctor/i })).toBeTruthy(); - }); - - it("shows run button", () => { - renderWithRouter(); - expect(screen.getByRole("button", { name: /run/i })).toBeTruthy(); - }); - - it("calls doctorCheck on run all click", async () => { - renderWithRouter(); - const runBtn = screen.getByRole("button", { name: /run/i }); - fireEvent.click(runBtn); - await waitFor(() => { - expect(mockSkell.doctorCheck).toHaveBeenCalledWith("/repo"); - }); - }); - - it("shows issues when doctorCheck returns entries", async () => { - mockSkell.doctorCheck.mockResolvedValue([ - mockDiagnosticEntry({ severity: "error", message: "Missing version" }), - ]); - renderWithRouter(); - const runBtn = screen.getByRole("button", { name: /run/i }); - fireEvent.click(runBtn); - await waitFor(() => { - expect(screen.getByText(/missing version/i)).toBeTruthy(); - }); - }); - - it("shows warning entries correctly", async () => { - mockSkell.doctorCheck.mockResolvedValue([ - mockDiagnosticEntry({ severity: "warning", message: "Deprecated lifecycle" }), - ]); - renderWithRouter(); - const runBtn = screen.getByRole("button", { name: /run/i }); - fireEvent.click(runBtn); - await waitFor(() => { - expect(screen.getByText(/deprecated lifecycle/i)).toBeTruthy(); - }); - }); - - it("shows 'no issues' after clean run", async () => { - mockSkell.doctorCheck.mockResolvedValue([]); - renderWithRouter(); - const runBtn = screen.getByRole("button", { name: /run/i }); - fireEvent.click(runBtn); - await waitFor(() => { - expect(screen.getByText(/no issues/i)).toBeTruthy(); - }); - }); -}); diff --git a/gui/frontend/src/test/pages/Registry.test.tsx b/gui/frontend/src/test/pages/Registry.test.tsx deleted file mode 100644 index 1a3f13b..0000000 --- a/gui/frontend/src/test/pages/Registry.test.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { screen, waitFor, fireEvent } from "@testing-library/react"; -import { renderWithRouter } from "@/test/utils"; -import { Registry } from "@/pages/Registry"; -import * as skell from "@/lib/skell"; -import { mockRegistrySkill, mockOkResult } from "@/test/fixtures"; -import { useUIStore } from "@/store"; - -vi.mock("@/lib/skell"); - -const mockSkell = skell as unknown as Record>; - -beforeEach(() => { - mockSkell.searchSkills.mockResolvedValue([]); - mockSkell.skellPresent = vi.fn().mockResolvedValue(true); - mockSkell.installSkill.mockResolvedValue(mockOkResult()); - mockSkell.listInstalled.mockResolvedValue([]); - mockSkell.listInstalledGlobal.mockResolvedValue([]); - mockSkell.isRepoInitialized.mockResolvedValue(false); - useUIStore.setState({ notifications: [] }); -}); - -describe("Registry", () => { - it("renders search UI heading", () => { - renderWithRouter(); - expect(screen.getByText(/discover skills|registry/i)).toBeTruthy(); - }); - - it("calls searchSkills on mount", async () => { - renderWithRouter(); - await waitFor(() => { - expect(mockSkell.searchSkills).toHaveBeenCalled(); - }); - }); - - it("displays skills returned by searchSkills", async () => { - mockSkell.searchSkills.mockResolvedValue([ - mockRegistrySkill({ name: "my-skill" }), - ]); - renderWithRouter(); - await waitFor(() => { - expect(screen.getByText("my-skill")).toBeTruthy(); - }); - }); - - it("re-searches when lifecycle filter changes", async () => { - renderWithRouter(); - await waitFor(() => expect(mockSkell.searchSkills).toHaveBeenCalledTimes(1)); - - const selects = document.querySelectorAll("select"); - expect(selects.length).toBeGreaterThan(0); - fireEvent.change(selects[0], { target: { value: "stable" } }); - - await waitFor(() => { - expect(mockSkell.searchSkills).toHaveBeenCalledWith( - expect.objectContaining({ lifecycle: "stable" }) - ); - }); - }); - - it("shows 'no skills found' when search returns empty", async () => { - mockSkell.searchSkills.mockResolvedValue([]); - renderWithRouter(); - await waitFor(() => { - expect(screen.getByText(/no skills found/i)).toBeTruthy(); - }); - }); - - it("opens install dialog when install button clicked", async () => { - mockSkell.searchSkills.mockResolvedValue([mockRegistrySkill({ name: "click-skill" })]); - mockSkell.isRepoInitialized.mockResolvedValue(true); - renderWithRouter(); - await waitFor(() => screen.getByText("click-skill")); - - // The Install button is inside the SkillCard - const installBtns = screen.getAllByRole("button", { name: /install/i }); - fireEvent.click(installBtns[0]); - await waitFor(() => { - // Dialog title: Install "click-skill" - expect(screen.getByText(/install "click-skill"/i)).toBeTruthy(); - }); - }); - - it("marks already installed skills as installed", async () => { - mockSkell.searchSkills.mockResolvedValue([mockRegistrySkill({ name: "installed-skill" })]); - mockSkell.listInstalledGlobal.mockResolvedValue([ - { - name: "installed-skill", - version: "1.0.0", - registry: "default", - source_repo: "", - installed_path: "/tmp/installed-skill", - installed_at: "2026-01-01T00:00:00Z", - pinned: false, - content_hash: "hash", - }, - ]); - mockSkell.isRepoInitialized.mockResolvedValue(true); - - renderWithRouter(); - - const installedButton = await screen.findByRole("button", { name: /installed/i }); - expect(installedButton).toBeDisabled(); - expect(mockSkell.installSkill).not.toHaveBeenCalled(); - }); - - it("shows error notification when search fails", async () => { - mockSkell.searchSkills.mockRejectedValue(new Error("Network error")); - renderWithRouter(); - await waitFor(() => { - const { notifications } = useUIStore.getState(); - expect(notifications.some((n) => /search failed/i.test(n.title))).toBe(true); - }); - }); -}); diff --git a/gui/frontend/src/test/pages/Sync.test.tsx b/gui/frontend/src/test/pages/Sync.test.tsx deleted file mode 100644 index c370fdf..0000000 --- a/gui/frontend/src/test/pages/Sync.test.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { screen, waitFor, fireEvent } from "@testing-library/react"; -import { renderWithRouter } from "@/test/utils"; -import { Sync } from "@/pages/Sync"; -import * as skell from "@/lib/skell"; -import { mockSyncReport } from "@/test/fixtures"; - -vi.mock("@/lib/skell"); - -const mockSkell = skell as unknown as Record>; - -beforeEach(async () => { - mockSkell.syncRepo.mockResolvedValue(mockSyncReport()); - mockSkell.skellPresent = vi.fn().mockResolvedValue(true); - const { useRepoStore } = await import("@/store"); - useRepoStore.setState({ repos: ["/repo"], selectedRepo: "global" }); -}); - -describe("Sync", () => { - it("renders the sync page heading", () => { - renderWithRouter(); - expect(screen.getByRole("heading", { name: "Sync" })).toBeTruthy(); - }); - - it("renders repo name in sync card", async () => { - renderWithRouter(); - await waitFor(() => { - expect(screen.getByText("repo")).toBeTruthy(); - }); - }); - - it("dry-run toggle is present and defaults on", () => { - renderWithRouter(); - const toggle = screen.queryByRole("checkbox"); - if (toggle) { - expect((toggle as HTMLInputElement).checked).toBe(true); - } - }); - - it("shows 'Not checked' status badge before any run", async () => { - renderWithRouter(); - await waitFor(() => screen.getByText("repo")); - expect(screen.getByText("Not checked")).toBeTruthy(); - }); - - it("calls syncRepo when Preview Sync button clicked", async () => { - renderWithRouter(); - await waitFor(() => screen.getByText("repo")); - - const btn = screen.getByRole("button", { name: /preview sync/i }); - fireEvent.click(btn); - await waitFor(() => { - expect(mockSkell.syncRepo).toHaveBeenCalled(); - }); - }); - - it("shows 'Already up to date' when in sync", async () => { - mockSkell.syncRepo.mockResolvedValue(mockSyncReport({ installed: [], removed: [] })); - renderWithRouter(); - await waitFor(() => screen.getByText("repo")); - - fireEvent.click(screen.getByRole("button", { name: /preview sync/i })); - await waitFor(() => { - expect(screen.getByText("Already up to date")).toBeTruthy(); - }); - }); - - it("shows 'Up to date' status badge when in sync", async () => { - mockSkell.syncRepo.mockResolvedValue(mockSyncReport({ installed: [], removed: [] })); - renderWithRouter(); - await waitFor(() => screen.getByText("repo")); - - fireEvent.click(screen.getByRole("button", { name: /preview sync/i })); - await waitFor(() => { - expect(screen.getByText("Up to date")).toBeTruthy(); - }); - }); - - it("shows 'Will install' label and skill names from dry-run report", async () => { - mockSkell.syncRepo.mockResolvedValue( - mockSyncReport({ installed: ["skill-a", "skill-b"], removed: [] }) - ); - renderWithRouter(); - await waitFor(() => screen.getByText("repo")); - - fireEvent.click(screen.getByRole("button", { name: /preview sync/i })); - await waitFor(() => { - expect(screen.getByText("skill-a")).toBeTruthy(); - expect(screen.getByText("skill-b")).toBeTruthy(); - }); - expect(screen.getByText(/will install/i)).toBeTruthy(); - }); - - it("shows pending changes count in status badge", async () => { - mockSkell.syncRepo.mockResolvedValue( - mockSyncReport({ installed: ["skill-a"], removed: ["old-skill"] }) - ); - renderWithRouter(); - await waitFor(() => screen.getByText("repo")); - - fireEvent.click(screen.getByRole("button", { name: /preview sync/i })); - await waitFor(() => { - expect(screen.getByText(/2 changes pending/i)).toBeTruthy(); - }); - }); - - it("shows 'Apply now' button when dry-run finds pending changes", async () => { - mockSkell.syncRepo.mockResolvedValue( - mockSyncReport({ installed: ["skill-a"], removed: [] }) - ); - renderWithRouter(); - await waitFor(() => screen.getByText("repo")); - - fireEvent.click(screen.getByRole("button", { name: /preview sync/i })); - await waitFor(() => { - expect(screen.getByRole("button", { name: /apply now/i })).toBeTruthy(); - }); - }); - - it("shows preview context banner with change count", async () => { - mockSkell.syncRepo.mockResolvedValue( - mockSyncReport({ installed: ["skill-a"], removed: [] }) - ); - renderWithRouter(); - await waitFor(() => screen.getByText("repo")); - - fireEvent.click(screen.getByRole("button", { name: /preview sync/i })); - await waitFor(() => { - expect(screen.getByText(/preview.*1 change would be applied/i)).toBeTruthy(); - }); - }); - - it("shows sync error on failure", async () => { - mockSkell.syncRepo.mockRejectedValue(new Error("sync failed")); - renderWithRouter(); - await waitFor(() => screen.getByText("repo")); - - fireEvent.click(screen.getByRole("button", { name: /preview sync/i })); - await waitFor(() => { - expect(screen.getByText(/sync failed/i)).toBeTruthy(); - }); - }); - - it("shows 'Error' status badge on failure", async () => { - mockSkell.syncRepo.mockRejectedValue(new Error("network error")); - renderWithRouter(); - await waitFor(() => screen.getByText("repo")); - - fireEvent.click(screen.getByRole("button", { name: /preview sync/i })); - await waitFor(() => { - expect(screen.getByText("Error")).toBeTruthy(); - }); - }); -}); From fca3cb92e7833deddd8a4704287666f564bc84ac Mon Sep 17 00:00:00 2001 From: Amin Mesbahi Date: Sun, 19 Jul 2026 19:53:45 +0200 Subject: [PATCH 02/14] chore: expand gitignore to cover Wails v3 generated build dirs Exclude auto-generated Android/iOS/Docker/Linux build scaffolding and macOS binary assets from version control. --- .gitignore | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.gitignore b/.gitignore index 8464438..7f4f21b 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,16 @@ gui/frontend/.playwright/ # Wails build outputs gui/build/bin/ +gui/build/android/ +gui/build/appicon.icon/ +gui/build/docker/ +gui/build/ios/ +gui/build/linux/ +gui/build/darwin/Assets.car +gui/build/darwin/icons.icns +gui/build/windows/msix/ +gui/build/windows/nsis/ +gui/build/windows/icon.ico gui/frontend/wailsjs/ # Bun From 9c5b7cfccb08b1b8aaabdf2dcd58c0bf93c37f34 Mon Sep 17 00:00:00 2001 From: Amin Mesbahi Date: Sun, 19 Jul 2026 19:54:41 +0200 Subject: [PATCH 03/14] chore(gui): add remaining UI components and bindings Include Sidebar nav updates, store changes, Wails bindings, and supporting components (AddSkillButton, CurrentProjectSummary, RecentProjects, navigation helpers). --- .../wailsapp/wails/v3/internal/eventcreate.ts | 9 + .../wailsapp/wails/v3/internal/eventdata.d.ts | 2 + gui/frontend/bindings/skell-gui/app.ts | 171 +++++++++++++++ gui/frontend/bindings/skell-gui/index.ts | 21 ++ gui/frontend/bindings/skell-gui/models.ts | 128 ++++++++++++ .../src/components/AddSkillButton.tsx | 72 +++++++ .../src/components/CurrentProjectSummary.tsx | 46 +++++ .../src/components/RecentProjects.tsx | 78 +++++++ gui/frontend/src/components/Sidebar.tsx | 194 ++++++------------ gui/frontend/src/lib/navigation.ts | 36 ++++ gui/frontend/src/store/index.ts | 175 ++++++++++++---- gui/frontend/src/test/setup.ts | 57 +++++ 12 files changed, 820 insertions(+), 169 deletions(-) create mode 100644 gui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts create mode 100644 gui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts create mode 100644 gui/frontend/bindings/skell-gui/app.ts create mode 100644 gui/frontend/bindings/skell-gui/index.ts create mode 100644 gui/frontend/bindings/skell-gui/models.ts create mode 100644 gui/frontend/src/components/AddSkillButton.tsx create mode 100644 gui/frontend/src/components/CurrentProjectSummary.tsx create mode 100644 gui/frontend/src/components/RecentProjects.tsx create mode 100644 gui/frontend/src/lib/navigation.ts diff --git a/gui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts b/gui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts new file mode 100644 index 0000000..1ea1058 --- /dev/null +++ b/gui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts @@ -0,0 +1,9 @@ +//@ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH ร‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +Object.freeze($Create.Events); diff --git a/gui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts b/gui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts new file mode 100644 index 0000000..3dd1807 --- /dev/null +++ b/gui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts @@ -0,0 +1,2 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH ร‚ MODIWL +// This file is automatically generated. DO NOT EDIT diff --git a/gui/frontend/bindings/skell-gui/app.ts b/gui/frontend/bindings/skell-gui/app.ts new file mode 100644 index 0000000..18f4ee6 --- /dev/null +++ b/gui/frontend/bindings/skell-gui/app.ts @@ -0,0 +1,171 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH ร‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * App is the Wails application service. All exported methods are bound to the frontend. + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as $models from "./models.js"; + +/** + * ActiveTarget returns the id of the target currently in use for repoPath, or + * the empty string when none is detected. + */ +export function ActiveTarget(repoPath: string): $CancellablePromise { + return $Call.ByID(2394438090, repoPath); +} + +/** + * AddSkillSource adds or updates a global skill source (git URL or local folder). + */ +export function AddSkillSource(alias: string, url: string): $CancellablePromise { + return $Call.ByID(2803609340, alias, url); +} + +/** + * AuditLogPath returns the platform-correct path to ~/.skell/audit.log. + */ +export function AuditLogPath(): $CancellablePromise { + return $Call.ByID(3629702033); +} + +/** + * ContributeMetadata creates a GitHub PR to improve the metadata of a skill. + * It relies on an existing GitHub CLI login and only forks when the current + * account cannot push a branch to the upstream repository. + */ +export function ContributeMetadata(params: $models.ContributeParams): $CancellablePromise<$models.ContributeResult> { + return $Call.ByID(1008264935, params); +} + +/** + * DetectTargets returns the platform(s) already present in repoPath. A target + * is reported as "detected" when its directory contains either skell.toml or a + * skills/ subdirectory. + */ +export function DetectTargets(repoPath: string): $CancellablePromise<$models.AgentTarget[] | null> { + return $Call.ByID(2920689880, repoPath); +} + +/** + * GlobalRootDir returns the global Skell root directory (~/.skell) and ensures + * the global manifest exists so that `skell search --repo ` can resolve it. + */ +export function GlobalRootDir(): $CancellablePromise { + return $Call.ByID(2693605881); +} + +/** + * IsRepoInitialized returns true when the given directory contains a Skell + * manifest in any supported AI-agent layout (.claude, .codex, .github, .cursor), + * meaning `skell init` has already been run for some target. + */ +export function IsRepoInitialized(repoPath: string): $CancellablePromise { + return $Call.ByID(3848862957, repoPath); +} + +/** + * ListDirectory returns the immediate children of a directory. + */ +export function ListDirectory(path: string): $CancellablePromise<$models.FileEntry[] | null> { + return $Call.ByID(3882022436, path); +} + +/** + * ListSkillSources returns the globally configured skill sources from ~/.skell/config.toml. + */ +export function ListSkillSources(): $CancellablePromise<$models.SkillSource[] | null> { + return $Call.ByID(3339277332); +} + +/** + * PreviewRegistrySkill returns the SKILL.md contents and disk path for a skill + * in a (cached or local-folder) registry, without installing it. Used by the + * Discover Skills preview modal. + */ +export function PreviewRegistrySkill(registryAlias: string, registryURL: string, skillName: string): $CancellablePromise<$models.SkillPreview> { + return $Call.ByID(3669316229, registryAlias, registryURL, skillName); +} + +/** + * ReadFileContent reads and returns the contents of a file. + */ +export function ReadFileContent(path: string): $CancellablePromise { + return $Call.ByID(2440131916, path); +} + +/** + * ReadSkillMetadata parses the SKILL.md in the given directory and returns + * the editable metadata fields. + */ +export function ReadSkillMetadata(installedPath: string): $CancellablePromise<$models.SkillMetadataFields> { + return $Call.ByID(2621935323, installedPath); +} + +/** + * RemoveSkillSource removes a global skill source by alias. + */ +export function RemoveSkillSource(alias: string): $CancellablePromise { + return $Call.ByID(2166783643, alias); +} + +/** + * ResolveSkillSourceRepoURL tries to upgrade a repo-root source URL into the + * nested skill folder URL by consulting the local registry cache. + */ +export function ResolveSkillSourceRepoURL(sourceRepo: string, registryAlias: string, skillName: string): $CancellablePromise { + return $Call.ByID(1846080558, sourceRepo, registryAlias, skillName); +} + +export function RunSkell(args: string[] | null): $CancellablePromise<$models.SkellResult> { + return $Call.ByID(2394374327, args); +} + +/** + * SelectDirectory opens a native directory picker dialog and returns the selected path. + */ +export function SelectDirectory(): $CancellablePromise { + return $Call.ByID(1735672136); +} + +/** + * SkellPresent returns true if a usable skell binary can be resolved. + * The GUI uses this (and catches the not-found error from RunSkell) to show + * friendly "install the CLI first" guidance instead of generic errors or + * perpetual spinners on brand-new machines (where only the GUI is installed). + */ +export function SkellPresent(): $CancellablePromise { + return $Call.ByID(2004525469); +} + +/** + * SkellVersion returns the output of `skell --version`. + */ +export function SkellVersion(): $CancellablePromise { + return $Call.ByID(3500197102); +} + +/** + * SupportedTargets returns the static list of platforms the GUI offers as + * choices in the init dialog. + */ +export function SupportedTargets(): $CancellablePromise<$models.AgentTarget[] | null> { + return $Call.ByID(1734000213); +} + +/** + * ValidateSkill runs `skell validate` for one skill (or all skills when + * skillName is empty) in repoPath and returns the parsed results. When full is + * true, offline content and contamination analysis are included. Results are + * returned even when validation reports errors (a non-zero CLI exit). + */ +export function ValidateSkill(repoPath: string, skillName: string, full: boolean): $CancellablePromise<$models.SkillValidationResult[] | null> { + return $Call.ByID(567263264, repoPath, skillName, full); +} diff --git a/gui/frontend/bindings/skell-gui/index.ts b/gui/frontend/bindings/skell-gui/index.ts new file mode 100644 index 0000000..cce7b13 --- /dev/null +++ b/gui/frontend/bindings/skell-gui/index.ts @@ -0,0 +1,21 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH ร‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +import * as App from "./app.js"; +export { + App +}; + +export type { + AgentTarget, + ContributeParams, + ContributeResult, + FileEntry, + SkellResult, + SkillAnalysis, + SkillMetadataFields, + SkillPreview, + SkillSource, + SkillValidationFinding, + SkillValidationResult +} from "./models.js"; diff --git a/gui/frontend/bindings/skell-gui/models.ts b/gui/frontend/bindings/skell-gui/models.ts new file mode 100644 index 0000000..e7b8506 --- /dev/null +++ b/gui/frontend/bindings/skell-gui/models.ts @@ -0,0 +1,128 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH ร‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * AgentTarget describes a supported AI-agent platform layout for the GUI. + */ +export interface AgentTarget { + "id": string; + "displayName": string; + "dir": string; + "detected": boolean; +} + +/** + * ContributeParams describes a metadata-contribution PR request. + */ +export interface ContributeParams { + "installedPath": string; + "sourceRepo": string; + "skillName": string; + "metadata": SkillMetadataFields; +} + +/** + * ContributeResult is returned after attempting to open a PR. + */ +export interface ContributeResult { + "prUrl": string; + "success": boolean; + "error"?: string; +} + +/** + * FileEntry represents a filesystem entry (file or directory). + */ +export interface FileEntry { + "name": string; + "is_dir": boolean; + "path": string; +} + +/** + * SkellResult is the JSON-shaped result returned to the frontend for skell invocations. + */ +export interface SkellResult { + "stdout": string; + "stderr": string; + "success": boolean; +} + +/** + * SkillAnalysis holds the offline quality metrics surfaced to the GUI (mirrors + * internal/validator.Analysis). Nil when only structure checks ran. + */ +export interface SkillAnalysis { + "word_count": number; + "code_block_ratio": number; + "imperative_ratio": number; + "information_density": number; + "instruction_specificity": number; + "section_count": number; + "list_item_count": number; + "has_content": boolean; + "contamination_score": number; + "contamination_level": string; + "code_languages": string[] | null; + "mismatched_categories": string[] | null; + "language_mismatch": boolean; + "has_contamination": boolean; + "total_tokens": number; + "skill_tokens": number; +} + +/** + * SkillMetadataFields holds the user-editable subset of SKILL.md frontmatter. + */ +export interface SkillMetadataFields { + "description": string; + "tags": string; + "lifecycle": string; + "owner": string; + "version": string; +} + +/** + * SkillPreview describes a not-yet-installed skill: its SKILL.md content and + * the on-disk location it was read from. Found is false when the registry + * cache hasn't been populated yet โ€” the caller can still render the metadata + * it already has from the registry listing. + */ +export interface SkillPreview { + "readme_content": string; + "source_path": string; + "source_url": string; + "source_type": string; + "found": boolean; +} + +/** + * SkillSource represents a persistent skill source (git or local folder). + */ +export interface SkillSource { + "alias": string; + "url": string; + "is_local": boolean; +} + +/** + * SkillValidationFinding mirrors one validator finding for the frontend. + */ +export interface SkillValidationFinding { + "severity": string; + "category": string; + "message": string; + "file": string; + "line": number; +} + +/** + * SkillValidationResult is the per-skill validation outcome surfaced to the GUI. + */ +export interface SkillValidationResult { + "name": string; + "errors": number; + "warnings": number; + "findings": SkillValidationFinding[] | null; + "analysis"?: SkillAnalysis | null; +} diff --git a/gui/frontend/src/components/AddSkillButton.tsx b/gui/frontend/src/components/AddSkillButton.tsx new file mode 100644 index 0000000..c48b7a4 --- /dev/null +++ b/gui/frontend/src/components/AddSkillButton.tsx @@ -0,0 +1,72 @@ +import { useMemo, useState } from "react"; +import { Plus, Library, GitBranchPlus } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { useRepoStore, useUIStore } from "@/store"; +import { AddFromURLDialog } from "./AddFromURLDialog"; + +interface AddSkillButtonProps { + projectPath: string; + onRefresh?: () => void; + label?: string; +} + +export function AddSkillButton({ projectPath, onRefresh, label = "Add Skill" }: AddSkillButtonProps) { + const navigate = useNavigate(); + const { selectedRepo } = useRepoStore(); + const { notify } = useUIStore(); + const [menuOpen, setMenuOpen] = useState(false); + const [addUrlOpen, setAddUrlOpen] = useState(false); + + const destination = useMemo(() => projectPath || selectedRepo || "", [projectPath, selectedRepo]); + + function handleBrowseCatalog() { + setMenuOpen(false); + if (!destination) { + notify({ kind: "info", title: "Select a project first", detail: "Choose a project before installing a catalog skill." }); + return; + } + navigate("/catalog", { state: { installDestination: destination } }); + } + + function handleAddFromRepository() { + setMenuOpen(false); + if (!destination) { + notify({ kind: "info", title: "Select a project first", detail: "Choose a project before adding a skill from a repository." }); + return; + } + setAddUrlOpen(true); + } + + return ( + <> +

+ + {menuOpen && ( +
+ + + +
+ )} +
+ setAddUrlOpen(false)} + initialRepo={destination} + onSuccess={() => onRefresh?.()} + /> + + ); +} diff --git a/gui/frontend/src/components/CurrentProjectSummary.tsx b/gui/frontend/src/components/CurrentProjectSummary.tsx new file mode 100644 index 0000000..889a1bb --- /dev/null +++ b/gui/frontend/src/components/CurrentProjectSummary.tsx @@ -0,0 +1,46 @@ +import { FolderKanban, ChevronRight } from "lucide-react"; + +interface CurrentProjectSummaryProps { + projectPath: string; + collapsed?: boolean; + onOpen?: () => void; +} + +export function CurrentProjectSummary({ projectPath, collapsed = false, onOpen }: CurrentProjectSummaryProps) { + if (!projectPath) return null; + + const displayName = projectPath.split(/[\\/]/).filter(Boolean).pop() ?? "Project"; + + return ( + + ); +} diff --git a/gui/frontend/src/components/RecentProjects.tsx b/gui/frontend/src/components/RecentProjects.tsx new file mode 100644 index 0000000..65388f7 --- /dev/null +++ b/gui/frontend/src/components/RecentProjects.tsx @@ -0,0 +1,78 @@ +import { ChevronRight, FolderOpen, Plus } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { useRepoStore } from "@/store"; +import type { RecentProject } from "@/store"; +import { createProjectId } from "@/lib/navigation"; + +interface RecentProjectsProps { + collapsed?: boolean; + onAddProject?: () => void; +} + +export function RecentProjects({ collapsed = false, onAddProject }: RecentProjectsProps) { + const navigate = useNavigate(); + const { recentProjects, selectedRepo, openProject } = useRepoStore(); + + const visibleProjects = recentProjects.slice(0, 5); + if (visibleProjects.length === 0) return null; + + function handleSelect(project: RecentProject) { + openProject(project.path); + navigate(`/projects/${createProjectId(project.path)}/skills`); + } + + return ( +
+ {!collapsed && ( +
+

Recent Projects

+ +
+ )} + +
+ {visibleProjects.map((project) => { + const isActive = selectedRepo === project.path; + return ( + + ); + })} +
+ + {!collapsed && ( + + )} +
+ ); +} diff --git a/gui/frontend/src/components/Sidebar.tsx b/gui/frontend/src/components/Sidebar.tsx index b1969bd..4107970 100644 --- a/gui/frontend/src/components/Sidebar.tsx +++ b/gui/frontend/src/components/Sidebar.tsx @@ -1,62 +1,41 @@ import { NavLink, useNavigate } from "react-router-dom"; import { SelectDirectory } from "../../wailsjs/skell-gui/app"; import { - LayoutDashboard, - FolderOpen, - Package, - Search, - RefreshCw, - Stethoscope, - Database, - ScrollText, - Settings, - Plus, - Globe, - FolderClosed, + Home, + FolderKanban, + Library, + Activity, + Settings as SettingsIcon, PanelLeftClose, PanelLeft, - FilePen, } from "lucide-react"; import { useRepoStore } from "@/store"; import clsx from "clsx"; import { isMac } from "@/lib/platform"; +import { createProjectId } from "@/lib/navigation"; +import { CurrentProjectSummary } from "./CurrentProjectSummary"; +import { RecentProjects } from "./RecentProjects"; -const NAV_SECTIONS = [ - { - title: "Main", - items: [ - { to: "/", icon: LayoutDashboard, label: "Dashboard" }, - { to: "/repositories", icon: FolderOpen, label: "Projects" }, - { to: "/skills", icon: Package, label: "My Skills" }, - { to: "/registry", icon: Search, label: "Discover Skills" }, - { to: "/sync", icon: RefreshCw, label: "Sync" }, - ], - }, - { - title: "Tools", - items: [ - { to: "/contribute-info", icon: FilePen, label: "Metadata Editor" }, - { to: "/doctor", icon: Stethoscope, label: "Doctor" }, - { to: "/cache", icon: Database, label: "Cache" }, - { to: "/audit", icon: ScrollText, label: "Audit Log" }, - { to: "/settings", icon: Settings, label: "Settings" }, - ], - }, +const NAV_ITEMS = [ + { to: "/", icon: Home, label: "Home" }, + { to: "/projects", icon: FolderKanban, label: "Projects" }, + { to: "/catalog", icon: Library, label: "Catalog" }, + { to: "/activity", icon: Activity, label: "Activity" }, + { to: "/settings", icon: SettingsIcon, label: "Settings" }, ]; // IS_MAC used for macOS titlebar padding and no-drag regions on traffic lights. const IS_MAC = isMac; export function Sidebar() { - const { repos, selectedRepo, setSelectedRepo, addRepo, sidebarCollapsed, toggleSidebar } = - useRepoStore(); + const { addRepo, sidebarCollapsed, toggleSidebar, selectedRepo } = useRepoStore(); const navigate = useNavigate(); async function handleAddRepo() { const selected = await SelectDirectory(); if (selected) { addRepo(selected); - navigate("/skills"); + navigate("/projects"); } } @@ -92,105 +71,58 @@ export function Sidebar() {
- {/* Navigation */} - - - {/* Project Context Switcher - now more prominent */} - {!sidebarCollapsed && ( -
-
- - Your Projects - - -
-

Skills are installed into the selected project

+ + {!sidebarCollapsed && {label}} + + ))} + - {/* Shared library entry */} - + {selectedRepo && selectedRepo !== "global" && ( +
+ navigate(`/projects/${createProjectId(selectedRepo)}/skills`)} + /> +
+ )} - {/* Local repos */} - {repos.map((repo) => { - const short = repo.split(/[/\\]/).at(-1) ?? repo; - const isActive = selectedRepo === repo; - return ( - - ); - })} + +
- {repos.length === 0 && ( -

No projects added yet

+
+ +
); } diff --git a/gui/frontend/src/lib/navigation.ts b/gui/frontend/src/lib/navigation.ts new file mode 100644 index 0000000..bb99957 --- /dev/null +++ b/gui/frontend/src/lib/navigation.ts @@ -0,0 +1,36 @@ +export type NavigationProjectKind = "project" | "personal"; + +export interface NavigationProject { + id: string; + path: string; + name: string; + kind: NavigationProjectKind; +} + +export function isPersonalScope(value?: string | null): boolean { + return value === "global" || value === "personal" || value === ""; +} + +export function getProjectDisplayName(path?: string | null): string { + if (!path || isPersonalScope(path)) return "Personal Skills"; + return path.split(/[\\/]/).filter(Boolean).pop() ?? "Project"; +} + +export function createProjectId(path: string): string { + const normalized = path.replace(/\\/g, "/").toLowerCase(); + let hash = 0; + for (let i = 0; i < normalized.length; i += 1) { + hash = (hash << 5) - hash + normalized.charCodeAt(i); + hash |= 0; + } + return `project-${Math.abs(hash).toString(36)}`; +} + +export function buildProjectRoute(path: string, subPath = "") { + const id = createProjectId(path); + return `/projects/${id}${subPath}`; +} + +export function resolveProject(projects: NavigationProject[], id?: string | null) { + return projects.find((project) => project.id === id) ?? null; +} diff --git a/gui/frontend/src/store/index.ts b/gui/frontend/src/store/index.ts index 97d5164..36591bd 100644 --- a/gui/frontend/src/store/index.ts +++ b/gui/frontend/src/store/index.ts @@ -1,59 +1,158 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; -// --------------------------------------------------------------------------- -// Repo store โ€” persisted across app restarts -// --------------------------------------------------------------------------- +export interface RecentProject { + id: string; + path: string; + displayName: string; + lastOpenedAt: string; +} -interface RepoStore { - /** Absolute paths of all managed repositories (local + global placeholder) */ +interface RepoStoreState { repos: string[]; - /** Currently focused repo (path or "global") */ selectedRepo: string; - /** Collapsed state of sidebar sections */ + recentProjects: RecentProject[]; sidebarCollapsed: boolean; +} - // Actions +interface RepoStore extends RepoStoreState { addRepo: (path: string) => void; removeRepo: (path: string) => void; setSelectedRepo: (path: string) => void; toggleSidebar: () => void; + openProject: (path: string) => void; } -export const useRepoStore = create()( - persist( - (set) => ({ - repos: [], - selectedRepo: "global", - sidebarCollapsed: false, +function normalizeForComparison(path: string): string { + return path.replace(/\\/g, "/").trim().toLowerCase(); +} - addRepo: (path) => - set((s) => ({ - repos: s.repos.includes(path) ? s.repos : [...s.repos, path], +function getDisplayName(path: string): string { + if (!path) return "Project"; + const parts = path.split(/[\\/]/).filter(Boolean); + const last = parts.at(-1); + return last || "Project"; +} + +function createProjectId(path: string): string { + const normalized = normalizeForComparison(path); + let hash = 0; + for (let i = 0; i < normalized.length; i += 1) { + hash = (hash << 5) - hash + normalized.charCodeAt(i); + hash |= 0; + } + return `project-${Math.abs(hash).toString(36)}`; +} + +function coerceProjectRecord(path: string, lastOpenedAt?: string): RecentProject { + return { + id: createProjectId(path), + path, + displayName: getDisplayName(path), + lastOpenedAt: lastOpenedAt ?? new Date().toISOString(), + }; +} + +function dedupeProjects(projects: RecentProject[]): RecentProject[] { + const seen = new Set(); + return projects.reduce((acc, project) => { + const key = normalizeForComparison(project.path); + if (seen.has(key)) return acc; + seen.add(key); + acc.push(project); + return acc; + }, []); +} + +export function migrateRepoStoreState(input: Partial & { repos?: string[]; selectedRepo?: string }): RepoStoreState { + const seen = new Set(); + const repos = Array.isArray(input.repos) + ? input.repos.filter((repo): repo is string => typeof repo === "string").filter((repo) => { + const key = normalizeForComparison(repo); + if (seen.has(key)) return false; + seen.add(key); + return true; + }) + : []; + const selectedRepo = typeof input.selectedRepo === "string" ? input.selectedRepo : "global"; + const recentProjects = dedupeProjects( + repos.filter((path) => path !== "global").map((path) => coerceProjectRecord(path, new Date().toISOString())) + ).slice(0, 5); + + return { + repos, + selectedRepo, + recentProjects, + sidebarCollapsed: false, + }; +} + +const createRepoStore = () => + create()( + persist( + (set) => ({ + repos: [], + selectedRepo: "global", + recentProjects: [], + sidebarCollapsed: false, + + addRepo: (path) => + set((s) => ({ + repos: s.repos.includes(path) ? s.repos : [...s.repos, path], + selectedRepo: path, + recentProjects: dedupeProjects([ + coerceProjectRecord(path), + ...s.recentProjects.filter((project) => project.path !== path), + ]).slice(0, 5), + })), + + removeRepo: (path) => + set((s) => { + const repos = s.repos.filter((r) => r !== path); + return { + repos, + selectedRepo: + s.selectedRepo === path ? (repos[0] ?? "global") : s.selectedRepo, + recentProjects: s.recentProjects.filter((project) => project.path !== path), + }; + }), + + setSelectedRepo: (path) => set((s) => ({ selectedRepo: path, + recentProjects: path && path !== "global" + ? dedupeProjects([ + coerceProjectRecord(path, new Date().toISOString()), + ...s.recentProjects.filter((project) => project.path !== path), + ]).slice(0, 5) + : s.recentProjects, })), - removeRepo: (path) => - set((s) => { - const repos = s.repos.filter((r) => r !== path); - return { - repos, - selectedRepo: - s.selectedRepo === path ? (repos[0] ?? "global") : s.selectedRepo, - }; - }), - - setSelectedRepo: (path) => set({ selectedRepo: path }), - - toggleSidebar: () => - set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })), - }), - { - name: "skell-gui-repos", - version: 1, - } - ) -); + toggleSidebar: () => + set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })), + + openProject: (path) => + set((s) => ({ + selectedRepo: path, + recentProjects: dedupeProjects([ + coerceProjectRecord(path, new Date().toISOString()), + ...s.recentProjects.filter((project) => project.path !== path), + ]).slice(0, 5), + })), + }), + { + name: "skell-gui-repos", + version: 2, + migrate: (persistedState, version) => { + if (version < 2) { + return migrateRepoStoreState(persistedState as Partial & { repos?: string[]; selectedRepo?: string }); + } + return persistedState as RepoStoreState; + }, + } + ) + ); + +export const useRepoStore = createRepoStore(); // --------------------------------------------------------------------------- // UI store โ€” transient, not persisted diff --git a/gui/frontend/src/test/setup.ts b/gui/frontend/src/test/setup.ts index 138ef0b..3ed677f 100644 --- a/gui/frontend/src/test/setup.ts +++ b/gui/frontend/src/test/setup.ts @@ -1,6 +1,50 @@ import "@testing-library/jest-dom"; import { vi } from "vitest"; +// Wails v3 generated bindings call @wailsio/runtime directly instead of using +// the old window.go bridge. Route those calls back through the bridge below so +// existing tests can replace individual service methods with vi.fn(). +vi.mock("@wailsio/runtime", async (importOriginal) => { + const actual = await importOriginal(); + const methodByID: Record = { + 2394438090: "ActiveTarget", + 2803609340: "AddSkillSource", + 3629702033: "AuditLogPath", + 1008264935: "ContributeMetadata", + 2920689880: "DetectTargets", + 2693605881: "GlobalRootDir", + 3848862957: "IsRepoInitialized", + 3882022436: "ListDirectory", + 3339277332: "ListSkillSources", + 3669316229: "PreviewRegistrySkill", + 2440131916: "ReadFileContent", + 2621935323: "ReadSkillMetadata", + 2166783643: "RemoveSkillSource", + 1846080558: "ResolveSkillSourceRepoURL", + 2394374327: "RunSkell", + 1735672136: "SelectDirectory", + 2004525469: "SkellPresent", + 3500197102: "SkellVersion", + 1734000213: "SupportedTargets", + 567263264: "ValidateSkill", + }; + + return { + ...actual, + Call: { + ...actual.Call, + ByID: vi.fn((id: number, ...args: unknown[]) => { + const method = methodByID[id]; + const app = window.go?.main?.App as unknown as Record unknown>; + if (!method || typeof app?.[method] !== "function") { + return Promise.reject(new Error(`Unmocked Wails method ID: ${id}`)); + } + return app[method](...args); + }), + }, + }; +}); + // --------------------------------------------------------------------------- // Mock the Wails runtime bridge (window.go) so imports of wailsjs bindings // don't throw "Cannot read properties of undefined" in jsdom. @@ -18,6 +62,19 @@ Object.defineProperty(window, "go", { SelectDirectory: vi.fn().mockResolvedValue(""), AuditLogPath: vi.fn().mockResolvedValue(""), GlobalRootDir: vi.fn().mockResolvedValue(""), + ActiveTarget: vi.fn().mockResolvedValue(""), + AddSkillSource: vi.fn().mockResolvedValue(undefined), + ContributeMetadata: vi.fn().mockResolvedValue({}), + DetectTargets: vi.fn().mockResolvedValue([]), + IsRepoInitialized: vi.fn().mockResolvedValue(false), + ListSkillSources: vi.fn().mockResolvedValue([]), + PreviewRegistrySkill: vi.fn().mockResolvedValue({ found: false }), + ReadSkillMetadata: vi.fn().mockResolvedValue({}), + RemoveSkillSource: vi.fn().mockResolvedValue(undefined), + ResolveSkillSourceRepoURL: vi.fn().mockResolvedValue(""), + SkellPresent: vi.fn().mockResolvedValue(true), + SupportedTargets: vi.fn().mockResolvedValue([]), + ValidateSkill: vi.fn().mockResolvedValue([]), }, }, }, From 214ced935ff842bfa350e74e745013c4c43eb13f Mon Sep 17 00:00:00 2001 From: Amin Mesbahi Date: Sun, 19 Jul 2026 19:56:06 +0200 Subject: [PATCH 04/14] chore(gui): add test files for new pages and components --- .../components/CurrentProjectSummary.test.tsx | 19 ++++ gui/frontend/src/test/pages/Catalog.test.tsx | 100 ++++++++++++++++++ .../src/test/pages/ProjectPages.test.tsx | 66 ++++++++++++ .../src/test/pages/Repositories.test.tsx | 24 ++++- gui/frontend/src/test/store/repoStore.test.ts | 26 +++++ 5 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 gui/frontend/src/test/components/CurrentProjectSummary.test.tsx create mode 100644 gui/frontend/src/test/pages/Catalog.test.tsx create mode 100644 gui/frontend/src/test/pages/ProjectPages.test.tsx create mode 100644 gui/frontend/src/test/store/repoStore.test.ts diff --git a/gui/frontend/src/test/components/CurrentProjectSummary.test.tsx b/gui/frontend/src/test/components/CurrentProjectSummary.test.tsx new file mode 100644 index 0000000..5026a1b --- /dev/null +++ b/gui/frontend/src/test/components/CurrentProjectSummary.test.tsx @@ -0,0 +1,19 @@ +import { describe, expect, it, vi } from "vitest"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { CurrentProjectSummary } from "@/components/CurrentProjectSummary"; + +describe("CurrentProjectSummary", () => { + it("opens the project when any part of the card is clicked", () => { + const onOpen = vi.fn(); + render( + + ); + + fireEvent.click(screen.getByRole("button", { name: "Open current project New folder (2)" })); + + expect(onOpen).toHaveBeenCalledOnce(); + }); +}); diff --git a/gui/frontend/src/test/pages/Catalog.test.tsx b/gui/frontend/src/test/pages/Catalog.test.tsx new file mode 100644 index 0000000..4a471bb --- /dev/null +++ b/gui/frontend/src/test/pages/Catalog.test.tsx @@ -0,0 +1,100 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { fireEvent, screen, waitFor } from "@testing-library/react"; +import { Catalog } from "@/pages/Catalog"; +import { renderWithRouter } from "@/test/utils"; +import { mockOkResult, mockRegistrySkill } from "@/test/fixtures"; +import { useRepoStore, useUIStore } from "@/store"; +import * as skell from "@/lib/skell"; + +vi.mock("@/lib/skell"); + +const mockSkell = skell as unknown as Record>; +const projectPath = "D:\\amin\\Desktop\\New folder (2)"; + +beforeEach(() => { + vi.resetAllMocks(); + mockSkell.listRegistry.mockResolvedValue([]); + mockSkell.listInstalled.mockResolvedValue([]); + mockSkell.listInstalledGlobal.mockResolvedValue([]); + mockSkell.installSkill.mockResolvedValue(mockOkResult()); + mockSkell.previewRegistrySkill.mockResolvedValue({ + found: true, + source_type: "git", + source_path: "", + source_url: "https://github.com/WordPress/agent-skills", + readme_content: "# Blueprint", + }); + useRepoStore.setState({ + repos: [projectPath], + selectedRepo: projectPath, + recentProjects: [], + sidebarCollapsed: false, + }); + useUIStore.setState({ notifications: [] }); +}); + +describe("Catalog", () => { + it("opens the real skill preview without running install", async () => { + mockSkell.listRegistry.mockResolvedValue([ + mockRegistrySkill({ + name: "blueprint", + registry_alias: "wordpress-skills", + registry_url: "https://github.com/WordPress/agent-skills", + }), + ]); + + renderWithRouter(, { initialEntries: ["/catalog"] }); + + fireEvent.click(await screen.findByRole("button", { name: "Preview" })); + + await waitFor(() => { + expect(mockSkell.previewRegistrySkill).toHaveBeenCalledWith( + "wordpress-skills", + "https://github.com/WordPress/agent-skills", + "blueprint" + ); + expect(screen.getByRole("dialog")).toBeTruthy(); + expect(mockSkell.installSkill).not.toHaveBeenCalled(); + expect(useUIStore.getState().notifications).toHaveLength(0); + }); + }); + + it("passes the catalog skill registry source to the real install", async () => { + mockSkell.listRegistry.mockResolvedValue([ + mockRegistrySkill({ + name: "blueprint", + registry_alias: "wordpress-skills", + registry_url: "https://github.com/WordPress/agent-skills", + }), + ]); + + renderWithRouter(, { initialEntries: ["/catalog"] }); + fireEvent.click(await screen.findByRole("button", { name: "Install" })); + + await waitFor(() => { + expect(mockSkell.installSkill).toHaveBeenCalledWith({ + skillName: "blueprint", + repo: projectPath, + registry: "wordpress-skills", + registryURL: "https://github.com/WordPress/agent-skills", + }); + }); + }); + + it("hides Cobra usage text from install errors", async () => { + mockSkell.listRegistry.mockResolvedValue([mockRegistrySkill({ name: "blueprint" })]); + mockSkell.installSkill.mockResolvedValue({ + success: false, + stdout: "", + stderr: "registry not configured\nUsage: skell install [flags]", + }); + + renderWithRouter(, { initialEntries: ["/catalog"] }); + fireEvent.click(await screen.findByRole("button", { name: "Install" })); + + await waitFor(() => { + const notification = useUIStore.getState().notifications.at(-1); + expect(notification?.detail).toBe("registry not configured"); + }); + }); +}); diff --git a/gui/frontend/src/test/pages/ProjectPages.test.tsx b/gui/frontend/src/test/pages/ProjectPages.test.tsx new file mode 100644 index 0000000..4f4945b --- /dev/null +++ b/gui/frontend/src/test/pages/ProjectPages.test.tsx @@ -0,0 +1,66 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { screen, waitFor } from "@testing-library/react"; +import { renderRoute } from "@/test/utils"; +import { ProjectSkillsPage } from "@/pages/projects/ProjectSkillsPage"; +import { useRepoStore } from "@/store"; +import { createProjectId } from "@/lib/navigation"; +import * as skell from "@/lib/skell"; + +vi.mock("@/lib/skell"); + +const mockSkell = skell as unknown as Record>; + +describe("Project skills page", () => { + beforeEach(() => { + useRepoStore.setState({ repos: ["/home/user/project"], selectedRepo: "/home/user/project" }); + mockSkell.listInstalled.mockResolvedValue([ + { + name: "alpha", + version: "1.0.0", + registry: "registry", + source_repo: "source", + installed_path: "/tmp/alpha", + installed_at: "", + pinned: false, + content_hash: "hash", + }, + ]); + mockSkell.getStatus.mockResolvedValue([ + { name: "alpha", installed: "1.0.0", latest: "1.0.0", status: "up-to-date" }, + ]); + mockSkell.isRepoInitialized.mockResolvedValue(true); + mockSkell.skellPresent.mockResolvedValue(true); + mockSkell.initRepo.mockResolvedValue({ success: true, stdout: "ok", stderr: "" }); + }); + + it("renders installed skills for the selected project", async () => { + renderRoute( + "/projects/:projectId/skills", + , + `/projects/${createProjectId("/home/user/project")}/skills` + ); + + await waitFor(() => { + expect(screen.getByText("alpha")).toBeTruthy(); + }); + expect(screen.getByText(/skills for/i)).toBeTruthy(); + }); + + it("offers catalog and repository install actions when a project has no skills", async () => { + mockSkell.listInstalled.mockResolvedValue([]); + mockSkell.getStatus.mockResolvedValue([]); + + renderRoute( + "/projects/:projectId/skills", + , + `/projects/${createProjectId("/home/user/project")}/skills` + ); + + await waitFor(() => { + expect(screen.getByText(/no skills installed yet/i)).toBeTruthy(); + }); + + expect(screen.getByRole("button", { name: /browse catalog/i })).toBeTruthy(); + expect(screen.getByRole("button", { name: /add from repository/i })).toBeTruthy(); + }); +}); diff --git a/gui/frontend/src/test/pages/Repositories.test.tsx b/gui/frontend/src/test/pages/Repositories.test.tsx index 23197f4..91c0eb5 100644 --- a/gui/frontend/src/test/pages/Repositories.test.tsx +++ b/gui/frontend/src/test/pages/Repositories.test.tsx @@ -1,5 +1,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { screen, waitFor, fireEvent } from "@testing-library/react"; +import { screen, waitFor, fireEvent, render } from "@testing-library/react"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; import { renderWithRouter } from "@/test/utils"; import { Repositories } from "@/pages/Repositories"; import * as skell from "@/lib/skell"; @@ -60,6 +61,27 @@ describe("Repositories", () => { }); }); + it("navigates to the project-scoped skills page when Skills is clicked", async () => { + const { useRepoStore } = await import("@/store"); + useRepoStore.setState({ repos: ["/home/user/project"], selectedRepo: "/home/user/project" }); + + render( + + + } /> + Project skills page
} /> + + + ); + + const skillsBtns = await screen.findAllByRole("button", { name: /skills/i }); + fireEvent.click(skillsBtns[skillsBtns.length - 1]); + + await waitFor(() => { + expect(screen.getByText("Project skills page")).toBeTruthy(); + }); + }); + it("calls initRepo when Init button clicked", async () => { const { useRepoStore } = await import("@/store"); useRepoStore.setState({ repos: ["/home/user/project"], selectedRepo: "/home/user/project" }); diff --git a/gui/frontend/src/test/store/repoStore.test.ts b/gui/frontend/src/test/store/repoStore.test.ts new file mode 100644 index 0000000..77835c3 --- /dev/null +++ b/gui/frontend/src/test/store/repoStore.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { migrateRepoStoreState } from "@/store"; + +describe("repo store migration", () => { + it("migrates legacy repo arrays without losing projects", () => { + const migrated = migrateRepoStoreState({ + repos: ["/tmp/Project A", "C:/tmp/project-a", "global", "/tmp/Project A"], + selectedRepo: "global", + } as any); + + expect(migrated.repos).toEqual(["/tmp/Project A", "C:/tmp/project-a", "global"]); + expect(migrated.recentProjects).toEqual([ + expect.objectContaining({ path: "/tmp/Project A", displayName: "Project A" }), + expect.objectContaining({ path: "C:/tmp/project-a", displayName: "project-a" }), + ]); + }); + + it("keeps a stable selected project after migration", () => { + const migrated = migrateRepoStoreState({ + repos: ["/tmp/Alpha", "/tmp/Beta"], + selectedRepo: "/tmp/Beta", + } as any); + + expect(migrated.selectedRepo).toBe("/tmp/Beta"); + }); +}); From f6e6b250155c72537d09081f57af550978a61a84 Mon Sep 17 00:00:00 2001 From: Amin Mesbahi Date: Sun, 19 Jul 2026 19:56:18 +0200 Subject: [PATCH 05/14] chore: gitignore Wails dev artifacts (node_modules, .task) --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 7f4f21b..3329f7a 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,10 @@ gui/build/windows/nsis/ gui/build/windows/icon.ico gui/frontend/wailsjs/ +# Wails dev artifacts +gui/node_modules/ +gui/.task/ + # Bun # bun.lock is intentionally committed (lockfile) From 84ad1393f7ef2124edd8636050b1948027606dd3 Mon Sep 17 00:00:00 2001 From: Amin Mesbahi Date: Sun, 19 Jul 2026 19:58:46 +0200 Subject: [PATCH 06/14] chore(gui): add Wails v3 build task configs --- gui/Taskfile.yml | 65 ++++++ gui/build/Taskfile.yml | 398 +++++++++++++++++++++++++++++++++ gui/build/config.yml | 79 +++++++ gui/build/darwin/Taskfile.yml | 193 ++++++++++++++++ gui/build/windows/Taskfile.yml | 195 ++++++++++++++++ 5 files changed, 930 insertions(+) create mode 100644 gui/Taskfile.yml create mode 100644 gui/build/Taskfile.yml create mode 100644 gui/build/config.yml create mode 100644 gui/build/darwin/Taskfile.yml create mode 100644 gui/build/windows/Taskfile.yml diff --git a/gui/Taskfile.yml b/gui/Taskfile.yml new file mode 100644 index 0000000..b5ce2aa --- /dev/null +++ b/gui/Taskfile.yml @@ -0,0 +1,65 @@ +version: '3' + +vars: + APP_NAME: "tempapp" + BIN_DIR: "bin" + PACKAGE_MANAGER: '{{.PACKAGE_MANAGER | default "npm"}}' + VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}' + # Target OS for build/package/run. Defaults to the host OS, and is overridden + # by `wails3 build GOOS=...` (or the GOOS env var) for cross-compilation. The + # tasks below dispatch to the matching platform Taskfile via this variable. + GOOS: '{{.GOOS | default OS}}' + +includes: + common: ./build/Taskfile.yml + windows: ./build/windows/Taskfile.yml + darwin: ./build/darwin/Taskfile.yml + linux: ./build/linux/Taskfile.yml + ios: ./build/ios/Taskfile.yml + android: ./build/android/Taskfile.yml + +tasks: + build: + summary: Builds the application + cmds: + - task: "{{.GOOS}}:build" + + package: + summary: Packages a production build of the application + cmds: + - task: "{{.GOOS}}:package" + + run: + summary: Runs the application + cmds: + - task: "{{.GOOS}}:run" + + dev: + summary: Runs the application in development mode + cmds: + - wails3 dev -config ./build/config.yml -port {{.VITE_PORT}} + + setup:docker: + summary: Builds Docker image for cross-compilation (~800MB download) + cmds: + - task: common:setup:docker + + build:server: + summary: Builds the application in server mode (no GUI, HTTP server only) + cmds: + - task: common:build:server + + run:server: + summary: Runs the application in server mode + cmds: + - task: common:run:server + + build:docker: + summary: Builds a Docker image for server mode deployment + cmds: + - task: common:build:docker + + run:docker: + summary: Builds and runs the Docker image + cmds: + - task: common:run:docker diff --git a/gui/build/Taskfile.yml b/gui/build/Taskfile.yml new file mode 100644 index 0000000..9714b9e --- /dev/null +++ b/gui/build/Taskfile.yml @@ -0,0 +1,398 @@ +version: '3' + +tasks: + go:mod:tidy: + summary: Runs `go mod tidy` + internal: true + # Universal/multi-arch builds invoke this concurrently from parallel deps; + # two `go mod tidy` processes racing on go.mod can corrupt it (#4637). + run: once + cmds: + - go mod tidy + + install:frontend:deps: + summary: Install frontend dependencies + run: once + cmds: + - task: install:frontend:deps:{{.PACKAGE_MANAGER}} + + install:frontend:deps:npm: + dir: frontend + sources: + - package.json + - package-lock.json + generates: + - node_modules + preconditions: + - sh: npm version + msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/" + cmds: + - npm install + + install:frontend:deps:bun: + dir: frontend + sources: + - package.json + - bun.lock + - bun.lockb + generates: + - node_modules + preconditions: + - sh: bun --version + msg: "bun not found" + cmds: + - bun install + + install:frontend:deps:pnpm: + dir: frontend + sources: + - package.json + - pnpm-lock.yaml + generates: + - node_modules + preconditions: + - sh: pnpm --version + msg: "pnpm not found" + cmds: + - pnpm install + + install:frontend:deps:yarn: + dir: frontend + sources: + - package.json + - yarn.lock + status: + - test -d node_modules || test -f .pnp.cjs + preconditions: + - sh: yarn --version + msg: "yarn not found" + cmds: + - yarn install + + build:frontend: + label: build:frontend (DEV={{.DEV}} RUNNER={{.PACKAGE_MANAGER}}) + summary: Build the frontend project + # darwin:build:universal runs its per-arch builds as parallel deps, each of + # which depends on this task. Without run:once the two executions race: + # one regenerates frontend/bindings (-clean deletes it first) while the + # other's bundler is reading it, failing intermittently with + # 'Could not resolve "./bindings/"' (#4637). + run: once + dir: frontend + sources: + - "**/*" + - exclude: node_modules/**/* + generates: + - dist/**/* + deps: + - task: install:frontend:deps + - task: generate:bindings + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + OBFUSCATED: + ref: .OBFUSCATED + cmds: + - task: frontend:run + vars: + SCRIPT: '{{if eq .DEV "true"}}build:dev{{else}}build{{end}}' + env: + PRODUCTION: '{{if eq .DEV "true"}}false{{else}}true{{end}}' + + frontend:run: + summary: Run a frontend script with selected runner + cmds: + - task: frontend:run:{{.PACKAGE_MANAGER}} + vars: + SCRIPT: "{{.SCRIPT}}" + vars: + SCRIPT: "{{.SCRIPT}}" + + frontend:run:npm: + dir: frontend + cmds: + - npm run {{.SCRIPT}} -q + vars: + SCRIPT: "{{.SCRIPT}}" + + frontend:run:yarn: + dir: frontend + cmds: + - yarn {{.SCRIPT}} + vars: + SCRIPT: "{{.SCRIPT}}" + + frontend:run:pnpm: + dir: frontend + cmds: + - pnpm run {{.SCRIPT}} + vars: + SCRIPT: "{{.SCRIPT}}" + + frontend:run:bun: + dir: frontend + cmds: + - bun run {{.SCRIPT}} + vars: + SCRIPT: "{{.SCRIPT}}" + + frontend:vendor:puppertino: + summary: Fetches Puppertino CSS into frontend/public for consistent mobile styling + sources: + - frontend/public/puppertino/puppertino.css + generates: + - frontend/public/puppertino/puppertino.css + cmds: + - | + set -euo pipefail + mkdir -p frontend/public/puppertino + # If bundled Puppertino exists, prefer it. Otherwise, try to fetch, but don't fail build on error. + if [ ! -f frontend/public/puppertino/puppertino.css ]; then + echo "No bundled Puppertino found. Attempting to fetch from GitHub..." + if curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/dist/css/full.css -o frontend/public/puppertino/puppertino.css; then + curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/LICENSE -o frontend/public/puppertino/LICENSE || true + echo "Puppertino CSS downloaded to frontend/public/puppertino/puppertino.css" + else + echo "Warning: Could not fetch Puppertino CSS. Proceeding without download since template may bundle it." + fi + else + echo "Using bundled Puppertino at frontend/public/puppertino/puppertino.css" + fi + # Ensure index.html includes Puppertino CSS and button classes + INDEX_HTML=frontend/index.html + if [ -f "$INDEX_HTML" ]; then + if ! grep -q 'href="/puppertino/puppertino.css"' "$INDEX_HTML"; then + # Insert Puppertino link tag after style.css link + awk ' + /href="\/style.css"\/?/ && !x { print; print " "; x=1; next }1 + ' "$INDEX_HTML" > "$INDEX_HTML.tmp" && mv "$INDEX_HTML.tmp" "$INDEX_HTML" + fi + # Replace default .btn with Puppertino primary button classes if present + sed -E -i'' 's/class=\"btn\"/class=\"p-btn p-prim-col\"/g' "$INDEX_HTML" || true + fi + + + + generate:bindings: + summary: Generates bindings for the frontend + run: once + deps: + - task: go:mod:tidy + sources: + - "**/*.[jt]s" + - exclude: frontend/**/* + - frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output + - "**/*.go" + - go.mod + - go.sum + generates: + - frontend/bindings/**/* + cmds: + - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true{{if eq .OBFUSCATED "true"}} -obfuscated{{end}} -ts -i + + generate:icons: + summary: Generates Windows `.ico` and Mac `.icns` from an image; on macOS, `-iconcomposerinput appicon.icon -macassetdir darwin` also produces `Assets.car` from a `.icon` file (skipped on other platforms). + run: once + dir: build + sources: + - "appicon.png" + - "appicon.icon" + generates: + - "darwin/icons.icns" + - "windows/icon.ico" + cmds: + - wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico -iconcomposerinput appicon.icon -macassetdir darwin + + dev:frontend: + summary: Runs the frontend in development mode + deps: + - task: install:frontend:deps + cmds: + - task: frontend:dev:{{.PACKAGE_MANAGER}} + + frontend:dev:npm: + dir: frontend + cmds: + - npm run dev -- --port {{.VITE_PORT}} --strictPort + + frontend:dev:yarn: + dir: frontend + cmds: + - yarn dev --port {{.VITE_PORT}} --strictPort + + frontend:dev:pnpm: + dir: frontend + cmds: + - pnpm dev --port {{.VITE_PORT}} --strictPort + + frontend:dev:bun: + dir: frontend + cmds: + - bun run dev --port {{.VITE_PORT}} --strictPort + + update:build-assets: + summary: Updates the build assets + dir: build + cmds: + - wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir . + + build:server: + summary: Builds the application in server mode (no GUI, HTTP server only) + desc: | + Builds a production server binary by default: -tags server,production, + -trimpath and a stripped binary, mirroring the desktop `build` task. + Server mode runs as a pure HTTP server without native GUI dependencies. + + Usage: task build:server [DEV=true] [OBFUSCATED=true] [EXTRA_TAGS=tag1,tag2] + DEV=true development server (-tags server, no strip, inlining kept) + OBFUSCATED=true obfuscated build via garble (requires garble installed) + EXTRA_TAGS additional comma-separated build tags + deps: + - task: build:frontend + vars: + DEV: + ref: .DEV + BUILD_FLAGS: + ref: .BUILD_FLAGS + OBFUSCATED: + ref: .OBFUSCATED + preconditions: + - sh: '{{if eq .OBFUSCATED "true"}}command -v garble >/dev/null 2>&1{{else}}true{{end}}' + msg: "garble is required for obfuscated builds. Install it with: go install mvdan.cc/garble@v0.16.0 (requires Go 1.24+). See https://github.com/burrowers/garble/releases for version/toolchain compatibility." + cmds: + - '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}}"' + vars: + BUILD_FLAGS: '-tags server{{if eq .DEV "true"}}{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -buildvcs=false -gcflags=all="-l"{{else}},production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}' + + run:server: + summary: Builds and runs a development server (DEV=true) + deps: + - task: build:server + vars: + DEV: "true" + cmds: + - '"./{{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}}"' + + build:docker: + summary: Builds a Docker image for server mode deployment + desc: | + Creates a minimal Docker image containing the production server binary. + Defaults to a pure-Go static binary on a distroless/static base. The + production frontend is built first so the embedded assets are current. + + Usage: task build:docker [TAG=myapp:latest] [CGO_ENABLED=1] [GO_IMAGE=...] [RUNTIME_IMAGE=...] + For CGO apps, set CGO_ENABLED=1 with libc-compatible builder/runtime images, e.g.: + task build:docker CGO_ENABLED=1 GO_IMAGE=golang:bookworm RUNTIME_IMAGE=gcr.io/distroless/base-debian12 + deps: + # Build the production frontend so frontend/dist (embedded by the Go build + # inside the image) is present and current in the Docker build context. + # Pass the server,production tags so binding generation analyses the same + # build the Docker image compiles, not the default-tag build. + - task: build:frontend + vars: + BUILD_FLAGS: "-tags server,production" + cmds: + - >- + docker build + --build-arg CGO_ENABLED={{.CGO_ENABLED | default "0"}} + --build-arg GO_IMAGE={{.GO_IMAGE | default "golang:alpine"}} + --build-arg RUNTIME_IMAGE={{.RUNTIME_IMAGE | default "gcr.io/distroless/static-debian12"}} + -t {{.TAG | default (printf "%s:latest" .APP_NAME)}} + -f build/docker/Dockerfile.server . + vars: + TAG: "{{.TAG}}" + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required. Please install Docker first." + - sh: test -f build/docker/Dockerfile.server + msg: "Dockerfile.server not found. Run 'wails3 update build-assets' to generate it." + + run:docker: + summary: Builds and runs the Docker image + desc: | + Builds the Docker image and runs it, exposing port 8080. + Usage: task run:docker [TAG=myapp:latest] [PORT=8080] + Note: The internal container port is always 8080. The PORT variable + only changes the host port mapping. Ensure your app uses port 8080 + or modify the Dockerfile to match your ServerOptions.Port setting. + deps: + - task: build:docker + vars: + TAG: + ref: .TAG + cmds: + - docker run --rm -p {{.PORT | default "8080"}}:8080 {{.TAG | default (printf "%s:latest" .APP_NAME)}} + vars: + TAG: "{{.TAG}}" + PORT: "{{.PORT}}" + + setup:docker: + summary: Builds Docker image for cross-compilation (~800MB download) + desc: | + Builds the Docker image needed for cross-compiling to any platform. + Run this once to enable cross-platform builds from any OS. + cmds: + - docker build -t wails-cross -f build/docker/Dockerfile.cross build/docker/ + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required. Please install Docker first." + + ios:device:list: + summary: Lists connected iOS devices (UDIDs) + cmds: + - xcrun xcdevice list + + ios:run:device: + summary: Build, install, and launch on a physical iPhone using Apple tools (xcodebuild/devicectl) + vars: + PROJECT: '{{.PROJECT}}' # e.g., build/ios/xcode/.xcodeproj + SCHEME: '{{.SCHEME}}' # e.g., ios.dev + CONFIG: '{{.CONFIG | default "Debug"}}' + DERIVED: '{{.DERIVED | default "build/ios/DerivedData"}}' + UDID: '{{.UDID}}' # from `task ios:device:list` + BUNDLE_ID: '{{.BUNDLE_ID}}' # e.g., com.yourco.wails.ios.dev + TEAM_ID: '{{.TEAM_ID}}' # optional, if your project is not already set up for signing + preconditions: + - sh: xcrun -f xcodebuild + msg: "xcodebuild not found. Please install Xcode." + - sh: xcrun -f devicectl + msg: "devicectl not found. Please update to Xcode 15+ (which includes devicectl)." + - sh: test -n '{{.PROJECT}}' + msg: "Set PROJECT to your .xcodeproj path (e.g., PROJECT=build/ios/xcode/App.xcodeproj)." + - sh: test -n '{{.SCHEME}}' + msg: "Set SCHEME to your app scheme (e.g., SCHEME=ios.dev)." + - sh: test -n '{{.UDID}}' + msg: "Set UDID to your device UDID (see: task ios:device:list)." + - sh: test -n '{{.BUNDLE_ID}}' + msg: "Set BUNDLE_ID to your app's bundle identifier (e.g., com.yourco.wails.ios.dev)." + cmds: + - | + set -euo pipefail + echo "Building for device: UDID={{.UDID}} SCHEME={{.SCHEME}} PROJECT={{.PROJECT}}" + XCB_ARGS=( + -project "{{.PROJECT}}" + -scheme "{{.SCHEME}}" + -configuration "{{.CONFIG}}" + -destination "id={{.UDID}}" + -derivedDataPath "{{.DERIVED}}" + -allowProvisioningUpdates + -allowProvisioningDeviceRegistration + ) + # Optionally inject signing identifiers if provided + if [ -n '{{.TEAM_ID}}' ]; then XCB_ARGS+=(DEVELOPMENT_TEAM={{.TEAM_ID}}); fi + if [ -n '{{.BUNDLE_ID}}' ]; then XCB_ARGS+=(PRODUCT_BUNDLE_IDENTIFIER={{.BUNDLE_ID}}); fi + xcodebuild "${XCB_ARGS[@]}" build | xcpretty || true + # If xcpretty isn't installed, run without it + if [ "${PIPESTATUS[0]}" -ne 0 ]; then + xcodebuild "${XCB_ARGS[@]}" build + fi + # Find built .app + APP_PATH=$(find "{{.DERIVED}}/Build/Products" -type d -name "*.app" -maxdepth 3 | head -n 1) + if [ -z "$APP_PATH" ]; then + echo "Could not locate built .app under {{.DERIVED}}/Build/Products" >&2 + exit 1 + fi + echo "Installing: $APP_PATH" + xcrun devicectl device install app --device "{{.UDID}}" "$APP_PATH" + echo "Launching: {{.BUNDLE_ID}}" + xcrun devicectl device process launch --device "{{.UDID}}" --stderr console --stdout console "{{.BUNDLE_ID}}" diff --git a/gui/build/config.yml b/gui/build/config.yml new file mode 100644 index 0000000..9d49291 --- /dev/null +++ b/gui/build/config.yml @@ -0,0 +1,79 @@ +# This file contains the configuration for this project. +# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets. +# Note that this will overwrite any changes you have made to the assets. +version: '3' + +# This information is used to generate the build assets. +info: + companyName: "My Company" # The name of the company + productName: "My Product" # The name of the application + productIdentifier: "com.mycompany.myproduct" # The unique product identifier + description: "A program that does X" # The application description + copyright: "(c) 2025, My Company" # Copyright text + comments: "Some Product Comments" # Comments + version: "0.0.1" # The application version + # cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional) + # # Should match the name of your .icon file without the extension + # # If not set and Assets.car exists, defaults to "appicon" + +# iOS build configuration (uncomment to customise iOS project generation) +# Note: Keys under `ios` OVERRIDE values under `info` when set. +# ios: +# # The iOS bundle identifier used in the generated Xcode project (CFBundleIdentifier) +# bundleID: "com.mycompany.myproduct" +# # The display name shown under the app icon (CFBundleDisplayName/CFBundleName) +# displayName: "My Product" +# # The app version to embed in Info.plist (CFBundleShortVersionString/CFBundleVersion) +# version: "0.0.1" +# # The company/organisation name for templates and project settings +# company: "My Company" +# # Additional comments to embed in Info.plist metadata +# comments: "Some Product Comments" + +# Dev mode configuration +dev_mode: + root_path: . + log_level: warn + debounce: 1000 + ignore: + dir: + - .git + - node_modules + - frontend + - bin + file: + - .DS_Store + - .gitignore + - .gitkeep + - "*_test.go" + watched_extension: + - "*.go" + - "*.js" # Watch for changes to JS/TS files included using the //wails:include directive. + - "*.ts" # The frontend directory will be excluded entirely by the setting above. + git_ignore: true + executes: + - cmd: wails3 build DEV=true + type: blocking + - cmd: wails3 task common:dev:frontend + type: background + - cmd: wails3 task run + type: primary + +# File Associations +# More information at: https://v3.wails.io/noit/done/yet +fileAssociations: +# - ext: wails +# name: Wails +# description: Wails Application File +# iconName: wailsFileIcon +# role: Editor +# - ext: jpg +# name: JPEG +# description: Image File +# iconName: jpegFileIcon +# role: Editor +# mimeType: image/jpeg # (optional) + +# Other data +other: + - name: My Other Data \ No newline at end of file diff --git a/gui/build/darwin/Taskfile.yml b/gui/build/darwin/Taskfile.yml new file mode 100644 index 0000000..1217537 --- /dev/null +++ b/gui/build/darwin/Taskfile.yml @@ -0,0 +1,193 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +vars: + # Docker image for cross-compilation (used when building on non-macOS) + CROSS_IMAGE: wails-cross + +tasks: + build: + summary: Builds the application + cmds: + - task: '{{if eq OS "darwin"}}build:native{{else}}build:docker{{end}}' + vars: + ARCH: '{{.ARCH}}' + DEV: '{{.DEV}}' + OUTPUT: '{{.OUTPUT}}' + EXTRA_TAGS: '{{.EXTRA_TAGS}}' + OBFUSCATED: '{{.OBFUSCATED}}' + GARBLE_ARGS: '{{.GARBLE_ARGS}}' + vars: + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + + build:native: + summary: Builds the application natively on macOS + internal: true + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + OBFUSCATED: + ref: .OBFUSCATED + DEV: + ref: .DEV + - task: common:generate:icons + preconditions: + - sh: '{{if eq .OBFUSCATED "true"}}command -v garble >/dev/null 2>&1{{else}}true{{end}}' + msg: "garble is required for obfuscated builds. Install it with: go install mvdan.cc/garble@v0.16.0 (requires Go 1.24+). See https://github.com/burrowers/garble/releases for version/toolchain compatibility." + cmds: + - '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o "{{.OUTPUT}}"' + vars: + BUILD_FLAGS: '{{if eq .DEV "true"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + env: + GOOS: darwin + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default ARCH}}' + CGO_CFLAGS: "-mmacosx-version-min=12.0" + CGO_LDFLAGS: "-mmacosx-version-min=12.0" + MACOSX_DEPLOYMENT_TARGET: "12.0" + + build:docker: + summary: Cross-compiles for macOS using Docker (for Linux/Windows hosts) + internal: true + deps: + - task: common:build:frontend + vars: + OBFUSCATED: + ref: .OBFUSCATED + - task: common:generate:icons + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required for cross-compilation. Please install Docker." + - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1 + msg: | + Docker image '{{.CROSS_IMAGE}}' not found. + Build it first: wails3 task setup:docker + cmds: + - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.DOCKER_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{if eq .OBFUSCATED "true"}}-e OBFUSCATED=true{{end}} {{if .GARBLE_ARGS}}-e GARBLE_ARGS="{{.GARBLE_ARGS}}"{{end}} {{.CROSS_IMAGE}} darwin {{.DOCKER_ARCH}} + - cmd: docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin + platforms: [linux, darwin] + - mkdir -p {{.BIN_DIR}} + - mv "bin/{{.APP_NAME}}-darwin-{{.DOCKER_ARCH}}" "{{.OUTPUT}}" + vars: + DOCKER_ARCH: '{{if eq .ARCH "arm64"}}arm64{{else if eq .ARCH "amd64"}}amd64{{else}}arm64{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + # Generate Docker volume mounts: Go module cache + go.mod replace directives + # Uses wails3 tool docker-mounts for cross-platform compatibility (Windows/Linux/macOS) + DOCKER_MOUNTS: + sh: 'wails3 tool docker-mounts' + + build:universal: + summary: Builds darwin universal binary (arm64 + amd64) + deps: + - task: build + vars: + ARCH: amd64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" + - task: build + vars: + ARCH: arm64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + cmds: + - task: '{{if eq OS "darwin"}}build:universal:lipo:native{{else}}build:universal:lipo:go{{end}}' + + build:universal:lipo:native: + summary: Creates universal binary using native lipo (macOS) + internal: true + cmds: + - lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + - rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + + build:universal:lipo:go: + summary: Creates universal binary using wails3 tool lipo (Linux/Windows) + internal: true + cmds: + - wails3 tool lipo -output "{{.BIN_DIR}}/{{.APP_NAME}}" -input "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" -input "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + - rm -f "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + + package: + summary: Packages the application into a `.app` bundle + deps: + - task: build + cmds: + - task: create:app:bundle + + package:universal: + summary: Packages darwin universal binary (arm64 + amd64) + deps: + - task: build:universal + cmds: + - task: create:app:bundle + + + create:app:bundle: + summary: Creates an `.app` bundle + cmds: + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS" + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" + - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" + - | + if [ -f build/darwin/Assets.car ]; then + cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" + fi + - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS" + - cp build/darwin/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents" + - task: '{{if eq OS "darwin"}}codesign:adhoc{{else}}codesign:skip{{end}}' + + codesign:adhoc: + summary: Ad-hoc signs the app bundle (macOS only) + internal: true + cmds: + - codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app" + + codesign:skip: + summary: Skips codesigning when cross-compiling + internal: true + cmds: + - 'echo "Skipping codesign (not available on {{OS}}). Sign the .app on macOS before distribution."' + + run: + cmds: + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS" + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" + - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" + - | + if [ -f build/darwin/Assets.car ]; then + cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" + fi + - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS" + - cp "build/darwin/Info.dev.plist" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist" + - codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + - '"{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}"' + + sign: + summary: Signs the application bundle with Developer ID + desc: | + Signs the .app bundle for distribution. + Uses signing identity from `wails3 setup` (stored in ~/.config/wails/defaults.yaml). + Override with: task darwin:sign -- --identity "Developer ID Application: ..." + deps: + - task: package + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" {{.CLI_ARGS}} + + sign:notarize: + summary: Signs and notarizes the application bundle + desc: | + Signs the .app bundle and submits it for notarization. + Uses signing identity and keychain profile from `wails3 setup`. + + First-time setup: + wails3 setup + deps: + - task: package + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --notarize {{.CLI_ARGS}} diff --git a/gui/build/windows/Taskfile.yml b/gui/build/windows/Taskfile.yml new file mode 100644 index 0000000..9cb4e35 --- /dev/null +++ b/gui/build/windows/Taskfile.yml @@ -0,0 +1,195 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +vars: + # Signing configuration - edit these values for your project + # SIGN_CERTIFICATE: "path/to/certificate.pfx" + # SIGN_THUMBPRINT: "certificate-thumbprint" # Alternative to SIGN_CERTIFICATE + # TIMESTAMP_SERVER: "http://timestamp.digicert.com" + # + # Password is stored securely in system keychain. Run: wails3 setup signing + + # Docker image for cross-compilation with CGO (used when CGO_ENABLED=1 on non-Windows) + CROSS_IMAGE: wails-cross + +tasks: + build: + summary: Builds the application for Windows + cmds: + # Auto-detect CGO: if CGO_ENABLED=1, use Docker; otherwise use native Go cross-compile + - task: '{{if and (ne OS "windows") (eq .CGO_ENABLED "1")}}build:docker{{else}}build:native{{end}}' + vars: + ARCH: '{{.ARCH}}' + DEV: '{{.DEV}}' + EXTRA_TAGS: '{{.EXTRA_TAGS}}' + OBFUSCATED: '{{.OBFUSCATED}}' + GARBLE_ARGS: '{{.GARBLE_ARGS}}' + vars: + # Default to CGO_ENABLED=0 if not explicitly set + CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}' + + build:native: + summary: Builds the application using native Go cross-compilation + internal: true + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + OBFUSCATED: + ref: .OBFUSCATED + DEV: + ref: .DEV + - task: common:generate:icons + preconditions: + - sh: '{{if eq .OBFUSCATED "true"}}command -v garble >/dev/null 2>&1{{else}}true{{end}}' + msg: "garble is required for obfuscated builds. Install it with: go install mvdan.cc/garble@v0.16.0 (requires Go 1.24+). See https://github.com/burrowers/garble/releases for version/toolchain compatibility." + cmds: + - task: generate:syso + vars: + ARCH: '{{.ARCH}}' + - '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}.exe"' + - cmd: powershell Remove-item *.syso + platforms: [windows] + - cmd: rm -f *.syso + platforms: [linux, darwin] + vars: + BUILD_FLAGS: '{{if eq .DEV "true"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{end}}' + env: + GOOS: windows + CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}' + GOARCH: '{{.ARCH | default ARCH}}' + + build:docker: + summary: Cross-compiles for Windows using Docker with Zig (for CGO builds on non-Windows) + internal: true + deps: + - task: common:build:frontend + vars: + OBFUSCATED: + ref: .OBFUSCATED + - task: common:generate:icons + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required for CGO cross-compilation. Please install Docker." + - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1 + msg: | + Docker image '{{.CROSS_IMAGE}}' not found. + Build it first: wails3 task setup:docker + cmds: + - task: generate:syso + vars: + ARCH: '{{.ARCH}}' + - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.DOCKER_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{if eq .OBFUSCATED "true"}}-e OBFUSCATED=true{{end}} {{if .GARBLE_ARGS}}-e GARBLE_ARGS="{{.GARBLE_ARGS}}"{{end}} {{.CROSS_IMAGE}} windows {{.DOCKER_ARCH}} + - cmd: docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin + platforms: [linux, darwin] + - rm -f *.syso + vars: + DOCKER_ARCH: '{{.ARCH | default "amd64"}}' + # Generate Docker volume mounts: Go module cache + go.mod replace directives + # Uses wails3 tool docker-mounts for cross-platform compatibility (Windows/Linux/macOS) + DOCKER_MOUNTS: + sh: 'wails3 tool docker-mounts' + + package: + summary: Packages the application + cmds: + - task: '{{if eq (.FORMAT | default "nsis") "msix"}}create:msix:package{{else}}create:nsis:installer{{end}}' + vars: + INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}' + vars: + FORMAT: '{{.FORMAT | default "nsis"}}' + INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}' + + generate:syso: + summary: Generates Windows `.syso` file + dir: build + cmds: + - wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso + vars: + ARCH: '{{.ARCH | default ARCH}}' + + create:nsis:installer: + summary: Creates an NSIS installer + dir: build/windows/nsis + deps: + - task: build + preconditions: + - sh: '[ "{{.INSTALL_SCOPE}}" = "user" ] || [ "{{.INSTALL_SCOPE}}" = "machine" ]' + msg: "INSTALL_SCOPE must be 'user' or 'machine', got '{{.INSTALL_SCOPE}}'" + cmds: + # Create the Microsoft WebView2 bootstrapper if it doesn't exist + - wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis" + - | + {{if eq OS "windows"}} + makensis {{if eq .INSTALL_SCOPE "user"}}-DWAILS_INSTALL_SCOPE=user -DREQUEST_EXECUTION_LEVEL=user {{end}}-DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi + {{else}} + makensis {{if eq .INSTALL_SCOPE "user"}}-DWAILS_INSTALL_SCOPE=user -DREQUEST_EXECUTION_LEVEL=user {{end}}-DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi + {{end}} + vars: + ARCH: '{{.ARCH | default ARCH}}' + ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}' + INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}' + + create:msix:package: + summary: Creates an MSIX package + deps: + - task: build + cmds: + - |- + wails3 tool msix \ + --config "{{.ROOT_DIR}}/wails.json" \ + --name "{{.APP_NAME}}" \ + --executable "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" \ + --arch "{{.ARCH}}" \ + --out "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}-{{.ARCH}}.msix" \ + {{if .CERT_PATH}}--cert "{{.CERT_PATH}}"{{end}} \ + {{if .PUBLISHER}}--publisher "{{.PUBLISHER}}"{{end}} \ + {{if .USE_MSIX_TOOL}}--use-msix-tool{{else}}--use-makeappx{{end}} + vars: + ARCH: '{{.ARCH | default ARCH}}' + CERT_PATH: '{{.CERT_PATH | default ""}}' + PUBLISHER: '{{.PUBLISHER | default ""}}' + USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}' + + install:msix:tools: + summary: Installs tools required for MSIX packaging + cmds: + - wails3 tool msix-install-tools + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}.exe' + + sign: + summary: Signs the Windows executable + desc: | + Signs the .exe with an Authenticode certificate. + Set SIGN_CERTIFICATE or SIGN_THUMBPRINT to override the certificate + configured globally via `wails3 setup` (~/.config/wails/defaults.yaml). + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: build + cmds: + # Certificate vars are optional: if unset, `wails3 tool sign` falls back to + # the certificate configured globally via `wails3 setup`. + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.exe" {{if .SIGN_CERTIFICATE}}--certificate "{{.SIGN_CERTIFICATE}}"{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint "{{.SIGN_THUMBPRINT}}"{{end}} {{if .TIMESTAMP_SERVER}}--timestamp "{{.TIMESTAMP_SERVER}}"{{end}} + + sign:installer: + summary: Signs the NSIS installer + desc: | + Creates and signs the NSIS installer. + Set SIGN_CERTIFICATE or SIGN_THUMBPRINT to override the certificate + configured globally via `wails3 setup` (~/.config/wails/defaults.yaml). + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: create:nsis:installer + vars: + INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}' + vars: + INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}' + cmds: + - wails3 tool sign --input "build/windows/nsis/{{.APP_NAME}}-installer.exe" {{if .SIGN_CERTIFICATE}}--certificate "{{.SIGN_CERTIFICATE}}"{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint "{{.SIGN_THUMBPRINT}}"{{end}} {{if .TIMESTAMP_SERVER}}--timestamp "{{.TIMESTAMP_SERVER}}"{{end}} From f8c5269fe9bfc16fb42e1de26c88d609fdf3bc83 Mon Sep 17 00:00:00 2001 From: Amin Mesbahi Date: Sun, 19 Jul 2026 20:03:22 +0200 Subject: [PATCH 07/14] fix(ci): remove Wails v2 -nsis flag from v3 build command --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ecac44..7c33028 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,7 +131,7 @@ jobs: - name: Build GUI working-directory: gui - run: wails3 build -nsis -o Skell-windows-amd64.exe + run: wails3 build -o Skell-windows-amd64.exe - name: Upload GUI artifact uses: actions/upload-artifact@v7 From 70cf4ce75af67dfb8d376b36e1d08f82c90b150c Mon Sep 17 00:00:00 2001 From: Amin Mesbahi Date: Sun, 19 Jul 2026 20:24:36 +0200 Subject: [PATCH 08/14] fix(ci): remove Wails v2 -o flag, output name from wails.json --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c33028..83e461b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,7 +131,7 @@ jobs: - name: Build GUI working-directory: gui - run: wails3 build -o Skell-windows-amd64.exe + run: wails3 build - name: Upload GUI artifact uses: actions/upload-artifact@v7 From eb5bb994e5ac59ba23b4a8efd1ae1f4655b2d26c Mon Sep 17 00:00:00 2001 From: Amin Mesbahi Date: Sun, 19 Jul 2026 20:33:20 +0200 Subject: [PATCH 09/14] fix(build): remove ios/android includes, Wails v3 doesn't need them --- .gitignore | 3 --- gui/Taskfile.yml | 2 -- 2 files changed, 5 deletions(-) diff --git a/.gitignore b/.gitignore index 3329f7a..a96b66c 100644 --- a/.gitignore +++ b/.gitignore @@ -58,11 +58,8 @@ gui/frontend/.playwright/ # Wails build outputs gui/build/bin/ -gui/build/android/ gui/build/appicon.icon/ gui/build/docker/ -gui/build/ios/ -gui/build/linux/ gui/build/darwin/Assets.car gui/build/darwin/icons.icns gui/build/windows/msix/ diff --git a/gui/Taskfile.yml b/gui/Taskfile.yml index b5ce2aa..9092776 100644 --- a/gui/Taskfile.yml +++ b/gui/Taskfile.yml @@ -15,8 +15,6 @@ includes: windows: ./build/windows/Taskfile.yml darwin: ./build/darwin/Taskfile.yml linux: ./build/linux/Taskfile.yml - ios: ./build/ios/Taskfile.yml - android: ./build/android/Taskfile.yml tasks: build: From 560e8cba28184d46a362ba8adffa15cf34f8c210 Mon Sep 17 00:00:00 2001 From: Amin Mesbahi Date: Sun, 19 Jul 2026 20:36:41 +0200 Subject: [PATCH 10/14] fix(build): recreate linux Taskfile after deleting generated build dirs --- gui/build/linux/Taskfile.yml | 85 ++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 gui/build/linux/Taskfile.yml diff --git a/gui/build/linux/Taskfile.yml b/gui/build/linux/Taskfile.yml new file mode 100644 index 0000000..2d9071a --- /dev/null +++ b/gui/build/linux/Taskfile.yml @@ -0,0 +1,85 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +vars: + CROSS_IMAGE: wails-cross + +tasks: + build: + summary: Builds the application for Linux + cmds: + - task: '{{if eq OS "linux"}}build:native{{else}}build:docker{{end}}' + vars: + ARCH: '{{.ARCH}}' + DEV: '{{.DEV}}' + OUTPUT: '{{.OUTPUT}}' + EXTRA_TAGS: '{{.EXTRA_TAGS}}' + OBFUSCATED: '{{.OBFUSCATED}}' + GARBLE_ARGS: '{{.GARBLE_ARGS}}' + vars: + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + + build:native: + summary: Builds the application natively on Linux + internal: true + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + OBFUSCATED: + ref: .OBFUSCATED + DEV: + ref: .DEV + - task: common:generate:icons + preconditions: + - sh: go version + msg: "Go is required. Install from https://go.dev/dl/" + cmds: + - go build + -buildvcs=false + -gcflags=all="-l" + {{if eq .DEV "true"}}-tags dev{{end}} + {{if .EXTRA_TAGS}}-tags '{{.EXTRA_TAGS}}'{{end}} + -o '{{.OUTPUT}}' + + build:docker: + summary: Cross-compiles Linux binary using Docker + internal: true + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + OBFUSCATED: + ref: .OBFUSCATED + DEV: + ref: .DEV + preconditions: + - sh: docker version + msg: "Docker is required for cross-compilation. Install from https://docs.docker.com/get-docker/" + cmds: + - docker run --rm + -v {{.ROOT_DIR}}:/app + -w /app/gui + -e CGO_ENABLED=1 + -e GOOS=linux + -e GOARCH={{.ARCH}} + {{.CROSS_IMAGE}} + go build + -buildvcs=false + -o '{{.OUTPUT}}' + + package: + summary: Packages the application for Linux + cmds: + - task: build + vars: + ARCH: '{{.ARCH}}' + - mkdir -p {{.BIN_DIR}}/{{.APP_NAME}} + - cp {{.OUTPUT}} {{.BIN_DIR}}/{{.APP_NAME}}/{{.APP_NAME}} From 9dac51c699bb90b08a60d0b89e1edb9b4e4cab4c Mon Sep 17 00:00:00 2001 From: Amin Mesbahi Date: Sun, 19 Jul 2026 20:38:03 +0200 Subject: [PATCH 11/14] fix(security): update x/crypto, x/net, x/image to fix 15 dependabot alerts --- gui/go.mod | 147 +------------------- gui/go.sum | 399 ++--------------------------------------------------- 2 files changed, 18 insertions(+), 528 deletions(-) diff --git a/gui/go.mod b/gui/go.mod index 3d11bd2..7595eca 100644 --- a/gui/go.mod +++ b/gui/go.mod @@ -7,161 +7,22 @@ toolchain go1.26.5 require ( github.com/BurntSushi/toml v1.6.0 github.com/stretchr/testify v1.11.1 - github.com/wailsapp/wails/v2 v2.12.0 github.com/wailsapp/wails/v3 v3.0.0-alpha2.117 ) require ( - al.essio.dev/pkg/shellescape v1.6.0 // indirect - atomicgo.dev/cursor v0.2.0 // indirect - atomicgo.dev/keyboard v0.2.9 // indirect - atomicgo.dev/schedule v0.1.0 // indirect - dario.cat/mergo v1.0.2 // indirect - git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect - github.com/AlekSi/pointer v1.2.0 // indirect - github.com/Ladicle/tabwriter v1.0.0 // indirect - github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.4.0 // indirect - github.com/Masterminds/sprig/v3 v3.3.0 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/adrg/xdg v0.5.3 // indirect - github.com/alecthomas/chroma/v2 v2.23.1 // indirect - github.com/atotto/clipboard v0.1.4 // indirect - github.com/atterpac/refresh v1.0.0 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/aymerick/douceur v0.2.0 // indirect - github.com/bep/debounce v1.2.1 // indirect - github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect - github.com/catppuccin/go v0.3.0 // indirect - github.com/cavaliergopher/cpio v1.0.1 // indirect - github.com/chainguard-dev/git-urls v1.0.2 // indirect - github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect - github.com/charmbracelet/bubbletea v1.3.10 // indirect - github.com/charmbracelet/colorprofile v0.4.1 // indirect - github.com/charmbracelet/glamour v0.10.0 // indirect - github.com/charmbracelet/huh v0.8.0 // indirect - github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect - github.com/charmbracelet/x/ansi v0.11.4 // indirect - github.com/charmbracelet/x/cellbuf v0.0.14 // indirect - github.com/charmbracelet/x/exp/slice v0.0.0-20260122224438-b01af16209d9 // indirect - github.com/charmbracelet/x/exp/strings v0.0.0-20260122224438-b01af16209d9 // indirect - github.com/charmbracelet/x/term v0.2.2 // indirect - github.com/clipperhouse/displaywidth v0.7.0 // indirect - github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.4.0 // indirect - github.com/cloudflare/circl v1.6.3 // indirect github.com/coder/websocket v1.8.14 // indirect - github.com/containerd/console v1.0.5 // indirect - github.com/cyphar/filepath-securejoin v0.6.1 // indirect - github.com/danieljoos/wincred v1.2.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dlclark/regexp2 v1.11.5 // indirect - github.com/dominikbraun/graph v0.23.0 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/emirpasic/gods v1.18.1 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.9.0 // indirect - github.com/go-git/go-git/v5 v5.19.1 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/go-task/template v0.2.0 // indirect - github.com/gobwas/glob v0.2.3 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect - github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/google/rpmpack v0.7.1 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/gookit/color v1.6.0 // indirect - github.com/goreleaser/chglog v0.7.4 // indirect - github.com/goreleaser/fileglob v1.4.0 // indirect - github.com/goreleaser/nfpm/v2 v2.44.1 // indirect - github.com/gorilla/css v1.0.1 // indirect - github.com/gorilla/websocket v1.5.3 // indirect - github.com/huandu/xstrings v1.5.0 // indirect - github.com/jackmordaunt/icns/v2 v2.2.7 // indirect - github.com/jaypipes/ghw v0.21.3 // indirect - github.com/jaypipes/pcidb v1.1.1 // indirect - github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect - github.com/joho/godotenv v1.5.1 // indirect - github.com/kevinburke/ssh_config v1.4.0 // indirect - github.com/klauspost/compress v1.18.3 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/klauspost/pgzip v1.2.6 // indirect - github.com/konoui/go-qsort v0.1.0 // indirect - github.com/konoui/lipo v0.10.0 // indirect - github.com/labstack/echo/v4 v4.15.2 // indirect - github.com/labstack/gommon v0.5.0 // indirect - github.com/leaanthony/clir v1.7.0 // indirect - github.com/leaanthony/go-ansi-parser v1.6.1 // indirect - github.com/leaanthony/gosod v1.0.4 // indirect - github.com/leaanthony/slicer v1.6.0 // indirect - github.com/leaanthony/u v1.1.1 // indirect - github.com/leaanthony/winicon v1.0.0 // indirect - github.com/lithammer/fuzzysearch v1.1.8 // indirect - github.com/lmittmann/tint v1.1.3 // indirect - github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.22 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect - github.com/mattn/go-zglob v0.0.6 // indirect - github.com/microcosm-cc/bluemonday v1.0.27 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.16.0 // indirect - github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect - github.com/pjbgf/sha1cd v0.6.0 // indirect - github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/pterm/pterm v0.12.82 // indirect - github.com/radovskyb/watcher v1.0.7 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/rjeczalik/notify v0.9.3 // indirect - github.com/sajari/fuzzy v1.0.0 // indirect - github.com/samber/lo v1.53.0 // indirect - github.com/sergi/go-diff v1.4.0 // indirect - github.com/shopspring/decimal v1.4.0 // indirect - github.com/skeema/knownhosts v1.3.2 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect - github.com/tc-hib/winres v0.3.1 // indirect - github.com/tkrajina/go-reflector v0.5.8 // indirect - github.com/ulikunitz/xz v0.5.15 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/wailsapp/go-webview2 v1.0.23 // indirect - github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/task/v3 v3.40.1-patched3 // indirect - github.com/xanzy/ssh-agent v0.3.3 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yuin/goldmark v1.7.16 // indirect - github.com/yuin/goldmark-emoji v1.0.6 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - github.com/zalando/go-keyring v0.2.6 // indirect - github.com/zeebo/xxh3 v1.1.0 // indirect - gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.51.0 // indirect - golang.org/x/exp/typeparams v0.0.0-20260112195511-716be5621a96 // indirect - golang.org/x/image v0.40.0 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.54.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.44.0 // indirect - golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.0 // indirect - golang.org/x/tools v0.44.0 // indirect - gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + golang.org/x/sys v0.47.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 // indirect - mvdan.cc/sh/v3 v3.12.0 // indirect ) diff --git a/gui/go.sum b/gui/go.sum index 310537d..34f8194 100644 --- a/gui/go.sum +++ b/gui/go.sum @@ -1,422 +1,51 @@ -al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= -al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= -atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= -atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= -atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= -atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= -atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= -atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= -dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= -dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA= -git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= -github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= -github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg= -github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4= -github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= -github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= -github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= -github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= -github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= -github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= -github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= -github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= -github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= -github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= -github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= -github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= -github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= -github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= -github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/atterpac/refresh v1.0.0 h1:IK/rh3w5cD7nb6GuqzfScIdNuAz/E0sZz10k1pioIFE= -github.com/atterpac/refresh v1.0.0/go.mod h1:+vQ8OHgGmZ7wwoZfxxkT6Nr/gKA8j78Rbt+qcLLDEoc= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= -github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= -github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= -github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= -github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= -github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= -github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= -github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= -github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ= -github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o= -github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= -github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= -github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= -github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= -github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= -github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= -github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= -github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= -github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= -github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= -github.com/charmbracelet/x/ansi v0.11.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI= -github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4= -github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4= -github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA= -github.com/charmbracelet/x/exp/slice v0.0.0-20260122224438-b01af16209d9 h1:BBTx26Fy+CW9U3kLiWBuWn9pI9C1NybaS+p/AZeAOkA= -github.com/charmbracelet/x/exp/slice v0.0.0-20260122224438-b01af16209d9/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= -github.com/charmbracelet/x/exp/strings v0.0.0-20260122224438-b01af16209d9 h1:JevRYfkTT0sN9OIXAOncYNC0cTP1Gml/0mCSnsmRkRk= -github.com/charmbracelet/x/exp/strings v0.0.0-20260122224438-b01af16209d9/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8= -github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= -github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= -github.com/clipperhouse/displaywidth v0.7.0 h1:QNv1GYsnLX9QBrcWUtMlogpTXuM5FVnBwKWp1O5NwmE= -github.com/clipperhouse/displaywidth v0.7.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= -github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.4.0 h1:RXqE/l5EiAbA4u97giimKNlmpvkmz+GrBVTelsoXy9g= -github.com/clipperhouse/uax29/v2 v2.4.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= -github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= -github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= -github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= -github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= -github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= -github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= -github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= -github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= -github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= -github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= -github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= -github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-task/template v0.2.0 h1:xW7ek0o65FUSTbKcSNeg2Vyf/I7wYXFgLUznptvviBE= -github.com/go-task/template v0.2.0/go.mod h1:dbdoUb6qKnHQi1y6o+IdIrs0J4o/SEhSTA6bbzZmdtc= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/rpmpack v0.7.1 h1:YdWh1IpzOjBz60Wvdw0TU0A5NWP+JTVHA5poDqwMO2o= -github.com/google/rpmpack v0.7.1/go.mod h1:h1JL16sUTWCLI/c39ox1rDaTBo3BXUQGjczVJyK4toU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= -github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= -github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA= -github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= -github.com/goreleaser/chglog v0.7.4 h1:3pnNt/XCrUcAOq+KC91Azlgp5CRv4GHo1nl8Aws7OzI= -github.com/goreleaser/chglog v0.7.4/go.mod h1:dTVoZZagTz7hHdWaZ9OshHntKiF44HbWIHWxYJQ/h0Y= -github.com/goreleaser/fileglob v1.4.0 h1:Y7zcUnzQjT1gbntacGAkIIfLv+OwojxTXBFxjSFoBBs= -github.com/goreleaser/fileglob v1.4.0/go.mod h1:1pbHx7hhmJIxNZvm6fi6WVrnP0tndq6p3ayWdLn1Yf8= -github.com/goreleaser/nfpm/v2 v2.44.1 h1:g+QNjkEx+C2Zu8dB48t9da/VfV0CWS5TMjxT8HG1APY= -github.com/goreleaser/nfpm/v2 v2.44.1/go.mod h1:drIYLqkla9SaOLbSnaFOmSIv5LXGfhHcbK54st97b4s= -github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= -github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= -github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/jackmordaunt/icns v1.0.0 h1:RYSxplerf/l/DUd09AHtITwckkv/mqjVv4DjYdPmAMQ= -github.com/jackmordaunt/icns/v2 v2.2.7 h1:K/RbfvuzjmjVY5y4g+XENRs8ZZatwz4YnLHypa2KwQg= -github.com/jackmordaunt/icns/v2 v2.2.7/go.mod h1:ovoTxGguSuoUGKMk5Nn3R7L7BgMQkylsO+bblBuI22A= -github.com/jaypipes/ghw v0.21.3 h1:v5mUHM+RN854Vqmk49Uh213jyUA4+8uqaRajlYESsh8= -github.com/jaypipes/ghw v0.21.3/go.mod h1:GPrvwbtPoxYUenr74+nAnWbardIZq600vJDD5HnPsPE= -github.com/jaypipes/pcidb v1.1.1 h1:QmPhpsbmmnCwZmHeYAATxEaoRuiMAJusKYkUncMC0ro= -github.com/jaypipes/pcidb v1.1.1/go.mod h1:x27LT2krrUgjf875KxQXKB0Ha/YXLdZRVmw6hH0G7g8= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= -github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= -github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= -github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= -github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/konoui/go-qsort v0.1.0 h1:0Os/0X0Fce6B54jqN26aR+J5uOExN+0t7nb9zs6zzzE= -github.com/konoui/go-qsort v0.1.0/go.mod h1:UOsvdDPBzyQDk9Tb21hETK6KYXGYQTnoZB5qeKA1ARs= -github.com/konoui/lipo v0.10.0 h1:1P2VkBSB6I38kgmyznvAjy9gmAqybK22pJt9iyx5CgY= -github.com/konoui/lipo v0.10.0/go.mod h1:R+0EgDVrLKKS37SumAO8zhpEprjjoKEkrT3QqKQE35k= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/labstack/echo/v4 v4.15.2 h1:nnh2sCzGCVYnU+wCisMPiYapEg/QVo/gcI9ePKg5/T4= -github.com/labstack/echo/v4 v4.15.2/go.mod h1:Xzp1Ns1RA2c9fY7nSgUJkpkUZGNbEIVHZbtbOMPktBI= -github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c= -github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0= -github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0= -github.com/leaanthony/clir v1.7.0 h1:xiAnhl7ryPwuH3ERwPWZp/pCHk8wTeiwuAOt6MiNyAw= -github.com/leaanthony/clir v1.7.0/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0= -github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= -github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= -github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= -github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= -github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= -github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= -github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= -github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= -github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= -github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= -github.com/leaanthony/winicon v1.0.0 h1:ZNt5U5dY71oEoKZ97UVwJRT4e+5xo5o/ieKuHuk8NqQ= -github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU= -github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= -github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= -github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I= -github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= -github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= -github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A= -github.com/mattn/go-zglob v0.0.6/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY= -github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= -github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= -github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= -github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= -github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= -github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= -github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= -github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= -github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= -github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= -github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= -github.com/pterm/pterm v0.12.82 h1:+D9wYhCaeaK0FIQoZtqbNQuNpe2lB2tajKKsTd5paVQ= -github.com/pterm/pterm v0.12.82/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw= -github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE= -github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= -github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= -github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY= -github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo= -github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= -github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= -github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= -github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tc-hib/winres v0.3.1 h1:CwRjEGrKdbi5CvZ4ID+iyVhgyfatxFoizjPhzez9Io4= -github.com/tc-hib/winres v0.3.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= -github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= -github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= -github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= -github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0= -github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= -github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= -github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -github.com/wailsapp/task/v3 v3.40.1-patched3 h1:i6O1WNdSur9CGaiMDIYGjsmj/qS4465zqv+WEs6sPRs= -github.com/wailsapp/task/v3 v3.40.1-patched3/go.mod h1:jIP48r8ftoSQNlxFP4+aEnkvGQqQXqCnRi/B7ROaecE= -github.com/wailsapp/wails/v2 v2.12.0 h1:BHO/kLNWFHYjCzucxbzAYZWUjub1Tvb4cSguQozHn5c= -github.com/wailsapp/wails/v2 v2.12.0/go.mod h1:mo1bzK1DEJrobt7YrBjgxvb5Sihb1mhAY09hppbibQg= github.com/wailsapp/wails/v3 v3.0.0-alpha2.117 h1:udyjqPG3AIgkod5QDR/WblCkpV8R86BFPSrsWxSyt5Y= github.com/wailsapp/wails/v3 v3.0.0-alpha2.117/go.mod h1:74WH2FScMsgucZvHHvv7eOefDXCm/CjuIxqhhZgPhKg= -github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= -github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= -github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= -github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= -github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= -github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= -github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= -gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8= -gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= -golang.org/x/exp/typeparams v0.0.0-20260112195511-716be5621a96 h1:RMc8anw0hCPcg5CZYN2PEQ8nMwosk461R6vFwPrCFVg= -golang.org/x/exp/typeparams v0.0.0-20260112195511-716be5621a96/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= -golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8= -golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 h1:eeH1AIcPvSc0Z25ThsYF+Xoqbn0CI/YnXVYoTLFdGQw= -howett.net/plist v1.0.2-0.20250314012144-ee69052608d9/go.mod h1:fyFX5Hj5tP1Mpk8obqA9MZgXT416Q5711SDT7dQLTLk= -mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI= -mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg= From 90bf95a3a5a3f259d98591938a872de161f42d55 Mon Sep 17 00:00:00 2001 From: Amin Mesbahi Date: Sun, 19 Jul 2026 20:43:33 +0200 Subject: [PATCH 12/14] fix: correct Wails bindings import path wailsjs -> bindings, update wailsjsdir --- .gitignore | 1 - gui/frontend/src/components/Sidebar.tsx | 2 +- gui/frontend/src/lib/skell.ts | 4 ++-- gui/frontend/src/pages/ContributeMetadata.tsx | 4 ++-- gui/frontend/src/pages/Repositories.tsx | 2 +- gui/frontend/src/pages/Settings.tsx | 2 +- gui/wails.json | 2 +- 7 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index a96b66c..49ac511 100644 --- a/.gitignore +++ b/.gitignore @@ -65,7 +65,6 @@ gui/build/darwin/icons.icns gui/build/windows/msix/ gui/build/windows/nsis/ gui/build/windows/icon.ico -gui/frontend/wailsjs/ # Wails dev artifacts gui/node_modules/ diff --git a/gui/frontend/src/components/Sidebar.tsx b/gui/frontend/src/components/Sidebar.tsx index 4107970..109d281 100644 --- a/gui/frontend/src/components/Sidebar.tsx +++ b/gui/frontend/src/components/Sidebar.tsx @@ -1,5 +1,5 @@ import { NavLink, useNavigate } from "react-router-dom"; -import { SelectDirectory } from "../../wailsjs/skell-gui/app"; +import { SelectDirectory } from "../../bindings/skell-gui/app"; import { Home, FolderKanban, diff --git a/gui/frontend/src/lib/skell.ts b/gui/frontend/src/lib/skell.ts index a514aa1..545f59f 100644 --- a/gui/frontend/src/lib/skell.ts +++ b/gui/frontend/src/lib/skell.ts @@ -15,8 +15,8 @@ import { PreviewRegistrySkill, ValidateSkill, SkellPresent as _SkellPresent, -} from "../../wailsjs/skell-gui/app"; -import type * as wailsModels from "../../wailsjs/skell-gui/models"; +} from "../../bindings/skell-gui/app"; +import type * as wailsModels from "../../bindings/skell-gui/models"; import type { InstalledSkill, RegistrySkill, diff --git a/gui/frontend/src/pages/ContributeMetadata.tsx b/gui/frontend/src/pages/ContributeMetadata.tsx index 86eaaf0..be4cdfb 100644 --- a/gui/frontend/src/pages/ContributeMetadata.tsx +++ b/gui/frontend/src/pages/ContributeMetadata.tsx @@ -9,8 +9,8 @@ import { ExternalLink, Info, } from "lucide-react"; -import { ReadSkillMetadata, ContributeMetadata, ResolveSkillSourceRepoURL } from "../../wailsjs/skell-gui/app"; -import * as wailsModels from "../../wailsjs/skell-gui/models"; +import { ReadSkillMetadata, ContributeMetadata, ResolveSkillSourceRepoURL } from "../../bindings/skell-gui/app"; +import * as wailsModels from "../../bindings/skell-gui/models"; const LIFECYCLE_OPTIONS = [ "draft", diff --git a/gui/frontend/src/pages/Repositories.tsx b/gui/frontend/src/pages/Repositories.tsx index bdbfd20..2ba2f75 100644 --- a/gui/frontend/src/pages/Repositories.tsx +++ b/gui/frontend/src/pages/Repositories.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, useCallback } from "react"; import { useNavigate } from "react-router-dom"; -import { SelectDirectory } from "../../wailsjs/skell-gui/app"; +import { SelectDirectory } from "../../bindings/skell-gui/app"; import { FolderOpen, Plus, diff --git a/gui/frontend/src/pages/Settings.tsx b/gui/frontend/src/pages/Settings.tsx index 5d0049d..03c76e8 100644 --- a/gui/frontend/src/pages/Settings.tsx +++ b/gui/frontend/src/pages/Settings.tsx @@ -20,7 +20,7 @@ import { } from "@/lib/skell"; import type { SkillSource } from "@/lib/types"; import { ConfirmDialog } from "@/components/ConfirmDialog"; -import { SelectDirectory } from "../../wailsjs/skell-gui/app"; +import { SelectDirectory } from "../../bindings/skell-gui/app"; export function Settings() { const { notify } = useUIStore(); diff --git a/gui/wails.json b/gui/wails.json index 25df712..82dbb98 100644 --- a/gui/wails.json +++ b/gui/wails.json @@ -7,7 +7,7 @@ "frontend:dev:watcher": "bun run dev", "frontend:dev:serverUrl": "auto", "frontend:dir": "frontend", - "wailsjsdir": "./frontend", + "wailsjsdir": "./frontend/bindings", "version": "3", "info": { "companyName": "Amin Mesbahi", From 6baee6afead0da0a90da4f8ddce1cfd07f387cdf Mon Sep 17 00:00:00 2001 From: Amin Mesbahi Date: Sun, 19 Jul 2026 20:45:40 +0200 Subject: [PATCH 13/14] chore: re-add wailsjs to gitignore, delete stale generated dir --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 49ac511..a96b66c 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ gui/build/darwin/icons.icns gui/build/windows/msix/ gui/build/windows/nsis/ gui/build/windows/icon.ico +gui/frontend/wailsjs/ # Wails dev artifacts gui/node_modules/ From e44ec7d69fbc51c612a91931d2a9c0a16a13eda8 Mon Sep 17 00:00:00 2001 From: Amin Mesbahi Date: Sun, 19 Jul 2026 20:52:25 +0200 Subject: [PATCH 14/14] fix: update bindings wrappers for Wails v3 CancellablePromise types --- gui/frontend/src/lib/skell.ts | 22 +++++++++++-------- gui/frontend/src/pages/ContributeMetadata.tsx | 18 +++++++-------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/gui/frontend/src/lib/skell.ts b/gui/frontend/src/lib/skell.ts index 545f59f..b02ea66 100644 --- a/gui/frontend/src/lib/skell.ts +++ b/gui/frontend/src/lib/skell.ts @@ -206,16 +206,18 @@ export async function initRepo(repo: string, target?: string): Promise { - return SupportedTargets(); +export async function listSupportedTargets(): Promise { + const result = await SupportedTargets(); + return result ?? []; } -export function detectRepoTargets(repo: string): Promise { - return DetectTargets(repo); +export async function detectRepoTargets(repo: string): Promise { + const result = await DetectTargets(repo); + return result ?? []; } -export function activeRepoTarget(repo: string): Promise { - return ActiveTarget(repo); +export async function activeRepoTarget(repo: string): Promise { + return await ActiveTarget(repo); } export async function searchSkills(opts: { @@ -279,7 +281,8 @@ export async function selfUpdate(): Promise { // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ export async function listSkillSources(): Promise { - return ListSkillSources(); + const result = await ListSkillSources(); + return result ?? []; } export async function addSkillSource(alias: string, url: string): Promise { @@ -298,8 +301,9 @@ export function readFileContent(path: string): Promise { return ReadFileContent(path); } -export function listDirectory(path: string): Promise { - return ListDirectory(path); +export async function listDirectory(path: string): Promise { + const result = await ListDirectory(path); + return result ?? []; } export function getSkellVersion(): Promise { diff --git a/gui/frontend/src/pages/ContributeMetadata.tsx b/gui/frontend/src/pages/ContributeMetadata.tsx index be4cdfb..13dcf33 100644 --- a/gui/frontend/src/pages/ContributeMetadata.tsx +++ b/gui/frontend/src/pages/ContributeMetadata.tsx @@ -10,7 +10,7 @@ import { Info, } from "lucide-react"; import { ReadSkillMetadata, ContributeMetadata, ResolveSkillSourceRepoURL } from "../../bindings/skell-gui/app"; -import * as wailsModels from "../../bindings/skell-gui/models"; +import type { SkillMetadataFields } from "../../bindings/skell-gui/models"; const LIFECYCLE_OPTIONS = [ "draft", @@ -55,7 +55,7 @@ export function ContributeMetadataPage() { return; } ReadSkillMetadata(installedPath) - .then((fields: wailsModels.SkillMetadataFields) => { + .then((fields: SkillMetadataFields) => { setDescription(fields.description ?? ""); setTags(fields.tags ?? ""); setLifecycle(fields.lifecycle || "stable"); @@ -88,14 +88,12 @@ export function ContributeMetadataPage() { setSubmitting(true); setSubmitError(null); try { - const result = await ContributeMetadata( - wailsModels.ContributeParams.createFrom({ - installedPath, - sourceRepo: sourceRepoInput.trim(), - skillName: skillName ?? "", - metadata: wailsModels.SkillMetadataFields.createFrom({ description, tags, lifecycle, owner, version }), - }) - ); + const result = await ContributeMetadata({ + installedPath, + sourceRepo: sourceRepoInput.trim(), + skillName: skillName ?? "", + metadata: { description, tags, lifecycle, owner, version }, + }); if (result.success) { setPrURL(result.prUrl); } else {