hlassiege/panopliepublic Fork
⑂ e2ab7eb
Commits
⬇ Clone ▾
git clone https://git.rickub.com/hlassiege/panoplie.git
git clone ssh://git@rickub.com/hlassiege/panoplie.git

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

Download ZIP

feat(ui): read-only inventory UI (dashboard, scope view, item details, add project)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hlassiege committed 2026-10-23T11:48:20+02:00 Browse files
e2ab7eb parent: 9c75087
added packages/ui/app/app.vue +7 -0
new file mode 100644
@@ -0,0 +1,7 @@
1+<template>
2+ <UApp>
3+ <NuxtLayout>
4+ <NuxtPage />
5+ </NuxtLayout>
6+ </UApp>
7+</template>
new file mode 100644
@@ -0,0 +1,7 @@
1+<template>
2+ <UApp>
3+ <NuxtLayout>
4+ <NuxtPage />
5+ </NuxtLayout>
6+ </UApp>
7+</template>
added packages/ui/app/assets/css/main.css +18 -0
new file mode 100644
@@ -0,0 +1,18 @@
1+@import "tailwindcss";
2+@import "@nuxt/ui";
3+
4+.md-body { font-size: 0.875rem; line-height: 1.6; overflow-wrap: anywhere; }
5+.md-body h1 { font-size: 1.25rem; font-weight: 700; margin: 1rem 0 0.5rem; }
6+.md-body h2 { font-size: 1.1rem; font-weight: 650; margin: 1rem 0 0.5rem; }
7+.md-body h3, .md-body h4 { font-size: 0.95rem; font-weight: 600; margin: 0.75rem 0 0.4rem; }
8+.md-body p { margin: 0.5rem 0; }
9+.md-body ul { list-style: disc; padding-left: 1.25rem; margin: 0.5rem 0; }
10+.md-body ol { list-style: decimal; padding-left: 1.25rem; margin: 0.5rem 0; }
11+.md-body a { color: var(--ui-primary); text-decoration: underline; }
12+.md-body code { background: var(--ui-bg-elevated); padding: 0.1rem 0.3rem; border-radius: 0.25rem; font-size: 0.8rem; }
13+.md-body pre { background: var(--ui-bg-elevated); padding: 0.75rem; border-radius: 0.5rem; overflow-x: auto; margin: 0.5rem 0; }
14+.md-body pre code { background: transparent; padding: 0; }
15+.md-body blockquote { border-left: 3px solid var(--ui-border-accented); padding-left: 0.75rem; color: var(--ui-text-muted); margin: 0.5rem 0; }
16+.md-body table { border-collapse: collapse; margin: 0.5rem 0; }
17+.md-body th, .md-body td { border: 1px solid var(--ui-border); padding: 0.25rem 0.5rem; }
18+.md-body hr { border-color: var(--ui-border); margin: 1rem 0; }
new file mode 100644
@@ -0,0 +1,18 @@
1+@import "tailwindcss";
2+@import "@nuxt/ui";
3+
4+.md-body { font-size: 0.875rem; line-height: 1.6; overflow-wrap: anywhere; }
5+.md-body h1 { font-size: 1.25rem; font-weight: 700; margin: 1rem 0 0.5rem; }
6+.md-body h2 { font-size: 1.1rem; font-weight: 650; margin: 1rem 0 0.5rem; }
7+.md-body h3, .md-body h4 { font-size: 0.95rem; font-weight: 600; margin: 0.75rem 0 0.4rem; }
8+.md-body p { margin: 0.5rem 0; }
9+.md-body ul { list-style: disc; padding-left: 1.25rem; margin: 0.5rem 0; }
10+.md-body ol { list-style: decimal; padding-left: 1.25rem; margin: 0.5rem 0; }
11+.md-body a { color: var(--ui-primary); text-decoration: underline; }
12+.md-body code { background: var(--ui-bg-elevated); padding: 0.1rem 0.3rem; border-radius: 0.25rem; font-size: 0.8rem; }
13+.md-body pre { background: var(--ui-bg-elevated); padding: 0.75rem; border-radius: 0.5rem; overflow-x: auto; margin: 0.5rem 0; }
14+.md-body pre code { background: transparent; padding: 0; }
15+.md-body blockquote { border-left: 3px solid var(--ui-border-accented); padding-left: 0.75rem; color: var(--ui-text-muted); margin: 0.5rem 0; }
16+.md-body table { border-collapse: collapse; margin: 0.5rem 0; }
17+.md-body th, .md-body td { border: 1px solid var(--ui-border); padding: 0.25rem 0.5rem; }
18+.md-body hr { border-color: var(--ui-border); margin: 1rem 0; }
added packages/ui/app/components/AddProjectModal.vue +160 -0
new file mode 100644
@@ -0,0 +1,160 @@
1+<script setup lang="ts">
2+import type { FsListing } from '@skilly/core'
3+
4+const open = defineModel<boolean>('open', { default: false })
5+
6+const { request } = useApi()
7+const { add } = useProjects()
8+const router = useRouter()
9+const toast = useToast()
10+
11+const listing = ref<FsListing | null>(null)
12+const loading = ref(false)
13+const listError = ref<string | null>(null)
14+const pathInput = ref('')
15+const submitting = ref(false)
16+const submitError = ref<string | null>(null)
17+const highlighted = ref<string | null>(null)
18+
19+const crumbs = computed(() => (listing.value?.path ? pathSegments(listing.value.path) : []))
20+
21+async function browse(path?: string) {
22+ loading.value = true
23+ listError.value = null
24+ highlighted.value = null
25+ try {
26+ listing.value = await request<FsListing>('/api/fs/list', { query: { path }, silent: true })
27+ } catch (e) {
28+ listError.value = (e as Error).message
29+ } finally {
30+ loading.value = false
31+ }
32+}
33+
34+async function submit(path: string) {
35+ const p = path.trim()
36+ if (!p) return
37+ submitting.value = true
38+ submitError.value = null
39+ try {
40+ const created = await add(p)
41+ open.value = false
42+ toast.add({ title: 'Project added', description: created.name, icon: 'i-lucide-check', color: 'success' })
43+ await router.push(`/projects/${encodeURIComponent(created.id)}`)
44+ } catch (e) {
45+ const err = e as { status?: number, message: string }
46+ submitError.value = err.status === 409
47+ ? 'This project was already added.'
48+ : err.status === 400
49+ ? `Not a valid folder: ${err.message}`
50+ : err.message
51+ } finally {
52+ submitting.value = false
53+ }
54+}
55+
56+function highlight(path: string) {
57+ highlighted.value = path
58+ pathInput.value = path
59+}
60+
61+watch(open, (v) => {
62+ if (v) {
63+ pathInput.value = ''
64+ submitError.value = null
65+ browse()
66+ }
67+})
68+</script>
69+
70+<template>
71+ <UModal
72+ v-model:open="open"
73+ title="Add project"
74+ description="Browse to a project folder, or paste its path."
75+ :ui="{ content: 'max-w-2xl' }"
76+ >
77+ <template #body>
78+ <div class="space-y-4">
79+ <!-- Paste a path -->
80+ <form class="flex gap-2" @submit.prevent="submit(pathInput)">
81+ <UInput
82+ v-model="pathInput"
83+ icon="i-lucide-folder-input"
84+ placeholder="Paste a folder path, e.g. G:\Dev\my-project"
85+ class="flex-1"
86+ autofocus
87+ @update:model-value="submitError = null"
88+ />
89+ <UButton type="submit" label="Add" :loading="submitting" :disabled="!pathInput.trim()" />
90+ </form>
91+ <UAlert v-if="submitError" color="error" variant="subtle" icon="i-lucide-circle-alert" :title="submitError" />
92+
93+ <!-- Breadcrumb -->
94+ <div class="flex flex-wrap items-center gap-1 text-sm">
95+ <UButton label="This PC" icon="i-lucide-hard-drive" size="xs" color="neutral" variant="ghost" @click="browse()" />
96+ <template v-for="c in crumbs" :key="c.path">
97+ <UIcon name="i-lucide-chevron-right" class="size-3.5 text-dimmed" />
98+ <UButton :label="c.name" size="xs" color="neutral" variant="ghost" @click="browse(c.path)" />
99+ </template>
100+ </div>
101+
102+ <!-- Folder list -->
103+ <div class="h-72 overflow-y-auto rounded-lg border border-default">
104+ <div v-if="loading" class="space-y-1 p-2">
105+ <USkeleton v-for="i in 6" :key="i" class="h-8 w-full" />
106+ </div>
107+ <div v-else-if="listError" class="p-3">
108+ <UAlert color="error" variant="subtle" icon="i-lucide-circle-alert" title="Cannot list this folder" :description="listError" />
109+ </div>
110+ <ul v-else-if="listing" class="divide-y divide-default text-sm">
111+ <li v-if="listing.parent">
112+ <button type="button" class="flex w-full items-center gap-2 px-3 py-2 hover:bg-elevated/60" @click="browse(listing.parent!)">
113+ <UIcon name="i-lucide-corner-left-up" class="size-4 text-muted" />
114+ <span class="text-muted">..</span>
115+ </button>
116+ </li>
117+ <li v-for="e in listing.entries" :key="e.path">
118+ <button
119+ type="button"
120+ class="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-elevated/60"
121+ :class="highlighted === e.path ? 'bg-elevated' : ''"
122+ @click="highlight(e.path)"
123+ @dblclick="browse(e.path)"
124+ >
125+ <UIcon name="i-lucide-folder" class="size-4 shrink-0 text-muted" />
126+ <span class="flex-1 truncate">{{ e.name }}</span>
127+ <UBadge v-if="e.hasAgentConfig" label="agent config" color="primary" variant="subtle" size="sm" icon="i-lucide-sparkles" />
128+ <UButton
129+ icon="i-lucide-chevron-right"
130+ size="xs"
131+ color="neutral"
132+ variant="ghost"
133+ aria-label="Open folder"
134+ @click.stop="browse(e.path)"
135+ />
136+ </button>
137+ </li>
138+ <li v-if="!listing.entries.length" class="px-3 py-8 text-center text-muted">No subfolder here.</li>
139+ </ul>
140+ </div>
141+ </div>
142+ </template>
143+
144+ <template #footer>
145+ <div class="flex w-full items-center justify-between gap-2">
146+ <span class="truncate font-mono text-xs text-muted">{{ listing?.path ?? 'Select a drive or folder' }}</span>
147+ <div class="flex shrink-0 gap-2">
148+ <UButton label="Cancel" color="neutral" variant="outline" @click="open = false" />
149+ <UButton
150+ label="Select this folder"
151+ icon="i-lucide-check"
152+ :disabled="!listing?.path"
153+ :loading="submitting"
154+ @click="submit(listing!.path!)"
155+ />
156+ </div>
157+ </div>
158+ </template>
159+ </UModal>
160+</template>
new file mode 100644
@@ -0,0 +1,160 @@
1+<script setup lang="ts">
2+import type { FsListing } from '@skilly/core'
3+
4+const open = defineModel<boolean>('open', { default: false })
5+
6+const { request } = useApi()
7+const { add } = useProjects()
8+const router = useRouter()
9+const toast = useToast()
10+
11+const listing = ref<FsListing | null>(null)
12+const loading = ref(false)
13+const listError = ref<string | null>(null)
14+const pathInput = ref('')
15+const submitting = ref(false)
16+const submitError = ref<string | null>(null)
17+const highlighted = ref<string | null>(null)
18+
19+const crumbs = computed(() => (listing.value?.path ? pathSegments(listing.value.path) : []))
20+
21+async function browse(path?: string) {
22+ loading.value = true
23+ listError.value = null
24+ highlighted.value = null
25+ try {
26+ listing.value = await request<FsListing>('/api/fs/list', { query: { path }, silent: true })
27+ } catch (e) {
28+ listError.value = (e as Error).message
29+ } finally {
30+ loading.value = false
31+ }
32+}
33+
34+async function submit(path: string) {
35+ const p = path.trim()
36+ if (!p) return
37+ submitting.value = true
38+ submitError.value = null
39+ try {
40+ const created = await add(p)
41+ open.value = false
42+ toast.add({ title: 'Project added', description: created.name, icon: 'i-lucide-check', color: 'success' })
43+ await router.push(`/projects/${encodeURIComponent(created.id)}`)
44+ } catch (e) {
45+ const err = e as { status?: number, message: string }
46+ submitError.value = err.status === 409
47+ ? 'This project was already added.'
48+ : err.status === 400
49+ ? `Not a valid folder: ${err.message}`
50+ : err.message
51+ } finally {
52+ submitting.value = false
53+ }
54+}
55+
56+function highlight(path: string) {
57+ highlighted.value = path
58+ pathInput.value = path
59+}
60+
61+watch(open, (v) => {
62+ if (v) {
63+ pathInput.value = ''
64+ submitError.value = null
65+ browse()
66+ }
67+})
68+</script>
69+
70+<template>
71+ <UModal
72+ v-model:open="open"
73+ title="Add project"
74+ description="Browse to a project folder, or paste its path."
75+ :ui="{ content: 'max-w-2xl' }"
76+ >
77+ <template #body>
78+ <div class="space-y-4">
79+ <!-- Paste a path -->
80+ <form class="flex gap-2" @submit.prevent="submit(pathInput)">
81+ <UInput
82+ v-model="pathInput"
83+ icon="i-lucide-folder-input"
84+ placeholder="Paste a folder path, e.g. G:\Dev\my-project"
85+ class="flex-1"
86+ autofocus
87+ @update:model-value="submitError = null"
88+ />
89+ <UButton type="submit" label="Add" :loading="submitting" :disabled="!pathInput.trim()" />
90+ </form>
91+ <UAlert v-if="submitError" color="error" variant="subtle" icon="i-lucide-circle-alert" :title="submitError" />
92+
93+ <!-- Breadcrumb -->
94+ <div class="flex flex-wrap items-center gap-1 text-sm">
95+ <UButton label="This PC" icon="i-lucide-hard-drive" size="xs" color="neutral" variant="ghost" @click="browse()" />
96+ <template v-for="c in crumbs" :key="c.path">
97+ <UIcon name="i-lucide-chevron-right" class="size-3.5 text-dimmed" />
98+ <UButton :label="c.name" size="xs" color="neutral" variant="ghost" @click="browse(c.path)" />
99+ </template>
100+ </div>
101+
102+ <!-- Folder list -->
103+ <div class="h-72 overflow-y-auto rounded-lg border border-default">
104+ <div v-if="loading" class="space-y-1 p-2">
105+ <USkeleton v-for="i in 6" :key="i" class="h-8 w-full" />
106+ </div>
107+ <div v-else-if="listError" class="p-3">
108+ <UAlert color="error" variant="subtle" icon="i-lucide-circle-alert" title="Cannot list this folder" :description="listError" />
109+ </div>
110+ <ul v-else-if="listing" class="divide-y divide-default text-sm">
111+ <li v-if="listing.parent">
112+ <button type="button" class="flex w-full items-center gap-2 px-3 py-2 hover:bg-elevated/60" @click="browse(listing.parent!)">
113+ <UIcon name="i-lucide-corner-left-up" class="size-4 text-muted" />
114+ <span class="text-muted">..</span>
115+ </button>
116+ </li>
117+ <li v-for="e in listing.entries" :key="e.path">
118+ <button
119+ type="button"
120+ class="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-elevated/60"
121+ :class="highlighted === e.path ? 'bg-elevated' : ''"
122+ @click="highlight(e.path)"
123+ @dblclick="browse(e.path)"
124+ >
125+ <UIcon name="i-lucide-folder" class="size-4 shrink-0 text-muted" />
126+ <span class="flex-1 truncate">{{ e.name }}</span>
127+ <UBadge v-if="e.hasAgentConfig" label="agent config" color="primary" variant="subtle" size="sm" icon="i-lucide-sparkles" />
128+ <UButton
129+ icon="i-lucide-chevron-right"
130+ size="xs"
131+ color="neutral"
132+ variant="ghost"
133+ aria-label="Open folder"
134+ @click.stop="browse(e.path)"
135+ />
136+ </button>
137+ </li>
138+ <li v-if="!listing.entries.length" class="px-3 py-8 text-center text-muted">No subfolder here.</li>
139+ </ul>
140+ </div>
141+ </div>
142+ </template>
143+
144+ <template #footer>
145+ <div class="flex w-full items-center justify-between gap-2">
146+ <span class="truncate font-mono text-xs text-muted">{{ listing?.path ?? 'Select a drive or folder' }}</span>
147+ <div class="flex shrink-0 gap-2">
148+ <UButton label="Cancel" color="neutral" variant="outline" @click="open = false" />
149+ <UButton
150+ label="Select this folder"
151+ icon="i-lucide-check"
152+ :disabled="!listing?.path"
153+ :loading="submitting"
154+ @click="submit(listing!.path!)"
155+ />
156+ </div>
157+ </div>
158+ </template>
159+ </UModal>
160+</template>
added packages/ui/app/components/AgentBadge.vue +16 -0
new file mode 100644
@@ -0,0 +1,16 @@
1+<script setup lang="ts">
2+import type { AgentId } from '@skilly/core'
3+
4+withDefaults(defineProps<{ agent: AgentId, compact?: boolean }>(), { compact: false })
5+</script>
6+
7+<template>
8+ <UBadge
9+ :color="agent === 'claude' ? 'warning' : 'success'"
10+ variant="subtle"
11+ :size="compact ? 'sm' : 'md'"
12+ :icon="agent === 'claude' ? 'i-lucide-asterisk' : 'i-lucide-terminal'"
13+ :label="compact ? (agent === 'claude' ? 'CC' : 'CX') : AGENT_LABELS[agent]"
14+ :title="AGENT_LABELS[agent]"
15+ />
16+</template>
new file mode 100644
@@ -0,0 +1,16 @@
1+<script setup lang="ts">
2+import type { AgentId } from '@skilly/core'
3+
4+withDefaults(defineProps<{ agent: AgentId, compact?: boolean }>(), { compact: false })
5+</script>
6+
7+<template>
8+ <UBadge
9+ :color="agent === 'claude' ? 'warning' : 'success'"
10+ variant="subtle"
11+ :size="compact ? 'sm' : 'md'"
12+ :icon="agent === 'claude' ? 'i-lucide-asterisk' : 'i-lucide-terminal'"
13+ :label="compact ? (agent === 'claude' ? 'CC' : 'CX') : AGENT_LABELS[agent]"
14+ :title="AGENT_LABELS[agent]"
15+ />
16+</template>
added packages/ui/app/components/AppSidebar.vue +78 -0
new file mode 100644
@@ -0,0 +1,78 @@
1+<script setup lang="ts">
2+defineEmits<{ add: [] }>()
3+
4+const { projects, loaded, error } = useProjects()
5+const route = useRoute()
6+
7+const linkClass = (active: boolean) =>
8+ `flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors ${active ? 'bg-elevated text-highlighted font-medium' : 'text-muted hover:bg-elevated/60 hover:text-default'}`
9+
10+const soon = [
11+ { label: 'Catalog', icon: 'i-lucide-library' },
12+ { label: 'Presets', icon: 'i-lucide-layers' },
13+ { label: 'Marketplaces', icon: 'i-lucide-store' }
14+]
15+</script>
16+
17+<template>
18+ <nav class="flex w-full flex-col gap-1">
19+ <NuxtLink to="/" :class="linkClass(route.path === '/')">
20+ <UIcon name="i-lucide-layout-dashboard" class="size-4" />
21+ Dashboard
22+ </NuxtLink>
23+ <NuxtLink to="/home" :class="linkClass(route.path === '/home')">
24+ <UIcon name="i-lucide-house" class="size-4" />
25+ Home
26+ </NuxtLink>
27+
28+ <div class="mt-4 flex items-center justify-between px-2">
29+ <span class="text-xs font-semibold uppercase tracking-wide text-muted">Projects</span>
30+ <UTooltip text="Add project">
31+ <UButton icon="i-lucide-plus" size="xs" color="neutral" variant="ghost" aria-label="Add project" @click="$emit('add')" />
32+ </UTooltip>
33+ </div>
34+
35+ <div v-if="!loaded" class="space-y-1 px-1">
36+ <USkeleton v-for="i in 3" :key="i" class="h-8 w-full" />
37+ </div>
38+ <p v-else-if="error" class="px-2 text-xs text-error">{{ error }}</p>
39+ <p v-else-if="!projects.length" class="px-2 text-xs text-muted">No project yet.</p>
40+ <NuxtLink
41+ v-for="p in projects"
42+ :key="p.id"
43+ :to="`/projects/${encodeURIComponent(p.id)}`"
44+ :class="linkClass(route.path === `/projects/${p.id}` || route.params.id === p.id)"
45+ >
46+ <UIcon name="i-lucide-folder" class="size-4 shrink-0" />
47+ <span class="min-w-0 flex-1 truncate" :title="p.path">{{ p.name }}</span>
48+ <UTooltip v-if="!p.exists" text="Folder not found on disk">
49+ <UIcon name="i-lucide-triangle-alert" class="size-4 shrink-0 text-warning" />
50+ </UTooltip>
51+ <AgentBadge v-for="a in p.agentsDetected" :key="a" :agent="a" compact />
52+ </NuxtLink>
53+
54+ <UButton
55+ label="Add project"
56+ icon="i-lucide-folder-plus"
57+ color="neutral"
58+ variant="outline"
59+ size="sm"
60+ class="mt-1"
61+ block
62+ @click="$emit('add')"
63+ />
64+
65+ <div class="mt-4 space-y-1">
66+ <div
67+ v-for="s in soon"
68+ :key="s.label"
69+ class="flex cursor-not-allowed items-center gap-2 rounded-md px-2 py-1.5 text-sm text-dimmed"
70+ aria-disabled="true"
71+ >
72+ <UIcon :name="s.icon" class="size-4" />
73+ <span class="flex-1">{{ s.label }}</span>
74+ <UBadge label="Soon" size="sm" color="neutral" variant="subtle" />
75+ </div>
76+ </div>
77+ </nav>
78+</template>
new file mode 100644
@@ -0,0 +1,78 @@
1+<script setup lang="ts">
2+defineEmits<{ add: [] }>()
3+
4+const { projects, loaded, error } = useProjects()
5+const route = useRoute()
6+
7+const linkClass = (active: boolean) =>
8+ `flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors ${active ? 'bg-elevated text-highlighted font-medium' : 'text-muted hover:bg-elevated/60 hover:text-default'}`
9+
10+const soon = [
11+ { label: 'Catalog', icon: 'i-lucide-library' },
12+ { label: 'Presets', icon: 'i-lucide-layers' },
13+ { label: 'Marketplaces', icon: 'i-lucide-store' }
14+]
15+</script>
16+
17+<template>
18+ <nav class="flex w-full flex-col gap-1">
19+ <NuxtLink to="/" :class="linkClass(route.path === '/')">
20+ <UIcon name="i-lucide-layout-dashboard" class="size-4" />
21+ Dashboard
22+ </NuxtLink>
23+ <NuxtLink to="/home" :class="linkClass(route.path === '/home')">
24+ <UIcon name="i-lucide-house" class="size-4" />
25+ Home
26+ </NuxtLink>
27+
28+ <div class="mt-4 flex items-center justify-between px-2">
29+ <span class="text-xs font-semibold uppercase tracking-wide text-muted">Projects</span>
30+ <UTooltip text="Add project">
31+ <UButton icon="i-lucide-plus" size="xs" color="neutral" variant="ghost" aria-label="Add project" @click="$emit('add')" />
32+ </UTooltip>
33+ </div>
34+
35+ <div v-if="!loaded" class="space-y-1 px-1">
36+ <USkeleton v-for="i in 3" :key="i" class="h-8 w-full" />
37+ </div>
38+ <p v-else-if="error" class="px-2 text-xs text-error">{{ error }}</p>
39+ <p v-else-if="!projects.length" class="px-2 text-xs text-muted">No project yet.</p>
40+ <NuxtLink
41+ v-for="p in projects"
42+ :key="p.id"
43+ :to="`/projects/${encodeURIComponent(p.id)}`"
44+ :class="linkClass(route.path === `/projects/${p.id}` || route.params.id === p.id)"
45+ >
46+ <UIcon name="i-lucide-folder" class="size-4 shrink-0" />
47+ <span class="min-w-0 flex-1 truncate" :title="p.path">{{ p.name }}</span>
48+ <UTooltip v-if="!p.exists" text="Folder not found on disk">
49+ <UIcon name="i-lucide-triangle-alert" class="size-4 shrink-0 text-warning" />
50+ </UTooltip>
51+ <AgentBadge v-for="a in p.agentsDetected" :key="a" :agent="a" compact />
52+ </NuxtLink>
53+
54+ <UButton
55+ label="Add project"
56+ icon="i-lucide-folder-plus"
57+ color="neutral"
58+ variant="outline"
59+ size="sm"
60+ class="mt-1"
61+ block
62+ @click="$emit('add')"
63+ />
64+
65+ <div class="mt-4 space-y-1">
66+ <div
67+ v-for="s in soon"
68+ :key="s.label"
69+ class="flex cursor-not-allowed items-center gap-2 rounded-md px-2 py-1.5 text-sm text-dimmed"
70+ aria-disabled="true"
71+ >
72+ <UIcon :name="s.icon" class="size-4" />
73+ <span class="flex-1">{{ s.label }}</span>
74+ <UBadge label="Soon" size="sm" color="neutral" variant="subtle" />
75+ </div>
76+ </div>
77+ </nav>
78+</template>
added packages/ui/app/components/CountChips.vue +15 -0
new file mode 100644
@@ -0,0 +1,15 @@
1+<script setup lang="ts">
2+import type { ItemType } from '@skilly/core'
3+
4+defineProps<{ counts: Record<ItemType, number> }>()
5+</script>
6+
7+<template>
8+ <div class="flex flex-wrap gap-x-4 gap-y-1.5">
9+ <div v-for="t in TYPE_ORDER" :key="t" class="flex items-center gap-1.5 text-sm" :class="counts[t] ? '' : 'text-dimmed'">
10+ <UIcon :name="TYPE_META[t].icon" class="size-4" />
11+ <span class="font-medium tabular-nums">{{ counts[t] ?? 0 }}</span>
12+ <span class="text-muted">{{ TYPE_META[t].label }}</span>
13+ </div>
14+ </div>
15+</template>
new file mode 100644
@@ -0,0 +1,15 @@
1+<script setup lang="ts">
2+import type { ItemType } from '@skilly/core'
3+
4+defineProps<{ counts: Record<ItemType, number> }>()
5+</script>
6+
7+<template>
8+ <div class="flex flex-wrap gap-x-4 gap-y-1.5">
9+ <div v-for="t in TYPE_ORDER" :key="t" class="flex items-center gap-1.5 text-sm" :class="counts[t] ? '' : 'text-dimmed'">
10+ <UIcon :name="TYPE_META[t].icon" class="size-4" />
11+ <span class="font-medium tabular-nums">{{ counts[t] ?? 0 }}</span>
12+ <span class="text-muted">{{ TYPE_META[t].label }}</span>
13+ </div>
14+ </div>
15+</template>
added packages/ui/app/components/ItemDetail.vue +121 -0
new file mode 100644
@@ -0,0 +1,121 @@
1+<script setup lang="ts">
2+import type { InventoryItem, SkillDetails } from '@skilly/core'
3+
4+const props = defineProps<{ item: InventoryItem | null }>()
5+const open = defineModel<boolean>('open', { default: false })
6+
7+const filePath = computed<string | null>(() => {
8+ const it = props.item
9+ if (!it) return null
10+ if (it.type === 'skill') return joinPath(it.source.path, 'SKILL.md')
11+ if (['rule', 'agent', 'instructions'].includes(it.type) && it.source.kind === 'file') return it.source.path
12+ return null
13+})
14+
15+/** Skill file currently displayed (defaults to SKILL.md). */
16+const currentFile = ref<string | null>(null)
17+watch(() => props.item?.id, () => { currentFile.value = null })
18+const shownPath = computed(() => currentFile.value ?? filePath.value)
19+
20+const skillFiles = computed(() => (props.item?.type === 'skill' ? (props.item.details as SkillDetails).files : []))
21+
22+const frontmatter = computed(() => {
23+ const d = props.item?.details as { frontmatter?: Record<string, unknown> } | undefined
24+ return currentFile.value ? undefined : d?.frontmatter
25+})
26+
27+const rows = computed(() => {
28+ const it = props.item
29+ if (!it) return []
30+ const r: [string, string][] = [
31+ ['Type', it.type],
32+ ['Agent', AGENT_LABELS[it.agent]],
33+ ['Source', it.source.path]
34+ ]
35+ if (it.source.configKey) r.push(['Config key', it.source.configKey])
36+ if (it.source.symlinkTarget) r.push(['Symlink target', it.source.symlinkTarget])
37+ if (it.source.plugin) r.push(['Plugin', it.source.plugin])
38+ if (it.hash) r.push(['Hash', it.hash.slice(0, 12)])
39+ const d = it.details
40+ if (d.type === 'skill') {
41+ r.push(['Visibility', d.visibility])
42+ if (d.visibilitySource) r.push(['Visibility set by', d.visibilitySource])
43+ } else if (d.type === 'rule') {
44+ r.push(['Applies to', d.paths.length ? d.paths.join(', ') : 'always'])
45+ } else if (d.type === 'mcp') {
46+ r.push(['Transport', d.transport])
47+ if (d.command) r.push(['Command', [d.command, ...(d.args ?? [])].join(' ')])
48+ if (d.url) r.push(['URL', d.url])
49+ if (d.agentScope) r.push(['Scope', d.agentScope])
50+ r.push(['Enabled', d.enabled ? 'yes' : 'no'])
51+ if (d.envVarNames.length) r.push(['Env vars', d.envVarNames.join(', ')])
52+ if (d.literalEnvKeys.length) r.push(['Literal env keys', d.literalEnvKeys.join(', ')])
53+ } else if (d.type === 'hook') {
54+ r.push(['Event', d.event])
55+ if (d.matcher) r.push(['Matcher', d.matcher])
56+ r.push(['Handler', d.handlerType])
57+ if (d.command) r.push(['Command', d.command])
58+ } else if (d.type === 'agent') {
59+ if (d.model) r.push(['Model', d.model])
60+ if (d.tools?.length) r.push(['Tools', d.tools.join(', ')])
61+ } else if (d.type === 'instructions') {
62+ r.push(['Size', formatBytes(d.sizeBytes)])
63+ r.push(['Loaded', d.loaded ? 'yes' : `no${d.notLoadedReason ? ` - ${d.notLoadedReason}` : ''}`])
64+ if (d.imports.length) r.push(['Imports', d.imports.join(', ')])
65+ }
66+ return r
67+})
68+</script>
69+
70+<template>
71+ <USlideover v-model:open="open" :title="item?.name" :description="item?.description" :ui="{ content: 'max-w-2xl' }">
72+ <template #body>
73+ <div v-if="item" class="space-y-5">
74+ <UAlert
75+ v-if="item.warnings.length"
76+ color="warning"
77+ variant="subtle"
78+ icon="i-lucide-triangle-alert"
79+ :title="item.warnings.length > 1 ? 'Warnings' : 'Warning'"
80+ >
81+ <template #description>
82+ <ul class="list-disc space-y-0.5 pl-4">
83+ <li v-for="w in item.warnings" :key="w">{{ w }}</li>
84+ </ul>
85+ </template>
86+ </UAlert>
87+
88+ <div class="overflow-hidden rounded-lg border border-default">
89+ <table class="w-full text-sm">
90+ <tbody class="divide-y divide-default">
91+ <tr v-for="[k, v] in rows" :key="k">
92+ <th class="w-36 bg-elevated/40 px-3 py-2 text-left align-top text-xs font-medium text-muted">{{ k }}</th>
93+ <td class="px-3 py-2 font-mono text-xs break-all">{{ v }}</td>
94+ </tr>
95+ </tbody>
96+ </table>
97+ </div>
98+
99+ <div v-if="skillFiles.length" class="space-y-2">
100+ <h3 class="text-sm font-semibold">Files ({{ skillFiles.length }})</h3>
101+ <ul class="max-h-48 overflow-auto rounded-lg border border-default text-xs">
102+ <li v-for="f in skillFiles" :key="f">
103+ <button
104+ type="button"
105+ class="flex w-full items-center gap-2 px-3 py-1.5 text-left font-mono hover:bg-elevated/60"
106+ :class="shownPath === joinPath(item.source.path, f) ? 'bg-elevated text-highlighted' : ''"
107+ @click="currentFile = f === 'SKILL.md' ? null : joinPath(item.source.path, f)"
108+ >
109+ <UIcon name="i-lucide-file" class="size-3.5 shrink-0 text-muted" />
110+ <span class="truncate">{{ f }}</span>
111+ </button>
112+ </li>
113+ </ul>
114+ </div>
115+
116+ <MarkdownView v-if="shownPath" :path="shownPath" :frontmatter="frontmatter" />
117+ <p v-else class="text-sm text-muted">This item is an entry of a configuration file: there is no separate file to display.</p>
118+ </div>
119+ </template>
120+ </USlideover>
121+</template>
new file mode 100644
@@ -0,0 +1,121 @@
1+<script setup lang="ts">
2+import type { InventoryItem, SkillDetails } from '@skilly/core'
3+
4+const props = defineProps<{ item: InventoryItem | null }>()
5+const open = defineModel<boolean>('open', { default: false })
6+
7+const filePath = computed<string | null>(() => {
8+ const it = props.item
9+ if (!it) return null
10+ if (it.type === 'skill') return joinPath(it.source.path, 'SKILL.md')
11+ if (['rule', 'agent', 'instructions'].includes(it.type) && it.source.kind === 'file') return it.source.path
12+ return null
13+})
14+
15+/** Skill file currently displayed (defaults to SKILL.md). */
16+const currentFile = ref<string | null>(null)
17+watch(() => props.item?.id, () => { currentFile.value = null })
18+const shownPath = computed(() => currentFile.value ?? filePath.value)
19+
20+const skillFiles = computed(() => (props.item?.type === 'skill' ? (props.item.details as SkillDetails).files : []))
21+
22+const frontmatter = computed(() => {
23+ const d = props.item?.details as { frontmatter?: Record<string, unknown> } | undefined
24+ return currentFile.value ? undefined : d?.frontmatter
25+})
26+
27+const rows = computed(() => {
28+ const it = props.item
29+ if (!it) return []
30+ const r: [string, string][] = [
31+ ['Type', it.type],
32+ ['Agent', AGENT_LABELS[it.agent]],
33+ ['Source', it.source.path]
34+ ]
35+ if (it.source.configKey) r.push(['Config key', it.source.configKey])
36+ if (it.source.symlinkTarget) r.push(['Symlink target', it.source.symlinkTarget])
37+ if (it.source.plugin) r.push(['Plugin', it.source.plugin])
38+ if (it.hash) r.push(['Hash', it.hash.slice(0, 12)])
39+ const d = it.details
40+ if (d.type === 'skill') {
41+ r.push(['Visibility', d.visibility])
42+ if (d.visibilitySource) r.push(['Visibility set by', d.visibilitySource])
43+ } else if (d.type === 'rule') {
44+ r.push(['Applies to', d.paths.length ? d.paths.join(', ') : 'always'])
45+ } else if (d.type === 'mcp') {
46+ r.push(['Transport', d.transport])
47+ if (d.command) r.push(['Command', [d.command, ...(d.args ?? [])].join(' ')])
48+ if (d.url) r.push(['URL', d.url])
49+ if (d.agentScope) r.push(['Scope', d.agentScope])
50+ r.push(['Enabled', d.enabled ? 'yes' : 'no'])
51+ if (d.envVarNames.length) r.push(['Env vars', d.envVarNames.join(', ')])
52+ if (d.literalEnvKeys.length) r.push(['Literal env keys', d.literalEnvKeys.join(', ')])
53+ } else if (d.type === 'hook') {
54+ r.push(['Event', d.event])
55+ if (d.matcher) r.push(['Matcher', d.matcher])
56+ r.push(['Handler', d.handlerType])
57+ if (d.command) r.push(['Command', d.command])
58+ } else if (d.type === 'agent') {
59+ if (d.model) r.push(['Model', d.model])
60+ if (d.tools?.length) r.push(['Tools', d.tools.join(', ')])
61+ } else if (d.type === 'instructions') {
62+ r.push(['Size', formatBytes(d.sizeBytes)])
63+ r.push(['Loaded', d.loaded ? 'yes' : `no${d.notLoadedReason ? ` - ${d.notLoadedReason}` : ''}`])
64+ if (d.imports.length) r.push(['Imports', d.imports.join(', ')])
65+ }
66+ return r
67+})
68+</script>
69+
70+<template>
71+ <USlideover v-model:open="open" :title="item?.name" :description="item?.description" :ui="{ content: 'max-w-2xl' }">
72+ <template #body>
73+ <div v-if="item" class="space-y-5">
74+ <UAlert
75+ v-if="item.warnings.length"
76+ color="warning"
77+ variant="subtle"
78+ icon="i-lucide-triangle-alert"
79+ :title="item.warnings.length > 1 ? 'Warnings' : 'Warning'"
80+ >
81+ <template #description>
82+ <ul class="list-disc space-y-0.5 pl-4">
83+ <li v-for="w in item.warnings" :key="w">{{ w }}</li>
84+ </ul>
85+ </template>
86+ </UAlert>
87+
88+ <div class="overflow-hidden rounded-lg border border-default">
89+ <table class="w-full text-sm">
90+ <tbody class="divide-y divide-default">
91+ <tr v-for="[k, v] in rows" :key="k">
92+ <th class="w-36 bg-elevated/40 px-3 py-2 text-left align-top text-xs font-medium text-muted">{{ k }}</th>
93+ <td class="px-3 py-2 font-mono text-xs break-all">{{ v }}</td>
94+ </tr>
95+ </tbody>
96+ </table>
97+ </div>
98+
99+ <div v-if="skillFiles.length" class="space-y-2">
100+ <h3 class="text-sm font-semibold">Files ({{ skillFiles.length }})</h3>
101+ <ul class="max-h-48 overflow-auto rounded-lg border border-default text-xs">
102+ <li v-for="f in skillFiles" :key="f">
103+ <button
104+ type="button"
105+ class="flex w-full items-center gap-2 px-3 py-1.5 text-left font-mono hover:bg-elevated/60"
106+ :class="shownPath === joinPath(item.source.path, f) ? 'bg-elevated text-highlighted' : ''"
107+ @click="currentFile = f === 'SKILL.md' ? null : joinPath(item.source.path, f)"
108+ >
109+ <UIcon name="i-lucide-file" class="size-3.5 shrink-0 text-muted" />
110+ <span class="truncate">{{ f }}</span>
111+ </button>
112+ </li>
113+ </ul>
114+ </div>
115+
116+ <MarkdownView v-if="shownPath" :path="shownPath" :frontmatter="frontmatter" />
117+ <p v-else class="text-sm text-muted">This item is an entry of a configuration file: there is no separate file to display.</p>
118+ </div>
119+ </template>
120+ </USlideover>
121+</template>
added packages/ui/app/components/ItemTable.vue +172 -0
new file mode 100644
@@ -0,0 +1,172 @@
1+<script setup lang="ts">
2+import type {
3+ AgentDetails, HookDetails, InstructionsDetails, InventoryItem, ItemType, McpDetails, RuleDetails, SkillDetails
4+} from '@skilly/core'
5+
6+defineProps<{ type: ItemType, items: InventoryItem[], filtered: boolean }>()
7+defineEmits<{ select: [item: InventoryItem] }>()
8+
9+const skill = (i: InventoryItem) => i.details as SkillDetails
10+const rule = (i: InventoryItem) => i.details as RuleDetails
11+const mcp = (i: InventoryItem) => i.details as McpDetails
12+const hook = (i: InventoryItem) => i.details as HookDetails
13+const agent = (i: InventoryItem) => i.details as AgentDetails
14+const instr = (i: InventoryItem) => i.details as InstructionsDetails
15+
16+const VISIBILITY = {
17+ 'auto': { label: 'auto', color: 'success' },
18+ 'on-demand': { label: 'on demand', color: 'info' },
19+ 'name-only': { label: 'name only', color: 'warning' },
20+ 'off': { label: 'off', color: 'neutral' }
21+} as const
22+
23+const TH = 'px-3 py-2 text-left text-xs font-medium uppercase tracking-wide text-muted'
24+const TD = 'px-3 py-2.5 align-top'
25+const MAX_ENV = 4
26+</script>
27+
28+<template>
29+ <div
30+ v-if="!items.length"
31+ class="flex flex-col items-center gap-2 rounded-xl border border-dashed border-default py-12 text-center"
32+ >
33+ <UIcon :name="filtered ? 'i-lucide-search-x' : TYPE_META[type].icon" class="size-8 text-dimmed" />
34+ <p class="text-sm text-muted">
35+ {{ filtered ? 'No result matches the current filters.' : `No ${TYPE_META[type].label.toLowerCase()} found in this scope.` }}
36+ </p>
37+ </div>
38+
39+ <div v-else class="overflow-x-auto rounded-xl border border-default">
40+ <table class="w-full min-w-[720px] text-sm">
41+ <thead class="border-b border-default bg-elevated/40">
42+ <tr v-if="type === 'skill'">
43+ <th :class="TH">Name</th><th :class="TH">Agent</th><th :class="TH">Visibility</th><th :class="TH">Origin</th>
44+ </tr>
45+ <tr v-else-if="type === 'rule'">
46+ <th :class="TH">Name</th><th :class="TH">Agent</th><th :class="TH">Applies to</th>
47+ </tr>
48+ <tr v-else-if="type === 'mcp'">
49+ <th :class="TH">Name</th><th :class="TH">Agent</th><th :class="TH">Transport</th><th :class="TH">Command / URL</th><th :class="TH">Env vars</th><th :class="TH">Scope</th><th :class="TH">Status</th>
50+ </tr>
51+ <tr v-else-if="type === 'hook'">
52+ <th :class="TH">Event</th><th :class="TH">Agent</th><th :class="TH">Matcher</th><th :class="TH">Command</th><th :class="TH">Handler</th>
53+ </tr>
54+ <tr v-else-if="type === 'agent'">
55+ <th :class="TH">Name</th><th :class="TH">Agent</th><th :class="TH">Model</th>
56+ </tr>
57+ <tr v-else>
58+ <th :class="TH">File</th><th :class="TH">Agent</th><th :class="TH">Size</th><th :class="TH">Status</th><th :class="TH">Imports</th>
59+ </tr>
60+ </thead>
61+
62+ <tbody class="divide-y divide-default">
63+ <tr
64+ v-for="item in items"
65+ :key="item.id"
66+ class="cursor-pointer transition-colors hover:bg-elevated/50"
67+ tabindex="0"
68+ @click="$emit('select', item)"
69+ @keydown.enter="$emit('select', item)"
70+ >
71+ <!-- Hooks lead with the event, others with the name -->
72+ <td :class="TD" class="max-w-md">
73+ <div class="flex items-start gap-2">
74+ <div class="min-w-0">
75+ <div class="flex items-center gap-1.5">
76+ <span class="truncate font-medium text-highlighted">{{ type === 'hook' ? hook(item).event : item.name }}</span>
77+ <UTooltip v-if="item.warnings.length" :text="item.warnings.join('\n')">
78+ <UIcon name="i-lucide-triangle-alert" class="size-4 shrink-0 text-warning" />
79+ </UTooltip>
80+ </div>
81+ <p v-if="item.description && type !== 'hook'" class="line-clamp-2 text-xs text-muted">{{ item.description }}</p>
82+ </div>
83+ </div>
84+ </td>
85+ <td :class="TD"><AgentBadge :agent="item.agent" compact /></td>
86+
87+ <!-- skills -->
88+ <template v-if="type === 'skill'">
89+ <td :class="TD">
90+ <UTooltip :text="skill(item).visibilitySource ?? 'default'">
91+ <UBadge :label="VISIBILITY[skill(item).visibility].label" :color="VISIBILITY[skill(item).visibility].color" variant="subtle" size="sm" />
92+ </UTooltip>
93+ </td>
94+ <td :class="TD">
95+ <div class="flex flex-wrap gap-1">
96+ <UBadge v-if="item.source.plugin" :label="`plugin: ${item.source.plugin}`" icon="i-lucide-puzzle" color="primary" variant="subtle" size="sm" />
97+ <UTooltip v-if="item.source.symlinkTarget" :text="item.source.symlinkTarget">
98+ <UBadge label="symlink" icon="i-lucide-link" color="neutral" variant="subtle" size="sm" />
99+ </UTooltip>
100+ <span v-if="item.source.symlinkTarget" class="max-w-48 truncate font-mono text-xs text-muted" :title="item.source.symlinkTarget">
101+ {{ item.source.symlinkTarget }}
102+ </span>
103+ <span v-if="!item.source.plugin && !item.source.symlinkTarget" class="text-xs text-dimmed">local</span>
104+ </div>
105+ </td>
106+ </template>
107+
108+ <!-- rules -->
109+ <template v-else-if="type === 'rule'">
110+ <td :class="TD">
111+ <span v-if="!rule(item).paths.length" class="text-xs text-muted">always</span>
112+ <div v-else class="flex flex-wrap gap-1">
113+ <UBadge v-for="p in rule(item).paths" :key="p" :label="p" color="neutral" variant="outline" size="sm" class="font-mono" />
114+ </div>
115+ </td>
116+ </template>
117+
118+ <!-- mcp -->
119+ <template v-else-if="type === 'mcp'">
120+ <td :class="TD"><UBadge :label="mcp(item).transport" color="neutral" variant="subtle" size="sm" /></td>
121+ <td :class="TD" class="max-w-xs">
122+ <span class="block truncate font-mono text-xs" :title="mcp(item).url ?? [mcp(item).command, ...(mcp(item).args ?? [])].join(' ')">
123+ {{ mcp(item).url ?? [mcp(item).command, ...(mcp(item).args ?? [])].filter(Boolean).join(' ') }}
124+ </span>
125+ </td>
126+ <td :class="TD">
127+ <div class="flex flex-wrap gap-1">
128+ <UBadge v-for="v in mcp(item).envVarNames.slice(0, MAX_ENV)" :key="v" :label="v" color="neutral" variant="outline" size="sm" class="font-mono" />
129+ <UBadge v-if="mcp(item).envVarNames.length > MAX_ENV" :label="`+${mcp(item).envVarNames.length - MAX_ENV}`" color="neutral" variant="subtle" size="sm" />
130+ </div>
131+ </td>
132+ <td :class="TD"><span class="text-xs text-muted">{{ mcp(item).agentScope ?? '-' }}</span></td>
133+ <td :class="TD">
134+ <UBadge :label="mcp(item).enabled ? 'enabled' : 'disabled'" :color="mcp(item).enabled ? 'success' : 'neutral'" variant="subtle" size="sm" />
135+ </td>
136+ </template>
137+
138+ <!-- hooks -->
139+ <template v-else-if="type === 'hook'">
140+ <td :class="TD"><span class="font-mono text-xs">{{ hook(item).matcher ?? '*' }}</span></td>
141+ <td :class="TD" class="max-w-sm">
142+ <span class="block truncate font-mono text-xs" :title="hook(item).command">{{ hook(item).command ?? '-' }}</span>
143+ </td>
144+ <td :class="TD"><UBadge :label="hook(item).handlerType" color="neutral" variant="subtle" size="sm" /></td>
145+ </template>
146+
147+ <!-- agents -->
148+ <template v-else-if="type === 'agent'">
149+ <td :class="TD"><span class="text-xs">{{ agent(item).model ?? 'inherit' }}</span></td>
150+ </template>
151+
152+ <!-- instructions -->
153+ <template v-else>
154+ <td :class="TD"><span class="text-xs tabular-nums text-muted">{{ formatBytes(instr(item).sizeBytes) }}</span></td>
155+ <td :class="TD">
156+ <UTooltip v-if="!instr(item).loaded && instr(item).notLoadedReason" :text="instr(item).notLoadedReason!">
157+ <UBadge label="not loaded" color="warning" variant="subtle" size="sm" icon="i-lucide-circle-help" />
158+ </UTooltip>
159+ <UBadge v-else :label="instr(item).loaded ? 'loaded' : 'not loaded'" :color="instr(item).loaded ? 'success' : 'warning'" variant="subtle" size="sm" />
160+ </td>
161+ <td :class="TD">
162+ <div class="flex flex-wrap gap-1">
163+ <UBadge v-for="im in instr(item).imports" :key="im" :label="im" color="neutral" variant="outline" size="sm" class="font-mono" />
164+ <span v-if="!instr(item).imports.length" class="text-xs text-dimmed">-</span>
165+ </div>
166+ </td>
167+ </template>
168+ </tr>
169+ </tbody>
170+ </table>
171+ </div>
172+</template>
new file mode 100644
@@ -0,0 +1,172 @@
1+<script setup lang="ts">
2+import type {
3+ AgentDetails, HookDetails, InstructionsDetails, InventoryItem, ItemType, McpDetails, RuleDetails, SkillDetails
4+} from '@skilly/core'
5+
6+defineProps<{ type: ItemType, items: InventoryItem[], filtered: boolean }>()
7+defineEmits<{ select: [item: InventoryItem] }>()
8+
9+const skill = (i: InventoryItem) => i.details as SkillDetails
10+const rule = (i: InventoryItem) => i.details as RuleDetails
11+const mcp = (i: InventoryItem) => i.details as McpDetails
12+const hook = (i: InventoryItem) => i.details as HookDetails
13+const agent = (i: InventoryItem) => i.details as AgentDetails
14+const instr = (i: InventoryItem) => i.details as InstructionsDetails
15+
16+const VISIBILITY = {
17+ 'auto': { label: 'auto', color: 'success' },
18+ 'on-demand': { label: 'on demand', color: 'info' },
19+ 'name-only': { label: 'name only', color: 'warning' },
20+ 'off': { label: 'off', color: 'neutral' }
21+} as const
22+
23+const TH = 'px-3 py-2 text-left text-xs font-medium uppercase tracking-wide text-muted'
24+const TD = 'px-3 py-2.5 align-top'
25+const MAX_ENV = 4
26+</script>
27+
28+<template>
29+ <div
30+ v-if="!items.length"
31+ class="flex flex-col items-center gap-2 rounded-xl border border-dashed border-default py-12 text-center"
32+ >
33+ <UIcon :name="filtered ? 'i-lucide-search-x' : TYPE_META[type].icon" class="size-8 text-dimmed" />
34+ <p class="text-sm text-muted">
35+ {{ filtered ? 'No result matches the current filters.' : `No ${TYPE_META[type].label.toLowerCase()} found in this scope.` }}
36+ </p>
37+ </div>
38+
39+ <div v-else class="overflow-x-auto rounded-xl border border-default">
40+ <table class="w-full min-w-[720px] text-sm">
41+ <thead class="border-b border-default bg-elevated/40">
42+ <tr v-if="type === 'skill'">
43+ <th :class="TH">Name</th><th :class="TH">Agent</th><th :class="TH">Visibility</th><th :class="TH">Origin</th>
44+ </tr>
45+ <tr v-else-if="type === 'rule'">
46+ <th :class="TH">Name</th><th :class="TH">Agent</th><th :class="TH">Applies to</th>
47+ </tr>
48+ <tr v-else-if="type === 'mcp'">
49+ <th :class="TH">Name</th><th :class="TH">Agent</th><th :class="TH">Transport</th><th :class="TH">Command / URL</th><th :class="TH">Env vars</th><th :class="TH">Scope</th><th :class="TH">Status</th>
50+ </tr>
51+ <tr v-else-if="type === 'hook'">
52+ <th :class="TH">Event</th><th :class="TH">Agent</th><th :class="TH">Matcher</th><th :class="TH">Command</th><th :class="TH">Handler</th>
53+ </tr>
54+ <tr v-else-if="type === 'agent'">
55+ <th :class="TH">Name</th><th :class="TH">Agent</th><th :class="TH">Model</th>
56+ </tr>
57+ <tr v-else>
58+ <th :class="TH">File</th><th :class="TH">Agent</th><th :class="TH">Size</th><th :class="TH">Status</th><th :class="TH">Imports</th>
59+ </tr>
60+ </thead>
61+
62+ <tbody class="divide-y divide-default">
63+ <tr
64+ v-for="item in items"
65+ :key="item.id"
66+ class="cursor-pointer transition-colors hover:bg-elevated/50"
67+ tabindex="0"
68+ @click="$emit('select', item)"
69+ @keydown.enter="$emit('select', item)"
70+ >
71+ <!-- Hooks lead with the event, others with the name -->
72+ <td :class="TD" class="max-w-md">
73+ <div class="flex items-start gap-2">
74+ <div class="min-w-0">
75+ <div class="flex items-center gap-1.5">
76+ <span class="truncate font-medium text-highlighted">{{ type === 'hook' ? hook(item).event : item.name }}</span>
77+ <UTooltip v-if="item.warnings.length" :text="item.warnings.join('\n')">
78+ <UIcon name="i-lucide-triangle-alert" class="size-4 shrink-0 text-warning" />
79+ </UTooltip>
80+ </div>
81+ <p v-if="item.description && type !== 'hook'" class="line-clamp-2 text-xs text-muted">{{ item.description }}</p>
82+ </div>
83+ </div>
84+ </td>
85+ <td :class="TD"><AgentBadge :agent="item.agent" compact /></td>
86+
87+ <!-- skills -->
88+ <template v-if="type === 'skill'">
89+ <td :class="TD">
90+ <UTooltip :text="skill(item).visibilitySource ?? 'default'">
91+ <UBadge :label="VISIBILITY[skill(item).visibility].label" :color="VISIBILITY[skill(item).visibility].color" variant="subtle" size="sm" />
92+ </UTooltip>
93+ </td>
94+ <td :class="TD">
95+ <div class="flex flex-wrap gap-1">
96+ <UBadge v-if="item.source.plugin" :label="`plugin: ${item.source.plugin}`" icon="i-lucide-puzzle" color="primary" variant="subtle" size="sm" />
97+ <UTooltip v-if="item.source.symlinkTarget" :text="item.source.symlinkTarget">
98+ <UBadge label="symlink" icon="i-lucide-link" color="neutral" variant="subtle" size="sm" />
99+ </UTooltip>
100+ <span v-if="item.source.symlinkTarget" class="max-w-48 truncate font-mono text-xs text-muted" :title="item.source.symlinkTarget">
101+ {{ item.source.symlinkTarget }}
102+ </span>
103+ <span v-if="!item.source.plugin && !item.source.symlinkTarget" class="text-xs text-dimmed">local</span>
104+ </div>
105+ </td>
106+ </template>
107+
108+ <!-- rules -->
109+ <template v-else-if="type === 'rule'">
110+ <td :class="TD">
111+ <span v-if="!rule(item).paths.length" class="text-xs text-muted">always</span>
112+ <div v-else class="flex flex-wrap gap-1">
113+ <UBadge v-for="p in rule(item).paths" :key="p" :label="p" color="neutral" variant="outline" size="sm" class="font-mono" />
114+ </div>
115+ </td>
116+ </template>
117+
118+ <!-- mcp -->
119+ <template v-else-if="type === 'mcp'">
120+ <td :class="TD"><UBadge :label="mcp(item).transport" color="neutral" variant="subtle" size="sm" /></td>
121+ <td :class="TD" class="max-w-xs">
122+ <span class="block truncate font-mono text-xs" :title="mcp(item).url ?? [mcp(item).command, ...(mcp(item).args ?? [])].join(' ')">
123+ {{ mcp(item).url ?? [mcp(item).command, ...(mcp(item).args ?? [])].filter(Boolean).join(' ') }}
124+ </span>
125+ </td>
126+ <td :class="TD">
127+ <div class="flex flex-wrap gap-1">
128+ <UBadge v-for="v in mcp(item).envVarNames.slice(0, MAX_ENV)" :key="v" :label="v" color="neutral" variant="outline" size="sm" class="font-mono" />
129+ <UBadge v-if="mcp(item).envVarNames.length > MAX_ENV" :label="`+${mcp(item).envVarNames.length - MAX_ENV}`" color="neutral" variant="subtle" size="sm" />
130+ </div>
131+ </td>
132+ <td :class="TD"><span class="text-xs text-muted">{{ mcp(item).agentScope ?? '-' }}</span></td>
133+ <td :class="TD">
134+ <UBadge :label="mcp(item).enabled ? 'enabled' : 'disabled'" :color="mcp(item).enabled ? 'success' : 'neutral'" variant="subtle" size="sm" />
135+ </td>
136+ </template>
137+
138+ <!-- hooks -->
139+ <template v-else-if="type === 'hook'">
140+ <td :class="TD"><span class="font-mono text-xs">{{ hook(item).matcher ?? '*' }}</span></td>
141+ <td :class="TD" class="max-w-sm">
142+ <span class="block truncate font-mono text-xs" :title="hook(item).command">{{ hook(item).command ?? '-' }}</span>
143+ </td>
144+ <td :class="TD"><UBadge :label="hook(item).handlerType" color="neutral" variant="subtle" size="sm" /></td>
145+ </template>
146+
147+ <!-- agents -->
148+ <template v-else-if="type === 'agent'">
149+ <td :class="TD"><span class="text-xs">{{ agent(item).model ?? 'inherit' }}</span></td>
150+ </template>
151+
152+ <!-- instructions -->
153+ <template v-else>
154+ <td :class="TD"><span class="text-xs tabular-nums text-muted">{{ formatBytes(instr(item).sizeBytes) }}</span></td>
155+ <td :class="TD">
156+ <UTooltip v-if="!instr(item).loaded && instr(item).notLoadedReason" :text="instr(item).notLoadedReason!">
157+ <UBadge label="not loaded" color="warning" variant="subtle" size="sm" icon="i-lucide-circle-help" />
158+ </UTooltip>
159+ <UBadge v-else :label="instr(item).loaded ? 'loaded' : 'not loaded'" :color="instr(item).loaded ? 'success' : 'warning'" variant="subtle" size="sm" />
160+ </td>
161+ <td :class="TD">
162+ <div class="flex flex-wrap gap-1">
163+ <UBadge v-for="im in instr(item).imports" :key="im" :label="im" color="neutral" variant="outline" size="sm" class="font-mono" />
164+ <span v-if="!instr(item).imports.length" class="text-xs text-dimmed">-</span>
165+ </div>
166+ </td>
167+ </template>
168+ </tr>
169+ </tbody>
170+ </table>
171+ </div>
172+</template>
added packages/ui/app/components/MarkdownView.vue +83 -0
new file mode 100644
@@ -0,0 +1,83 @@
1+<script setup lang="ts">
2+import type { FileContent } from '@skilly/core'
3+
4+const props = defineProps<{
5+ /** Absolute path of the file to display. */
6+ path: string
7+ /** Frontmatter already parsed by the scanner (preferred over parsing the file). */
8+ frontmatter?: Record<string, unknown>
9+}>()
10+
11+const { request } = useApi()
12+const file = ref<FileContent | null>(null)
13+const loading = ref(false)
14+const error = ref<string | null>(null)
15+const raw = ref(false)
16+
17+const isMarkdown = computed(() => /\.(md|markdown|mdc)$/i.test(props.path))
18+const parsed = computed(() => splitFrontmatter(file.value?.content ?? ''))
19+const html = computed(() => (isMarkdown.value && file.value ? renderMarkdown(parsed.value.body) : ''))
20+
21+const fmRows = computed(() => {
22+ const src = props.frontmatter && Object.keys(props.frontmatter).length ? props.frontmatter : parsed.value.frontmatter
23+ if (!src || !isMarkdown.value) return []
24+ return Object.entries(src).map(([k, v]) => [k, typeof v === 'string' ? v : JSON.stringify(v)] as const)
25+})
26+
27+async function load() {
28+ loading.value = true
29+ error.value = null
30+ file.value = null
31+ try {
32+ file.value = await request<FileContent>('/api/files', { query: { path: props.path }, silent: true })
33+ } catch (e) {
34+ error.value = (e as Error).message
35+ } finally {
36+ loading.value = false
37+ }
38+}
39+
40+watch(() => props.path, load, { immediate: true })
41+</script>
42+
43+<template>
44+ <div class="space-y-3">
45+ <div class="flex items-center justify-between gap-2">
46+ <span class="truncate font-mono text-xs text-muted" :title="path">{{ path }}</span>
47+ <USwitch v-if="isMarkdown" v-model="raw" label="Raw" size="sm" class="shrink-0" />
48+ </div>
49+
50+ <div v-if="loading" class="space-y-2">
51+ <USkeleton class="h-4 w-1/3" />
52+ <USkeleton class="h-4 w-full" />
53+ <USkeleton class="h-4 w-5/6" />
54+ </div>
55+ <UAlert v-else-if="error" color="error" variant="subtle" icon="i-lucide-circle-alert" title="Could not read the file" :description="error" />
56+ <template v-else-if="file">
57+ <UAlert
58+ v-if="file.truncated"
59+ color="warning"
60+ variant="subtle"
61+ icon="i-lucide-scissors"
62+ title="Large file"
63+ :description="`Only the first part of this file is shown (${formatBytes(file.sizeBytes)} in total).`"
64+ />
65+
66+ <pre v-if="raw || !isMarkdown" class="max-h-[60vh] overflow-auto whitespace-pre-wrap rounded-lg bg-elevated p-3 font-mono text-xs">{{ file.content }}</pre>
67+ <template v-else>
68+ <div v-if="fmRows.length" class="overflow-hidden rounded-lg border border-default">
69+ <table class="w-full text-xs">
70+ <tbody class="divide-y divide-default">
71+ <tr v-for="[k, v] in fmRows" :key="k">
72+ <th class="w-1/4 bg-elevated/40 px-2 py-1.5 text-left align-top font-medium text-muted">{{ k }}</th>
73+ <td class="px-2 py-1.5 font-mono break-words">{{ v }}</td>
74+ </tr>
75+ </tbody>
76+ </table>
77+ </div>
78+ <!-- eslint-disable-next-line vue/no-v-html -->
79+ <div class="md-body" v-html="html" />
80+ </template>
81+ </template>
82+ </div>
83+</template>
new file mode 100644
@@ -0,0 +1,83 @@
1+<script setup lang="ts">
2+import type { FileContent } from '@skilly/core'
3+
4+const props = defineProps<{
5+ /** Absolute path of the file to display. */
6+ path: string
7+ /** Frontmatter already parsed by the scanner (preferred over parsing the file). */
8+ frontmatter?: Record<string, unknown>
9+}>()
10+
11+const { request } = useApi()
12+const file = ref<FileContent | null>(null)
13+const loading = ref(false)
14+const error = ref<string | null>(null)
15+const raw = ref(false)
16+
17+const isMarkdown = computed(() => /\.(md|markdown|mdc)$/i.test(props.path))
18+const parsed = computed(() => splitFrontmatter(file.value?.content ?? ''))
19+const html = computed(() => (isMarkdown.value && file.value ? renderMarkdown(parsed.value.body) : ''))
20+
21+const fmRows = computed(() => {
22+ const src = props.frontmatter && Object.keys(props.frontmatter).length ? props.frontmatter : parsed.value.frontmatter
23+ if (!src || !isMarkdown.value) return []
24+ return Object.entries(src).map(([k, v]) => [k, typeof v === 'string' ? v : JSON.stringify(v)] as const)
25+})
26+
27+async function load() {
28+ loading.value = true
29+ error.value = null
30+ file.value = null
31+ try {
32+ file.value = await request<FileContent>('/api/files', { query: { path: props.path }, silent: true })
33+ } catch (e) {
34+ error.value = (e as Error).message
35+ } finally {
36+ loading.value = false
37+ }
38+}
39+
40+watch(() => props.path, load, { immediate: true })
41+</script>
42+
43+<template>
44+ <div class="space-y-3">
45+ <div class="flex items-center justify-between gap-2">
46+ <span class="truncate font-mono text-xs text-muted" :title="path">{{ path }}</span>
47+ <USwitch v-if="isMarkdown" v-model="raw" label="Raw" size="sm" class="shrink-0" />
48+ </div>
49+
50+ <div v-if="loading" class="space-y-2">
51+ <USkeleton class="h-4 w-1/3" />
52+ <USkeleton class="h-4 w-full" />
53+ <USkeleton class="h-4 w-5/6" />
54+ </div>
55+ <UAlert v-else-if="error" color="error" variant="subtle" icon="i-lucide-circle-alert" title="Could not read the file" :description="error" />
56+ <template v-else-if="file">
57+ <UAlert
58+ v-if="file.truncated"
59+ color="warning"
60+ variant="subtle"
61+ icon="i-lucide-scissors"
62+ title="Large file"
63+ :description="`Only the first part of this file is shown (${formatBytes(file.sizeBytes)} in total).`"
64+ />
65+
66+ <pre v-if="raw || !isMarkdown" class="max-h-[60vh] overflow-auto whitespace-pre-wrap rounded-lg bg-elevated p-3 font-mono text-xs">{{ file.content }}</pre>
67+ <template v-else>
68+ <div v-if="fmRows.length" class="overflow-hidden rounded-lg border border-default">
69+ <table class="w-full text-xs">
70+ <tbody class="divide-y divide-default">
71+ <tr v-for="[k, v] in fmRows" :key="k">
72+ <th class="w-1/4 bg-elevated/40 px-2 py-1.5 text-left align-top font-medium text-muted">{{ k }}</th>
73+ <td class="px-2 py-1.5 font-mono break-words">{{ v }}</td>
74+ </tr>
75+ </tbody>
76+ </table>
77+ </div>
78+ <!-- eslint-disable-next-line vue/no-v-html -->
79+ <div class="md-body" v-html="html" />
80+ </template>
81+ </template>
82+ </div>
83+</template>
added packages/ui/app/components/ScopeView.vue +264 -0
new file mode 100644
@@ -0,0 +1,264 @@
1+<script setup lang="ts">
2+import type { AgentId, InventoryItem, ItemType, ScopeInventory } from '@skilly/core'
3+
4+const props = defineProps<{ kind: 'home' | 'project', projectId?: string }>()
5+
6+const { request } = useApi()
7+const { projects, remove } = useProjects()
8+const toast = useToast()
9+const router = useRouter()
10+
11+const inventory = ref<ScopeInventory | null>(null)
12+const loading = ref(true)
13+const error = ref<string | null>(null)
14+
15+const agentFilter = ref<'all' | AgentId>('all')
16+const search = ref('')
17+const tab = ref<ItemType>('skill')
18+const selected = ref<InventoryItem | null>(null)
19+const detailOpen = ref(false)
20+const removeOpen = ref(false)
21+const removing = ref(false)
22+const errorsOpen = ref(false)
23+
24+const project = computed(() => (props.kind === 'project' ? projects.value.find(p => p.id === props.projectId) : undefined))
25+const title = computed(() => (props.kind === 'home' ? 'Home' : project.value?.name ?? props.projectId ?? 'Project'))
26+const rootPath = computed(() => inventory.value?.root ?? project.value?.path ?? '')
27+const folderMissing = computed(() => props.kind === 'project' && project.value && !project.value.exists)
28+
29+async function load(showLoading = true) {
30+ if (showLoading) loading.value = true
31+ try {
32+ const url = props.kind === 'home' ? '/api/home' : `/api/projects/${encodeURIComponent(props.projectId!)}/inventory`
33+ inventory.value = await request<ScopeInventory>(url, { silent: true })
34+ error.value = null
35+ } catch (e) {
36+ error.value = (e as Error).message
37+ } finally {
38+ loading.value = false
39+ }
40+}
41+
42+async function rescan() {
43+ await load(false)
44+ if (!error.value) toast.add({ title: 'Rescan complete', icon: 'i-lucide-check', color: 'success' })
45+}
46+
47+onMounted(() => load())
48+
49+const matches = (item: InventoryItem, q: string) => {
50+ const d = item.details
51+ const hay = [
52+ item.name, item.description, item.source.path, item.source.plugin,
53+ d.type === 'mcp' ? `${d.command ?? ''} ${d.url ?? ''} ${d.transport}` : '',
54+ d.type === 'hook' ? `${d.event} ${d.matcher ?? ''} ${d.command ?? ''}` : '',
55+ d.type === 'agent' ? d.model : ''
56+ ].join(' ').toLowerCase()
57+ return hay.includes(q)
58+}
59+
60+const filtered = computed(() => {
61+ const q = search.value.trim().toLowerCase()
62+ return (inventory.value?.items ?? []).filter(i =>
63+ (agentFilter.value === 'all' || i.agent === agentFilter.value) && (!q || matches(i, q))
64+ )
65+})
66+
67+const byType = computed(() => {
68+ const m = Object.fromEntries(TYPE_ORDER.map(t => [t, [] as InventoryItem[]])) as Record<ItemType, InventoryItem[]>
69+ for (const i of filtered.value) m[i.type].push(i)
70+ return m
71+})
72+
73+const tabs = computed(() => TYPE_ORDER.map(t => ({
74+ label: TYPE_META[t].label,
75+ icon: TYPE_META[t].icon,
76+ value: t,
77+ badge: { label: String(byType.value[t].length), color: 'neutral' as const, variant: 'subtle' as const }
78+})))
79+
80+const agentOptions = [
81+ { label: 'All', value: 'all' },
82+ { label: 'Claude Code', value: 'claude' },
83+ { label: 'Codex', value: 'codex' }
84+]
85+
86+function open(item: InventoryItem) {
87+ selected.value = item
88+ detailOpen.value = true
89+}
90+
91+async function copyPath() {
92+ try {
93+ await navigator.clipboard.writeText(rootPath.value)
94+ toast.add({ title: 'Path copied', icon: 'i-lucide-check', color: 'success' })
95+ } catch {
96+ toast.add({ title: 'Could not copy', color: 'error' })
97+ }
98+}
99+
100+async function confirmRemove() {
101+ if (!props.projectId) return
102+ removing.value = true
103+ try {
104+ await remove(props.projectId)
105+ removeOpen.value = false
106+ toast.add({ title: 'Project removed from skilly', description: 'No file was deleted.', icon: 'i-lucide-check', color: 'success' })
107+ await router.push('/')
108+ } catch {
109+ // toast already shown by useApi
110+ } finally {
111+ removing.value = false
112+ }
113+}
114+</script>
115+
116+<template>
117+ <UDashboardPanel :id="`scope-${kind}`">
118+ <template #header>
119+ <UDashboardNavbar :title="title" :icon="kind === 'home' ? 'i-lucide-house' : 'i-lucide-folder'">
120+ <template #right>
121+ <UButton
122+ icon="i-lucide-refresh-cw"
123+ label="Rescan"
124+ color="neutral"
125+ variant="outline"
126+ :loading="loading && !!inventory"
127+ @click="rescan"
128+ />
129+ <UButton
130+ v-if="kind === 'project'"
131+ icon="i-lucide-trash-2"
132+ label="Remove from skilly"
133+ color="error"
134+ variant="soft"
135+ @click="removeOpen = true"
136+ />
137+ </template>
138+ </UDashboardNavbar>
139+ </template>
140+
141+ <template #body>
142+ <!-- Loading -->
143+ <div v-if="loading && !inventory" class="space-y-3">
144+ <USkeleton class="h-6 w-1/2" />
145+ <USkeleton class="h-10 w-full" />
146+ <USkeleton v-for="i in 5" :key="i" class="h-12 w-full" />
147+ </div>
148+
149+ <!-- Error -->
150+ <div v-else-if="error && !inventory" class="mx-auto max-w-lg py-12">
151+ <UAlert
152+ color="error"
153+ variant="subtle"
154+ icon="i-lucide-circle-alert"
155+ title="Could not load the inventory"
156+ :description="error"
157+ :actions="[{ label: 'Retry', color: 'neutral', variant: 'outline', onClick: () => load() }]"
158+ />
159+ </div>
160+
161+ <div v-else-if="inventory" class="flex flex-col gap-4">
162+ <!-- Header info -->
163+ <div class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm">
164+ <div class="flex min-w-0 items-center gap-1">
165+ <span class="truncate font-mono text-xs text-muted" :title="rootPath">{{ rootPath }}</span>
166+ <UTooltip text="Copy path">
167+ <UButton icon="i-lucide-copy" size="xs" color="neutral" variant="ghost" aria-label="Copy path" @click="copyPath" />
168+ </UTooltip>
169+ </div>
170+ <div class="flex items-center gap-1.5">
171+ <AgentBadge v-for="a in inventory.agentsDetected" :key="a" :agent="a" />
172+ <span v-if="!inventory.agentsDetected.length" class="text-muted">No agent detected</span>
173+ </div>
174+ <span class="text-xs text-muted">Scanned {{ formatDate(inventory.scannedAt) }}</span>
175+ </div>
176+
177+ <UAlert
178+ v-if="folderMissing"
179+ color="warning"
180+ variant="subtle"
181+ icon="i-lucide-triangle-alert"
182+ title="This folder no longer exists on disk"
183+ description="The inventory below may be empty or outdated. You can remove the project from skilly."
184+ />
185+
186+ <!-- Scan errors -->
187+ <UAlert
188+ v-if="inventory.errors.length"
189+ color="warning"
190+ variant="subtle"
191+ icon="i-lucide-triangle-alert"
192+ :title="`${inventory.errors.length} scan error${inventory.errors.length > 1 ? 's' : ''}`"
193+ >
194+ <template #description>
195+ <UCollapsible v-model:open="errorsOpen">
196+ <UButton
197+ :label="errorsOpen ? 'Hide details' : 'Show details'"
198+ :trailing-icon="errorsOpen ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
199+ color="neutral"
200+ variant="link"
201+ size="xs"
202+ class="px-0"
203+ />
204+ <template #content>
205+ <ul class="mt-2 space-y-1.5 text-xs">
206+ <li v-for="(e, i) in inventory.errors" :key="i">
207+ <AgentBadge v-if="e.agent" :agent="e.agent" compact class="mr-1" />
208+ <span class="font-mono">{{ e.path }}</span>
209+ <span class="block text-muted">{{ e.message }}</span>
210+ </li>
211+ </ul>
212+ </template>
213+ </UCollapsible>
214+ </template>
215+ </UAlert>
216+
217+ <!-- Filters -->
218+ <div class="flex flex-wrap items-center gap-3">
219+ <UTabs
220+ v-model="agentFilter"
221+ :items="agentOptions"
222+ :content="false"
223+ size="sm"
224+ variant="pill"
225+ color="neutral"
226+ class="w-auto"
227+ />
228+ <UInput
229+ v-model="search"
230+ icon="i-lucide-search"
231+ placeholder="Search name, description, command..."
232+ class="w-72"
233+ >
234+ <template v-if="search" #trailing>
235+ <UButton icon="i-lucide-x" size="xs" color="neutral" variant="link" aria-label="Clear search" @click="search = ''" />
236+ </template>
237+ </UInput>
238+ </div>
239+
240+ <!-- Type tabs -->
241+ <UTabs v-model="tab" :items="tabs" :content="false" variant="link" class="w-full" />
242+
243+ <ItemTable :type="tab" :items="byType[tab]" :filtered="!!search || agentFilter !== 'all'" @select="open" />
244+ </div>
245+ </template>
246+ </UDashboardPanel>
247+
248+ <ItemDetail v-model:open="detailOpen" :item="selected" />
249+
250+ <UModal v-model:open="removeOpen" title="Remove project from skilly" description="This only forgets the project in skilly.">
251+ <template #body>
252+ <p class="text-sm">
253+ <strong>{{ title }}</strong> will disappear from the project list. <strong>No file is deleted</strong>:
254+ the folder and its agent configuration stay untouched. You can add it again at any time.
255+ </p>
256+ </template>
257+ <template #footer>
258+ <div class="flex w-full justify-end gap-2">
259+ <UButton label="Cancel" color="neutral" variant="outline" @click="removeOpen = false" />
260+ <UButton label="Remove from skilly" color="error" :loading="removing" @click="confirmRemove" />
261+ </div>
262+ </template>
263+ </UModal>
264+</template>
new file mode 100644
@@ -0,0 +1,264 @@
1+<script setup lang="ts">
2+import type { AgentId, InventoryItem, ItemType, ScopeInventory } from '@skilly/core'
3+
4+const props = defineProps<{ kind: 'home' | 'project', projectId?: string }>()
5+
6+const { request } = useApi()
7+const { projects, remove } = useProjects()
8+const toast = useToast()
9+const router = useRouter()
10+
11+const inventory = ref<ScopeInventory | null>(null)
12+const loading = ref(true)
13+const error = ref<string | null>(null)
14+
15+const agentFilter = ref<'all' | AgentId>('all')
16+const search = ref('')
17+const tab = ref<ItemType>('skill')
18+const selected = ref<InventoryItem | null>(null)
19+const detailOpen = ref(false)
20+const removeOpen = ref(false)
21+const removing = ref(false)
22+const errorsOpen = ref(false)
23+
24+const project = computed(() => (props.kind === 'project' ? projects.value.find(p => p.id === props.projectId) : undefined))
25+const title = computed(() => (props.kind === 'home' ? 'Home' : project.value?.name ?? props.projectId ?? 'Project'))
26+const rootPath = computed(() => inventory.value?.root ?? project.value?.path ?? '')
27+const folderMissing = computed(() => props.kind === 'project' && project.value && !project.value.exists)
28+
29+async function load(showLoading = true) {
30+ if (showLoading) loading.value = true
31+ try {
32+ const url = props.kind === 'home' ? '/api/home' : `/api/projects/${encodeURIComponent(props.projectId!)}/inventory`
33+ inventory.value = await request<ScopeInventory>(url, { silent: true })
34+ error.value = null
35+ } catch (e) {
36+ error.value = (e as Error).message
37+ } finally {
38+ loading.value = false
39+ }
40+}
41+
42+async function rescan() {
43+ await load(false)
44+ if (!error.value) toast.add({ title: 'Rescan complete', icon: 'i-lucide-check', color: 'success' })
45+}
46+
47+onMounted(() => load())
48+
49+const matches = (item: InventoryItem, q: string) => {
50+ const d = item.details
51+ const hay = [
52+ item.name, item.description, item.source.path, item.source.plugin,
53+ d.type === 'mcp' ? `${d.command ?? ''} ${d.url ?? ''} ${d.transport}` : '',
54+ d.type === 'hook' ? `${d.event} ${d.matcher ?? ''} ${d.command ?? ''}` : '',
55+ d.type === 'agent' ? d.model : ''
56+ ].join(' ').toLowerCase()
57+ return hay.includes(q)
58+}
59+
60+const filtered = computed(() => {
61+ const q = search.value.trim().toLowerCase()
62+ return (inventory.value?.items ?? []).filter(i =>
63+ (agentFilter.value === 'all' || i.agent === agentFilter.value) && (!q || matches(i, q))
64+ )
65+})
66+
67+const byType = computed(() => {
68+ const m = Object.fromEntries(TYPE_ORDER.map(t => [t, [] as InventoryItem[]])) as Record<ItemType, InventoryItem[]>
69+ for (const i of filtered.value) m[i.type].push(i)
70+ return m
71+})
72+
73+const tabs = computed(() => TYPE_ORDER.map(t => ({
74+ label: TYPE_META[t].label,
75+ icon: TYPE_META[t].icon,
76+ value: t,
77+ badge: { label: String(byType.value[t].length), color: 'neutral' as const, variant: 'subtle' as const }
78+})))
79+
80+const agentOptions = [
81+ { label: 'All', value: 'all' },
82+ { label: 'Claude Code', value: 'claude' },
83+ { label: 'Codex', value: 'codex' }
84+]
85+
86+function open(item: InventoryItem) {
87+ selected.value = item
88+ detailOpen.value = true
89+}
90+
91+async function copyPath() {
92+ try {
93+ await navigator.clipboard.writeText(rootPath.value)
94+ toast.add({ title: 'Path copied', icon: 'i-lucide-check', color: 'success' })
95+ } catch {
96+ toast.add({ title: 'Could not copy', color: 'error' })
97+ }
98+}
99+
100+async function confirmRemove() {
101+ if (!props.projectId) return
102+ removing.value = true
103+ try {
104+ await remove(props.projectId)
105+ removeOpen.value = false
106+ toast.add({ title: 'Project removed from skilly', description: 'No file was deleted.', icon: 'i-lucide-check', color: 'success' })
107+ await router.push('/')
108+ } catch {
109+ // toast already shown by useApi
110+ } finally {
111+ removing.value = false
112+ }
113+}
114+</script>
115+
116+<template>
117+ <UDashboardPanel :id="`scope-${kind}`">
118+ <template #header>
119+ <UDashboardNavbar :title="title" :icon="kind === 'home' ? 'i-lucide-house' : 'i-lucide-folder'">
120+ <template #right>
121+ <UButton
122+ icon="i-lucide-refresh-cw"
123+ label="Rescan"
124+ color="neutral"
125+ variant="outline"
126+ :loading="loading && !!inventory"
127+ @click="rescan"
128+ />
129+ <UButton
130+ v-if="kind === 'project'"
131+ icon="i-lucide-trash-2"
132+ label="Remove from skilly"
133+ color="error"
134+ variant="soft"
135+ @click="removeOpen = true"
136+ />
137+ </template>
138+ </UDashboardNavbar>
139+ </template>
140+
141+ <template #body>
142+ <!-- Loading -->
143+ <div v-if="loading && !inventory" class="space-y-3">
144+ <USkeleton class="h-6 w-1/2" />
145+ <USkeleton class="h-10 w-full" />
146+ <USkeleton v-for="i in 5" :key="i" class="h-12 w-full" />
147+ </div>
148+
149+ <!-- Error -->
150+ <div v-else-if="error && !inventory" class="mx-auto max-w-lg py-12">
151+ <UAlert
152+ color="error"
153+ variant="subtle"
154+ icon="i-lucide-circle-alert"
155+ title="Could not load the inventory"
156+ :description="error"
157+ :actions="[{ label: 'Retry', color: 'neutral', variant: 'outline', onClick: () => load() }]"
158+ />
159+ </div>
160+
161+ <div v-else-if="inventory" class="flex flex-col gap-4">
162+ <!-- Header info -->
163+ <div class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm">
164+ <div class="flex min-w-0 items-center gap-1">
165+ <span class="truncate font-mono text-xs text-muted" :title="rootPath">{{ rootPath }}</span>
166+ <UTooltip text="Copy path">
167+ <UButton icon="i-lucide-copy" size="xs" color="neutral" variant="ghost" aria-label="Copy path" @click="copyPath" />
168+ </UTooltip>
169+ </div>
170+ <div class="flex items-center gap-1.5">
171+ <AgentBadge v-for="a in inventory.agentsDetected" :key="a" :agent="a" />
172+ <span v-if="!inventory.agentsDetected.length" class="text-muted">No agent detected</span>
173+ </div>
174+ <span class="text-xs text-muted">Scanned {{ formatDate(inventory.scannedAt) }}</span>
175+ </div>
176+
177+ <UAlert
178+ v-if="folderMissing"
179+ color="warning"
180+ variant="subtle"
181+ icon="i-lucide-triangle-alert"
182+ title="This folder no longer exists on disk"
183+ description="The inventory below may be empty or outdated. You can remove the project from skilly."
184+ />
185+
186+ <!-- Scan errors -->
187+ <UAlert
188+ v-if="inventory.errors.length"
189+ color="warning"
190+ variant="subtle"
191+ icon="i-lucide-triangle-alert"
192+ :title="`${inventory.errors.length} scan error${inventory.errors.length > 1 ? 's' : ''}`"
193+ >
194+ <template #description>
195+ <UCollapsible v-model:open="errorsOpen">
196+ <UButton
197+ :label="errorsOpen ? 'Hide details' : 'Show details'"
198+ :trailing-icon="errorsOpen ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
199+ color="neutral"
200+ variant="link"
201+ size="xs"
202+ class="px-0"
203+ />
204+ <template #content>
205+ <ul class="mt-2 space-y-1.5 text-xs">
206+ <li v-for="(e, i) in inventory.errors" :key="i">
207+ <AgentBadge v-if="e.agent" :agent="e.agent" compact class="mr-1" />
208+ <span class="font-mono">{{ e.path }}</span>
209+ <span class="block text-muted">{{ e.message }}</span>
210+ </li>
211+ </ul>
212+ </template>
213+ </UCollapsible>
214+ </template>
215+ </UAlert>
216+
217+ <!-- Filters -->
218+ <div class="flex flex-wrap items-center gap-3">
219+ <UTabs
220+ v-model="agentFilter"
221+ :items="agentOptions"
222+ :content="false"
223+ size="sm"
224+ variant="pill"
225+ color="neutral"
226+ class="w-auto"
227+ />
228+ <UInput
229+ v-model="search"
230+ icon="i-lucide-search"
231+ placeholder="Search name, description, command..."
232+ class="w-72"
233+ >
234+ <template v-if="search" #trailing>
235+ <UButton icon="i-lucide-x" size="xs" color="neutral" variant="link" aria-label="Clear search" @click="search = ''" />
236+ </template>
237+ </UInput>
238+ </div>
239+
240+ <!-- Type tabs -->
241+ <UTabs v-model="tab" :items="tabs" :content="false" variant="link" class="w-full" />
242+
243+ <ItemTable :type="tab" :items="byType[tab]" :filtered="!!search || agentFilter !== 'all'" @select="open" />
244+ </div>
245+ </template>
246+ </UDashboardPanel>
247+
248+ <ItemDetail v-model:open="detailOpen" :item="selected" />
249+
250+ <UModal v-model:open="removeOpen" title="Remove project from skilly" description="This only forgets the project in skilly.">
251+ <template #body>
252+ <p class="text-sm">
253+ <strong>{{ title }}</strong> will disappear from the project list. <strong>No file is deleted</strong>:
254+ the folder and its agent configuration stay untouched. You can add it again at any time.
255+ </p>
256+ </template>
257+ <template #footer>
258+ <div class="flex w-full justify-end gap-2">
259+ <UButton label="Cancel" color="neutral" variant="outline" @click="removeOpen = false" />
260+ <UButton label="Remove from skilly" color="error" :loading="removing" @click="confirmRemove" />
261+ </div>
262+ </template>
263+ </UModal>
264+</template>
added packages/ui/app/composables/useApi.ts +44 -0
new file mode 100644
@@ -0,0 +1,44 @@
1+import type { ApiError } from '@skilly/core'
2+
3+export class ApiRequestError extends Error {
4+ status: number
5+ constructor(message: string, status: number) {
6+ super(message)
7+ this.status = status
8+ }
9+}
10+
11+export interface ApiOptions {
12+ method?: 'GET' | 'POST' | 'DELETE'
13+ body?: Record<string, unknown>
14+ query?: Record<string, string | undefined>
15+ /** Do not show an error toast (the caller handles the error itself). */
16+ silent?: boolean
17+}
18+
19+export function useApi() {
20+ const toast = useToast()
21+
22+ async function request<T = unknown>(path: string, opts: ApiOptions = {}): Promise<T> {
23+ try {
24+ return await $fetch<T>(path, {
25+ method: opts.method ?? 'GET',
26+ body: opts.body,
27+ query: opts.query,
28+ headers: { 'x-skilly-token': getToken() }
29+ })
30+ } catch (e: any) {
31+ const status: number = e?.response?.status ?? e?.statusCode ?? 0
32+ const data = e?.data as Partial<ApiError> | undefined
33+ const message = data?.error
34+ ?? (status === 401 ? 'Unauthorized: the session token is missing or invalid. Relaunch skilly.' : undefined)
35+ ?? (status === 0 ? 'Cannot reach the skilly server.' : e?.message ?? 'Request failed')
36+ if (!opts.silent) {
37+ toast.add({ title: 'Request failed', description: message, color: 'error', icon: 'i-lucide-circle-alert' })
38+ }
39+ throw new ApiRequestError(message, status)
40+ }
41+ }
42+
43+ return { request }
44+}
new file mode 100644
@@ -0,0 +1,44 @@
1+import type { ApiError } from '@skilly/core'
2+
3+export class ApiRequestError extends Error {
4+ status: number
5+ constructor(message: string, status: number) {
6+ super(message)
7+ this.status = status
8+ }
9+}
10+
11+export interface ApiOptions {
12+ method?: 'GET' | 'POST' | 'DELETE'
13+ body?: Record<string, unknown>
14+ query?: Record<string, string | undefined>
15+ /** Do not show an error toast (the caller handles the error itself). */
16+ silent?: boolean
17+}
18+
19+export function useApi() {
20+ const toast = useToast()
21+
22+ async function request<T = unknown>(path: string, opts: ApiOptions = {}): Promise<T> {
23+ try {
24+ return await $fetch<T>(path, {
25+ method: opts.method ?? 'GET',
26+ body: opts.body,
27+ query: opts.query,
28+ headers: { 'x-skilly-token': getToken() }
29+ })
30+ } catch (e: any) {
31+ const status: number = e?.response?.status ?? e?.statusCode ?? 0
32+ const data = e?.data as Partial<ApiError> | undefined
33+ const message = data?.error
34+ ?? (status === 401 ? 'Unauthorized: the session token is missing or invalid. Relaunch skilly.' : undefined)
35+ ?? (status === 0 ? 'Cannot reach the skilly server.' : e?.message ?? 'Request failed')
36+ if (!opts.silent) {
37+ toast.add({ title: 'Request failed', description: message, color: 'error', icon: 'i-lucide-circle-alert' })
38+ }
39+ throw new ApiRequestError(message, status)
40+ }
41+ }
42+
43+ return { request }
44+}
added packages/ui/app/composables/useProjects.ts +58 -0
new file mode 100644
@@ -0,0 +1,58 @@
1+import type { ProjectSummary, ScopeInventory } from '@skilly/core'
2+
3+/** Shared list of added projects (sidebar, dashboard, scope pages). */
4+export function useProjects() {
5+ const { request } = useApi()
6+ const projects = useState<ProjectSummary[]>('skilly-projects', () => [])
7+ const loading = useState('skilly-projects-loading', () => false)
8+ const loaded = useState('skilly-projects-loaded', () => false)
9+ const error = useState<string | null>('skilly-projects-error', () => null)
10+
11+ async function refresh() {
12+ loading.value = true
13+ try {
14+ projects.value = await request<ProjectSummary[]>('/api/projects', { silent: true })
15+ error.value = null
16+ } catch (e) {
17+ error.value = (e as Error).message
18+ } finally {
19+ loading.value = false
20+ loaded.value = true
21+ }
22+ }
23+
24+ async function add(path: string): Promise<ProjectSummary> {
25+ const created = await request<ProjectSummary>('/api/projects', { method: 'POST', body: { path }, silent: true })
26+ await refresh()
27+ return created
28+ }
29+
30+ async function remove(id: string) {
31+ await request(`/api/projects/${encodeURIComponent(id)}`, { method: 'DELETE' })
32+ await refresh()
33+ }
34+
35+ return { projects, loading, loaded, error, refresh, add, remove }
36+}
37+
38+/** Shared home inventory (used by the dashboard card and the Home page). */
39+export function useHomeInventory() {
40+ const { request } = useApi()
41+ const home = useState<ScopeInventory | null>('skilly-home', () => null)
42+ const loading = useState('skilly-home-loading', () => false)
43+ const error = useState<string | null>('skilly-home-error', () => null)
44+
45+ async function refresh() {
46+ loading.value = true
47+ try {
48+ home.value = await request<ScopeInventory>('/api/home', { silent: true })
49+ error.value = null
50+ } catch (e) {
51+ error.value = (e as Error).message
52+ } finally {
53+ loading.value = false
54+ }
55+ }
56+
57+ return { home, loading, error, refresh }
58+}
new file mode 100644
@@ -0,0 +1,58 @@
1+import type { ProjectSummary, ScopeInventory } from '@skilly/core'
2+
3+/** Shared list of added projects (sidebar, dashboard, scope pages). */
4+export function useProjects() {
5+ const { request } = useApi()
6+ const projects = useState<ProjectSummary[]>('skilly-projects', () => [])
7+ const loading = useState('skilly-projects-loading', () => false)
8+ const loaded = useState('skilly-projects-loaded', () => false)
9+ const error = useState<string | null>('skilly-projects-error', () => null)
10+
11+ async function refresh() {
12+ loading.value = true
13+ try {
14+ projects.value = await request<ProjectSummary[]>('/api/projects', { silent: true })
15+ error.value = null
16+ } catch (e) {
17+ error.value = (e as Error).message
18+ } finally {
19+ loading.value = false
20+ loaded.value = true
21+ }
22+ }
23+
24+ async function add(path: string): Promise<ProjectSummary> {
25+ const created = await request<ProjectSummary>('/api/projects', { method: 'POST', body: { path }, silent: true })
26+ await refresh()
27+ return created
28+ }
29+
30+ async function remove(id: string) {
31+ await request(`/api/projects/${encodeURIComponent(id)}`, { method: 'DELETE' })
32+ await refresh()
33+ }
34+
35+ return { projects, loading, loaded, error, refresh, add, remove }
36+}
37+
38+/** Shared home inventory (used by the dashboard card and the Home page). */
39+export function useHomeInventory() {
40+ const { request } = useApi()
41+ const home = useState<ScopeInventory | null>('skilly-home', () => null)
42+ const loading = useState('skilly-home-loading', () => false)
43+ const error = useState<string | null>('skilly-home-error', () => null)
44+
45+ async function refresh() {
46+ loading.value = true
47+ try {
48+ home.value = await request<ScopeInventory>('/api/home', { silent: true })
49+ error.value = null
50+ } catch (e) {
51+ error.value = (e as Error).message
52+ } finally {
53+ loading.value = false
54+ }
55+ }
56+
57+ return { home, loading, error, refresh }
58+}
added packages/ui/app/layouts/default.vue +34 -0
new file mode 100644
@@ -0,0 +1,34 @@
1+<script setup lang="ts">
2+const { refresh } = useProjects()
3+const addOpen = useState('skilly-add-open', () => false)
4+
5+onMounted(refresh)
6+</script>
7+
8+<template>
9+ <UDashboardGroup>
10+ <UDashboardSidebar :ui="{ footer: 'border-t border-default' }">
11+ <template #header>
12+ <NuxtLink to="/" class="flex items-center gap-2 px-1 py-1">
13+ <span class="grid size-8 place-items-center rounded-lg bg-primary text-inverted">
14+ <UIcon name="i-lucide-wand-sparkles" class="size-5" />
15+ </span>
16+ <span class="text-lg font-semibold tracking-tight">skilly</span>
17+ </NuxtLink>
18+ </template>
19+
20+ <AppSidebar @add="addOpen = true" />
21+
22+ <template #footer>
23+ <div class="flex w-full items-center justify-between">
24+ <span class="text-xs text-muted">Read-only inventory</span>
25+ <UColorModeButton />
26+ </div>
27+ </template>
28+ </UDashboardSidebar>
29+
30+ <slot />
31+
32+ <AddProjectModal v-model:open="addOpen" />
33+ </UDashboardGroup>
34+</template>
new file mode 100644
@@ -0,0 +1,34 @@
1+<script setup lang="ts">
2+const { refresh } = useProjects()
3+const addOpen = useState('skilly-add-open', () => false)
4+
5+onMounted(refresh)
6+</script>
7+
8+<template>
9+ <UDashboardGroup>
10+ <UDashboardSidebar :ui="{ footer: 'border-t border-default' }">
11+ <template #header>
12+ <NuxtLink to="/" class="flex items-center gap-2 px-1 py-1">
13+ <span class="grid size-8 place-items-center rounded-lg bg-primary text-inverted">
14+ <UIcon name="i-lucide-wand-sparkles" class="size-5" />
15+ </span>
16+ <span class="text-lg font-semibold tracking-tight">skilly</span>
17+ </NuxtLink>
18+ </template>
19+
20+ <AppSidebar @add="addOpen = true" />
21+
22+ <template #footer>
23+ <div class="flex w-full items-center justify-between">
24+ <span class="text-xs text-muted">Read-only inventory</span>
25+ <UColorModeButton />
26+ </div>
27+ </template>
28+ </UDashboardSidebar>
29+
30+ <slot />
31+
32+ <AddProjectModal v-model:open="addOpen" />
33+ </UDashboardGroup>
34+</template>
added packages/ui/app/pages/home.vue +3 -0
new file mode 100644
@@ -0,0 +1,3 @@
1+<template>
2+ <ScopeView kind="home" />
3+</template>
new file mode 100644
@@ -0,0 +1,3 @@
1+<template>
2+ <ScopeView kind="home" />
3+</template>
added packages/ui/app/pages/index.vue +117 -0
new file mode 100644
@@ -0,0 +1,117 @@
1+<script setup lang="ts">
2+import type { ItemType, ScopeInventory } from '@skilly/core'
3+
4+const { projects, loaded, error: projectsError, refresh: refreshProjects } = useProjects()
5+const { home, loading: homeLoading, error: homeError, refresh: refreshHome } = useHomeInventory()
6+const addOpen = useState('skilly-add-open', () => false)
7+
8+onMounted(() => {
9+ if (!home.value) refreshHome()
10+})
11+
12+function countItems(inv: ScopeInventory): Record<ItemType, number> {
13+ const c = Object.fromEntries(TYPE_ORDER.map(t => [t, 0])) as Record<ItemType, number>
14+ for (const i of inv.items) c[i.type]++
15+ return c
16+}
17+
18+const homeCounts = computed(() => (home.value ? countItems(home.value) : null))
19+</script>
20+
21+<template>
22+ <UDashboardPanel id="dashboard">
23+ <template #header>
24+ <UDashboardNavbar title="Dashboard">
25+ <template #right>
26+ <UButton icon="i-lucide-folder-plus" label="Add project" @click="addOpen = true" />
27+ </template>
28+ </UDashboardNavbar>
29+ </template>
30+
31+ <template #body>
32+ <div class="mx-auto w-full max-w-5xl space-y-6">
33+ <p class="text-sm text-muted">
34+ Everything your coding agents can see: skills, rules, MCP servers, hooks, subagents and instruction files.
35+ </p>
36+
37+ <div class="grid gap-4 md:grid-cols-2">
38+ <!-- Home -->
39+ <UCard>
40+ <template #header>
41+ <div class="flex items-center gap-2">
42+ <UIcon name="i-lucide-house" class="size-5 text-primary" />
43+ <h2 class="font-semibold">Home</h2>
44+ <span class="text-xs text-muted">user-level configuration</span>
45+ </div>
46+ </template>
47+ <div v-if="homeLoading && !home" class="space-y-2">
48+ <USkeleton class="h-4 w-2/3" />
49+ <USkeleton class="h-4 w-full" />
50+ </div>
51+ <UAlert v-else-if="homeError" color="error" variant="subtle" icon="i-lucide-circle-alert" :title="homeError" />
52+ <div v-else-if="home && homeCounts" class="space-y-3">
53+ <p class="truncate font-mono text-xs text-muted" :title="home.root">{{ home.root }}</p>
54+ <div class="flex gap-2">
55+ <AgentBadge v-for="a in home.agentsDetected" :key="a" :agent="a" />
56+ <span v-if="!home.agentsDetected.length" class="text-sm text-muted">No agent detected</span>
57+ </div>
58+ <CountChips :counts="homeCounts" />
59+ </div>
60+ <template #footer>
61+ <UButton to="/home" label="Open" trailing-icon="i-lucide-arrow-right" color="neutral" variant="soft" size="sm" />
62+ </template>
63+ </UCard>
64+
65+ <!-- Projects -->
66+ <template v-if="loaded">
67+ <UCard v-for="p in projects" :key="p.id">
68+ <template #header>
69+ <div class="flex items-center gap-2">
70+ <UIcon name="i-lucide-folder" class="size-5 text-primary" />
71+ <h2 class="truncate font-semibold">{{ p.name }}</h2>
72+ <UBadge v-if="!p.exists" label="Folder missing" color="warning" variant="subtle" icon="i-lucide-triangle-alert" size="sm" />
73+ </div>
74+ </template>
75+ <div class="space-y-3">
76+ <p class="truncate font-mono text-xs text-muted" :title="p.path">{{ p.path }}</p>
77+ <div class="flex gap-2">
78+ <AgentBadge v-for="a in p.agentsDetected" :key="a" :agent="a" />
79+ <span v-if="!p.agentsDetected.length" class="text-sm text-muted">No agent detected</span>
80+ </div>
81+ <CountChips :counts="p.counts" />
82+ </div>
83+ <template #footer>
84+ <UButton :to="`/projects/${encodeURIComponent(p.id)}`" label="Open" trailing-icon="i-lucide-arrow-right" color="neutral" variant="soft" size="sm" />
85+ </template>
86+ </UCard>
87+ </template>
88+ <template v-else>
89+ <USkeleton class="h-48 w-full" />
90+ </template>
91+ </div>
92+
93+ <UAlert
94+ v-if="projectsError"
95+ color="error"
96+ variant="subtle"
97+ icon="i-lucide-circle-alert"
98+ title="Could not load projects"
99+ :description="projectsError"
100+ :actions="[{ label: 'Retry', color: 'neutral', variant: 'outline', onClick: refreshProjects }]"
101+ />
102+
103+ <div
104+ v-else-if="loaded && !projects.length"
105+ class="flex flex-col items-center gap-3 rounded-xl border border-dashed border-default py-14 text-center"
106+ >
107+ <UIcon name="i-lucide-folder-plus" class="size-10 text-dimmed" />
108+ <h3 class="font-semibold">Add your first project</h3>
109+ <p class="max-w-md text-sm text-muted">
110+ Pick a folder to see which skills, rules, MCP servers and instructions Claude Code and Codex use there.
111+ </p>
112+ <UButton icon="i-lucide-folder-plus" label="Add project" @click="addOpen = true" />
113+ </div>
114+ </div>
115+ </template>
116+ </UDashboardPanel>
117+</template>
new file mode 100644
@@ -0,0 +1,117 @@
1+<script setup lang="ts">
2+import type { ItemType, ScopeInventory } from '@skilly/core'
3+
4+const { projects, loaded, error: projectsError, refresh: refreshProjects } = useProjects()
5+const { home, loading: homeLoading, error: homeError, refresh: refreshHome } = useHomeInventory()
6+const addOpen = useState('skilly-add-open', () => false)
7+
8+onMounted(() => {
9+ if (!home.value) refreshHome()
10+})
11+
12+function countItems(inv: ScopeInventory): Record<ItemType, number> {
13+ const c = Object.fromEntries(TYPE_ORDER.map(t => [t, 0])) as Record<ItemType, number>
14+ for (const i of inv.items) c[i.type]++
15+ return c
16+}
17+
18+const homeCounts = computed(() => (home.value ? countItems(home.value) : null))
19+</script>
20+
21+<template>
22+ <UDashboardPanel id="dashboard">
23+ <template #header>
24+ <UDashboardNavbar title="Dashboard">
25+ <template #right>
26+ <UButton icon="i-lucide-folder-plus" label="Add project" @click="addOpen = true" />
27+ </template>
28+ </UDashboardNavbar>
29+ </template>
30+
31+ <template #body>
32+ <div class="mx-auto w-full max-w-5xl space-y-6">
33+ <p class="text-sm text-muted">
34+ Everything your coding agents can see: skills, rules, MCP servers, hooks, subagents and instruction files.
35+ </p>
36+
37+ <div class="grid gap-4 md:grid-cols-2">
38+ <!-- Home -->
39+ <UCard>
40+ <template #header>
41+ <div class="flex items-center gap-2">
42+ <UIcon name="i-lucide-house" class="size-5 text-primary" />
43+ <h2 class="font-semibold">Home</h2>
44+ <span class="text-xs text-muted">user-level configuration</span>
45+ </div>
46+ </template>
47+ <div v-if="homeLoading && !home" class="space-y-2">
48+ <USkeleton class="h-4 w-2/3" />
49+ <USkeleton class="h-4 w-full" />
50+ </div>
51+ <UAlert v-else-if="homeError" color="error" variant="subtle" icon="i-lucide-circle-alert" :title="homeError" />
52+ <div v-else-if="home && homeCounts" class="space-y-3">
53+ <p class="truncate font-mono text-xs text-muted" :title="home.root">{{ home.root }}</p>
54+ <div class="flex gap-2">
55+ <AgentBadge v-for="a in home.agentsDetected" :key="a" :agent="a" />
56+ <span v-if="!home.agentsDetected.length" class="text-sm text-muted">No agent detected</span>
57+ </div>
58+ <CountChips :counts="homeCounts" />
59+ </div>
60+ <template #footer>
61+ <UButton to="/home" label="Open" trailing-icon="i-lucide-arrow-right" color="neutral" variant="soft" size="sm" />
62+ </template>
63+ </UCard>
64+
65+ <!-- Projects -->
66+ <template v-if="loaded">
67+ <UCard v-for="p in projects" :key="p.id">
68+ <template #header>
69+ <div class="flex items-center gap-2">
70+ <UIcon name="i-lucide-folder" class="size-5 text-primary" />
71+ <h2 class="truncate font-semibold">{{ p.name }}</h2>
72+ <UBadge v-if="!p.exists" label="Folder missing" color="warning" variant="subtle" icon="i-lucide-triangle-alert" size="sm" />
73+ </div>
74+ </template>
75+ <div class="space-y-3">
76+ <p class="truncate font-mono text-xs text-muted" :title="p.path">{{ p.path }}</p>
77+ <div class="flex gap-2">
78+ <AgentBadge v-for="a in p.agentsDetected" :key="a" :agent="a" />
79+ <span v-if="!p.agentsDetected.length" class="text-sm text-muted">No agent detected</span>
80+ </div>
81+ <CountChips :counts="p.counts" />
82+ </div>
83+ <template #footer>
84+ <UButton :to="`/projects/${encodeURIComponent(p.id)}`" label="Open" trailing-icon="i-lucide-arrow-right" color="neutral" variant="soft" size="sm" />
85+ </template>
86+ </UCard>
87+ </template>
88+ <template v-else>
89+ <USkeleton class="h-48 w-full" />
90+ </template>
91+ </div>
92+
93+ <UAlert
94+ v-if="projectsError"
95+ color="error"
96+ variant="subtle"
97+ icon="i-lucide-circle-alert"
98+ title="Could not load projects"
99+ :description="projectsError"
100+ :actions="[{ label: 'Retry', color: 'neutral', variant: 'outline', onClick: refreshProjects }]"
101+ />
102+
103+ <div
104+ v-else-if="loaded && !projects.length"
105+ class="flex flex-col items-center gap-3 rounded-xl border border-dashed border-default py-14 text-center"
106+ >
107+ <UIcon name="i-lucide-folder-plus" class="size-10 text-dimmed" />
108+ <h3 class="font-semibold">Add your first project</h3>
109+ <p class="max-w-md text-sm text-muted">
110+ Pick a folder to see which skills, rules, MCP servers and instructions Claude Code and Codex use there.
111+ </p>
112+ <UButton icon="i-lucide-folder-plus" label="Add project" @click="addOpen = true" />
113+ </div>
114+ </div>
115+ </template>
116+ </UDashboardPanel>
117+</template>
added packages/ui/app/pages/projects/[id].vue +8 -0
new file mode 100644
@@ -0,0 +1,8 @@
1+<script setup lang="ts">
2+const route = useRoute()
3+const id = computed(() => String(route.params.id))
4+</script>
5+
6+<template>
7+ <ScopeView :key="id" kind="project" :project-id="id" />
8+</template>
new file mode 100644
@@ -0,0 +1,8 @@
1+<script setup lang="ts">
2+const route = useRoute()
3+const id = computed(() => String(route.params.id))
4+</script>
5+
6+<template>
7+ <ScopeView :key="id" kind="project" :project-id="id" />
8+</template>
added packages/ui/app/plugins/01.token.client.ts +13 -0
new file mode 100644
@@ -0,0 +1,13 @@
1+// Captures `?t=<token>` from the launch URL, stores it and removes it from the address bar.
2+export default defineNuxtPlugin(async () => {
3+ const router = useRouter()
4+ await router.isReady()
5+ const route = router.currentRoute.value
6+ const t = route.query.t
7+ const token = Array.isArray(t) ? t[0] : t
8+ if (token) {
9+ storeToken(token)
10+ const { t: _drop, ...query } = route.query
11+ await router.replace({ path: route.path, query, hash: route.hash })
12+ }
13+})
new file mode 100644
@@ -0,0 +1,13 @@
1+// Captures `?t=<token>` from the launch URL, stores it and removes it from the address bar.
2+export default defineNuxtPlugin(async () => {
3+ const router = useRouter()
4+ await router.isReady()
5+ const route = router.currentRoute.value
6+ const t = route.query.t
7+ const token = Array.isArray(t) ? t[0] : t
8+ if (token) {
9+ storeToken(token)
10+ const { t: _drop, ...query } = route.query
11+ await router.replace({ path: route.path, query, hash: route.hash })
12+ }
13+})
added packages/ui/app/plugins/02.heartbeat.client.ts +12 -0
new file mode 100644
@@ -0,0 +1,12 @@
1+// The server exits when heartbeats stop (window closed).
2+export default defineNuxtPlugin(() => {
3+ const ping = () => {
4+ fetch('/api/heartbeat', {
5+ method: 'POST',
6+ headers: { 'x-skilly-token': getToken() },
7+ keepalive: true
8+ }).catch(() => {})
9+ }
10+ ping()
11+ setInterval(ping, 5000)
12+})
new file mode 100644
@@ -0,0 +1,12 @@
1+// The server exits when heartbeats stop (window closed).
2+export default defineNuxtPlugin(() => {
3+ const ping = () => {
4+ fetch('/api/heartbeat', {
5+ method: 'POST',
6+ headers: { 'x-skilly-token': getToken() },
7+ keepalive: true
8+ }).catch(() => {})
9+ }
10+ ping()
11+ setInterval(ping, 5000)
12+})
added packages/ui/app/utils/format.ts +45 -0
new file mode 100644
@@ -0,0 +1,45 @@
1+import type { AgentId, ItemType } from '@skilly/core'
2+
3+export const AGENT_LABELS: Record<AgentId, string> = { claude: 'Claude Code', codex: 'Codex' }
4+
5+export const TYPE_ORDER: ItemType[] = ['skill', 'rule', 'mcp', 'hook', 'agent', 'instructions']
6+
7+export const TYPE_META: Record<ItemType, { label: string, icon: string }> = {
8+ skill: { label: 'Skills', icon: 'i-lucide-sparkles' },
9+ rule: { label: 'Rules', icon: 'i-lucide-scroll-text' },
10+ mcp: { label: 'MCP', icon: 'i-lucide-plug' },
11+ hook: { label: 'Hooks', icon: 'i-lucide-webhook' },
12+ agent: { label: 'Agents', icon: 'i-lucide-bot' },
13+ instructions: { label: 'Instructions', icon: 'i-lucide-file-text' }
14+}
15+
16+export function formatBytes(n: number): string {
17+ if (n < 1024) return `${n} B`
18+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
19+ return `${(n / 1024 / 1024).toFixed(1)} MB`
20+}
21+
22+export function formatDate(iso: string): string {
23+ const d = new Date(iso)
24+ return Number.isNaN(d.getTime()) ? iso : d.toLocaleString()
25+}
26+
27+/** Joins a directory path and a relative path, keeping the separator style of the directory. */
28+export function joinPath(dir: string, rel: string): string {
29+ const sep = dir.includes('\\') ? '\\' : '/'
30+ return dir.replace(/[\\/]+$/, '') + sep + rel.replace(/[\\/]/g, sep)
31+}
32+
33+/** Splits an absolute path into breadcrumb segments with their cumulative paths. */
34+export function pathSegments(p: string): { name: string, path: string }[] {
35+ const sep = p.includes('\\') ? '\\' : '/'
36+ const parts = p.split(/[\\/]+/).filter(Boolean)
37+ const out: { name: string, path: string }[] = []
38+ let acc = ''
39+ parts.forEach((part, i) => {
40+ if (i === 0) acc = /^[a-zA-Z]:$/.test(part) ? part + sep : (p.startsWith('/') ? '/' + part : part)
41+ else acc = acc.replace(/[\\/]+$/, '') + sep + part
42+ out.push({ name: part, path: acc })
43+ })
44+ return out
45+}
new file mode 100644
@@ -0,0 +1,45 @@
1+import type { AgentId, ItemType } from '@skilly/core'
2+
3+export const AGENT_LABELS: Record<AgentId, string> = { claude: 'Claude Code', codex: 'Codex' }
4+
5+export const TYPE_ORDER: ItemType[] = ['skill', 'rule', 'mcp', 'hook', 'agent', 'instructions']
6+
7+export const TYPE_META: Record<ItemType, { label: string, icon: string }> = {
8+ skill: { label: 'Skills', icon: 'i-lucide-sparkles' },
9+ rule: { label: 'Rules', icon: 'i-lucide-scroll-text' },
10+ mcp: { label: 'MCP', icon: 'i-lucide-plug' },
11+ hook: { label: 'Hooks', icon: 'i-lucide-webhook' },
12+ agent: { label: 'Agents', icon: 'i-lucide-bot' },
13+ instructions: { label: 'Instructions', icon: 'i-lucide-file-text' }
14+}
15+
16+export function formatBytes(n: number): string {
17+ if (n < 1024) return `${n} B`
18+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
19+ return `${(n / 1024 / 1024).toFixed(1)} MB`
20+}
21+
22+export function formatDate(iso: string): string {
23+ const d = new Date(iso)
24+ return Number.isNaN(d.getTime()) ? iso : d.toLocaleString()
25+}
26+
27+/** Joins a directory path and a relative path, keeping the separator style of the directory. */
28+export function joinPath(dir: string, rel: string): string {
29+ const sep = dir.includes('\\') ? '\\' : '/'
30+ return dir.replace(/[\\/]+$/, '') + sep + rel.replace(/[\\/]/g, sep)
31+}
32+
33+/** Splits an absolute path into breadcrumb segments with their cumulative paths. */
34+export function pathSegments(p: string): { name: string, path: string }[] {
35+ const sep = p.includes('\\') ? '\\' : '/'
36+ const parts = p.split(/[\\/]+/).filter(Boolean)
37+ const out: { name: string, path: string }[] = []
38+ let acc = ''
39+ parts.forEach((part, i) => {
40+ if (i === 0) acc = /^[a-zA-Z]:$/.test(part) ? part + sep : (p.startsWith('/') ? '/' + part : part)
41+ else acc = acc.replace(/[\\/]+$/, '') + sep + part
42+ out.push({ name: part, path: acc })
43+ })
44+ return out
45+}
added packages/ui/app/utils/markdown.ts +43 -0
new file mode 100644
@@ -0,0 +1,43 @@
1+import { Marked } from 'marked'
2+
3+const escapeHtml = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
4+
5+const md = new Marked({
6+ gfm: true,
7+ renderer: {
8+ // Never render raw HTML coming from files.
9+ html({ text }) { return escapeHtml(text) },
10+ link({ href, title, tokens }) {
11+ const safe = /^(https?:|mailto:|#)/i.test(href) ? href : '#'
12+ return `<a href="${escapeHtml(safe)}"${title ? ` title="${escapeHtml(title)}"` : ''} target="_blank" rel="noopener noreferrer">${this.parser.parseInline(tokens)}</a>`
13+ },
14+ image({ text }) { return escapeHtml(text) }
15+ }
16+})
17+
18+export function renderMarkdown(src: string): string {
19+ return md.parse(src, { async: false }) as string
20+}
21+
22+export interface ParsedMarkdown {
23+ frontmatter: Record<string, string> | null
24+ body: string
25+}
26+
27+/** Lightweight frontmatter extraction (top-level `key: value` pairs; continuation lines are appended). */
28+export function splitFrontmatter(src: string): ParsedMarkdown {
29+ const m = /^?---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(src)
30+ if (!m) return { frontmatter: null, body: src }
31+ const fm: Record<string, string> = {}
32+ let lastKey = ''
33+ for (const line of m[1]!.split(/\r?\n/)) {
34+ const kv = /^([A-Za-z0-9_.-]+):\s*(.*)$/.exec(line)
35+ if (kv) {
36+ lastKey = kv[1]!
37+ fm[lastKey] = kv[2]!.replace(/^(['"])(.*)\1$/, '$2')
38+ } else if (lastKey && line.trim()) {
39+ fm[lastKey] = (fm[lastKey] ? fm[lastKey] + ' ' : '') + line.trim()
40+ }
41+ }
42+ return { frontmatter: fm, body: src.slice(m[0].length) }
43+}
new file mode 100644
@@ -0,0 +1,43 @@
1+import { Marked } from 'marked'
2+
3+const escapeHtml = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
4+
5+const md = new Marked({
6+ gfm: true,
7+ renderer: {
8+ // Never render raw HTML coming from files.
9+ html({ text }) { return escapeHtml(text) },
10+ link({ href, title, tokens }) {
11+ const safe = /^(https?:|mailto:|#)/i.test(href) ? href : '#'
12+ return `<a href="${escapeHtml(safe)}"${title ? ` title="${escapeHtml(title)}"` : ''} target="_blank" rel="noopener noreferrer">${this.parser.parseInline(tokens)}</a>`
13+ },
14+ image({ text }) { return escapeHtml(text) }
15+ }
16+})
17+
18+export function renderMarkdown(src: string): string {
19+ return md.parse(src, { async: false }) as string
20+}
21+
22+export interface ParsedMarkdown {
23+ frontmatter: Record<string, string> | null
24+ body: string
25+}
26+
27+/** Lightweight frontmatter extraction (top-level `key: value` pairs; continuation lines are appended). */
28+export function splitFrontmatter(src: string): ParsedMarkdown {
29+ const m = /^?---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(src)
30+ if (!m) return { frontmatter: null, body: src }
31+ const fm: Record<string, string> = {}
32+ let lastKey = ''
33+ for (const line of m[1]!.split(/\r?\n/)) {
34+ const kv = /^([A-Za-z0-9_.-]+):\s*(.*)$/.exec(line)
35+ if (kv) {
36+ lastKey = kv[1]!
37+ fm[lastKey] = kv[2]!.replace(/^(['"])(.*)\1$/, '$2')
38+ } else if (lastKey && line.trim()) {
39+ fm[lastKey] = (fm[lastKey] ? fm[lastKey] + ' ' : '') + line.trim()
40+ }
41+ }
42+ return { frontmatter: fm, body: src.slice(m[0].length) }
43+}
added packages/ui/app/utils/token.ts +11 -0
new file mode 100644
@@ -0,0 +1,11 @@
1+const STORAGE_KEY = 'skilly-token'
2+
3+/** Reads the session token from sessionStorage, falling back to `dev` in development. */
4+export function getToken(): string {
5+ if (!import.meta.client) return ''
6+ return sessionStorage.getItem(STORAGE_KEY) ?? (import.meta.dev ? 'dev' : '')
7+}
8+
9+export function storeToken(token: string) {
10+ sessionStorage.setItem(STORAGE_KEY, token)
11+}
new file mode 100644
@@ -0,0 +1,11 @@
1+const STORAGE_KEY = 'skilly-token'
2+
3+/** Reads the session token from sessionStorage, falling back to `dev` in development. */
4+export function getToken(): string {
5+ if (!import.meta.client) return ''
6+ return sessionStorage.getItem(STORAGE_KEY) ?? (import.meta.dev ? 'dev' : '')
7+}
8+
9+export function storeToken(token: string) {
10+ sessionStorage.setItem(STORAGE_KEY, token)
11+}
added packages/ui/nuxt.config.ts +25 -0
new file mode 100644
@@ -0,0 +1,25 @@
1+export default defineNuxtConfig({
2+ compatibilityDate: '2025-07-15',
3+ ssr: false,
4+ modules: ['@nuxt/ui'],
5+ css: ['~/assets/css/main.css'],
6+ devtools: { enabled: false },
7+ app: {
8+ head: {
9+ title: 'skilly',
10+ link: [{ rel: 'icon', href: 'data:,' }]
11+ }
12+ },
13+ icon: {
14+ // Icons come from the locally installed lucide collection (no network needed at runtime).
15+ provider: 'none',
16+ clientBundle: { scan: true },
17+ serverBundle: 'local'
18+ },
19+ nitro: {
20+ devProxy: {
21+ '/api': { target: 'http://127.0.0.1:4300/api', changeOrigin: true }
22+ }
23+ },
24+ typescript: { strict: true }
25+})
new file mode 100644
@@ -0,0 +1,25 @@
1+export default defineNuxtConfig({
2+ compatibilityDate: '2025-07-15',
3+ ssr: false,
4+ modules: ['@nuxt/ui'],
5+ css: ['~/assets/css/main.css'],
6+ devtools: { enabled: false },
7+ app: {
8+ head: {
9+ title: 'skilly',
10+ link: [{ rel: 'icon', href: 'data:,' }]
11+ }
12+ },
13+ icon: {
14+ // Icons come from the locally installed lucide collection (no network needed at runtime).
15+ provider: 'none',
16+ clientBundle: { scan: true },
17+ serverBundle: 'local'
18+ },
19+ nitro: {
20+ devProxy: {
21+ '/api': { target: 'http://127.0.0.1:4300/api', changeOrigin: true }
22+ }
23+ },
24+ typescript: { strict: true }
25+})
modified packages/ui/package.json +5 -0
@@ -12,9 +12,14 @@
1212 "@iconify-json/lucide": "^1.2.0",
1313 "@nuxt/ui": "^4.12.0",
1414 "@skilly/core": "workspace:*",
15+ "marked": "^18.1.1",
1516 "nuxt": "^4.6.0",
1617 "tailwindcss": "^4.1.0",
1718 "vue": "^3.5.0",
1819 "vue-router": "^4.5.0"
20+ },
21+ "devDependencies": {
22+ "typescript": "~5.9.3",
23+ "vue-tsc": "^3.3.13"
1924 }
2025 }
@@ -12,9 +12,14 @@
12 "@iconify-json/lucide": "^1.2.0",12 "@iconify-json/lucide": "^1.2.0",
13 "@nuxt/ui": "^4.12.0",13 "@nuxt/ui": "^4.12.0",
14 "@skilly/core": "workspace:*",14 "@skilly/core": "workspace:*",
15+ "marked": "^18.1.1",
15 "nuxt": "^4.6.0",16 "nuxt": "^4.6.0",
16 "tailwindcss": "^4.1.0",17 "tailwindcss": "^4.1.0",
17 "vue": "^3.5.0",18 "vue": "^3.5.0",
18 "vue-router": "^4.5.0"19 "vue-router": "^4.5.0"
20+ },
21+ "devDependencies": {
22+ "typescript": "~5.9.3",
23+ "vue-tsc": "^3.3.13"
19 }24 }
20 }25 }
modified pnpm-lock.yaml +92 -13
@@ -65,9 +65,12 @@ importers:
6565 '@skilly/core':
6666 specifier: workspace:*
6767 version: link:../core
68+ marked:
69+ specifier: ^18.1.1
70+ version: 18.1.1
6871 nuxt:
6972 specifier: ^4.6.0
70- version: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1)
73+ version: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue-tsc@3.3.13(typescript@5.9.3))(yaml@2.9.1)
7174 tailwindcss:
7275 specifier: ^4.1.0
7376 version: 4.3.3
@@ -77,6 +80,13 @@ importers:
7780 vue-router:
7881 specifier: ^4.5.0
7982 version: 4.6.4(vue@3.5.44(typescript@5.9.3))
83+ devDependencies:
84+ typescript:
85+ specifier: ~5.9.3
86+ version: 5.9.3
87+ vue-tsc:
88+ specifier: ^3.3.13
89+ version: 3.3.13(typescript@5.9.3)
8090
8191 packages:
8292
@@ -1631,6 +1641,15 @@ packages:
16311641 '@vitest/utils@4.1.12':
16321642 resolution: {integrity: sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==}
16331643
1644+ '@volar/language-core@2.4.28':
1645+ resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
1646+
1647+ '@volar/source-map@2.4.28':
1648+ resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==}
1649+
1650+ '@volar/typescript@2.4.28':
1651+ resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==}
1652+
16341653 '@vue-macros/common@3.1.4':
16351654 resolution: {integrity: sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==}
16361655 engines: {node: '>=20.19.0'}
@@ -1685,6 +1704,9 @@ packages:
16851704 '@vue/devtools-shared@8.2.1':
16861705 resolution: {integrity: sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==}
16871706
1707+ '@vue/language-core@3.3.13':
1708+ resolution: {integrity: sha512-axA0Xemn5LuUKuKWob0/kVq7xQ2mJJM6VqA9Exvt35nM9yRC31goWVve/G/Zb2ZCLsXC8FHYjjfXlnshknSCew==}
1709+
16881710 '@vue/reactivity@3.5.44':
16891711 resolution: {integrity: sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw==}
16901712
@@ -1786,6 +1808,9 @@ packages:
17861808 resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
17871809 engines: {node: '>= 14'}
17881810
1811+ alien-signals@3.2.1:
1812+ resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
1813+
17891814 ansi-regex@5.0.1:
17901815 resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
17911816 engines: {node: '>=8'}
@@ -2894,6 +2919,11 @@ packages:
28942919 engines: {node: '>= 20'}
28952920 hasBin: true
28962921
2922+ marked@18.1.1:
2923+ resolution: {integrity: sha512-zEGgrtUG8crk+TrMacJSVABU4DYDDkS8uqgU3ohrn/2fW9C2AEfUrO3m6IhpIpQpl7YcVpiXXC9cSMWDANT3dg==}
2924+ engines: {node: '>= 20'}
2925+ hasBin: true
2926+
28972927 mdn-data@2.27.1:
28982928 resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
28992929
@@ -3124,6 +3154,9 @@ packages:
31243154 resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
31253155 engines: {node: '>= 0.8'}
31263156
3157+ path-browserify@1.0.1:
3158+ resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
3159+
31273160 path-key@3.1.1:
31283161 resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
31293162 engines: {node: '>=8'}
@@ -4037,6 +4070,9 @@ packages:
40374070 jsdom:
40384071 optional: true
40394072
4073+ vscode-uri@3.2.0:
4074+ resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==}
4075+
40404076 vue-bundle-renderer@2.4.0:
40414077 resolution: {integrity: sha512-IxHopQZgzTGBiuGERrQrAS7Lr3dCv+qxvgirEgCUFGAD4gYNfiaGYzC1DDojeZmJAmehp9gG8+17PGuVP6Ab2Q==}
40424078
@@ -4080,6 +4116,12 @@ packages:
40804116 vite:
40814117 optional: true
40824118
4119+ vue-tsc@3.3.13:
4120+ resolution: {integrity: sha512-jE4/bXdOMSHVxEGgQCqNllJy+9N3Gx6tfSAsg0yhFK/MAN6eXqISihupnDEKSCt12VVnjN2gI3AqLYcqoh5x/A==}
4121+ hasBin: true
4122+ peerDependencies:
4123+ typescript: '>=5.0.0'
4124+
40834125 vue@3.5.44:
40844126 resolution: {integrity: sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ==}
40854127 peerDependencies:
@@ -4956,7 +4998,7 @@ snapshots:
49564998 - magic-string
49574999 - unplugin
49585000
4959- '@nuxt/nitro-server@4.6.2(2361615bb0aa1e79f3cfd32ae4f54216)':
5001+ '@nuxt/nitro-server@4.6.2(e1cbd802f5c636043c2b5a4595875576)':
49605002 dependencies:
49615003 '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))
49625004 '@unhead/vue': 3.4.3(@oxc-project/types@0.155.0)(esbuild@0.28.2)(lightningcss@1.33.0)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3))
@@ -4975,7 +5017,7 @@ snapshots:
49755017 my-bad: 0.2.11(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))
49765018 nitropack: 2.13.5(rolldown@1.2.15)(srvx@1.0.6)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))
49775019 nostics: 1.3.0
4978- nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1)
5020+ nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue-tsc@3.3.13(typescript@5.9.3))(yaml@2.9.1)
49795021 ohash: 2.0.12
49805022 pathe: 2.0.3
49815023 radix3: 1.1.2
@@ -5191,7 +5233,7 @@ snapshots:
51915233 - vue
51925234 - webpack
51935235
5194- '@nuxt/vite-builder@4.6.2(657d436dea0451b72d4de16847ab6ff5)':
5236+ '@nuxt/vite-builder@4.6.2(cba4483e2f4589592cec2b2715beb36b)':
51955237 dependencies:
51965238 '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))
51975239 '@vitejs/plugin-vue': 6.0.10(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3))
@@ -5209,7 +5251,7 @@ snapshots:
52095251 knitwork: 1.3.0
52105252 mocked-exports: 0.1.1
52115253 my-bad: 0.2.11(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))
5212- nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1)
5254+ nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue-tsc@3.3.13(typescript@5.9.3))(yaml@2.9.1)
52135255 pathe: 2.0.3
52145256 pkg-types: 2.4.0
52155257 postcss: 8.5.31
@@ -5220,7 +5262,7 @@ snapshots:
52205262 unenv: 2.0.0-rc.24
52215263 vite: 8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)
52225264 vite-node: 6.0.0(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)
5223- vite-plugin-checker: 0.14.5(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))
5265+ vite-plugin-checker: 0.14.5(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue-tsc@3.3.13(typescript@5.9.3))
52245266 vue: 3.5.44(typescript@5.9.3)
52255267 vue-bundle-renderer: 2.4.0
52265268 optionalDependencies:
@@ -5258,7 +5300,7 @@ snapshots:
52585300 - vue-tsc
52595301 - yaml
52605302
5261- '@nuxt/vite-server@4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1))(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))':
5303+ '@nuxt/vite-server@4.6.2(ee8202584928b0f003c6ca97f6e69333)':
52625304 dependencies:
52635305 '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))
52645306 clickable-path: 0.1.1
@@ -5266,7 +5308,7 @@ snapshots:
52665308 exsolve: 1.1.3
52675309 mocked-exports: 0.1.1
52685310 my-bad: 0.2.11(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))
5269- nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1)
5311+ nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue-tsc@3.3.13(typescript@5.9.3))(yaml@2.9.1)
52705312 pathe: 2.0.3
52715313 rou3: 0.9.2
52725314 srvx: 1.0.5
@@ -5984,6 +6026,18 @@ snapshots:
59846026 convert-source-map: 2.0.0
59856027 tinyrainbow: 3.2.0
59866028
6029+ '@volar/language-core@2.4.28':
6030+ dependencies:
6031+ '@volar/source-map': 2.4.28
6032+
6033+ '@volar/source-map@2.4.28': {}
6034+
6035+ '@volar/typescript@2.4.28':
6036+ dependencies:
6037+ '@volar/language-core': 2.4.28
6038+ path-browserify: 1.0.1
6039+ vscode-uri: 3.2.0
6040+
59876041 '@vue-macros/common@3.1.4(vue@3.5.44(typescript@5.9.3))':
59886042 dependencies:
59896043 '@vue/compiler-sfc': 3.5.44
@@ -6074,6 +6128,16 @@ snapshots:
60746128
60756129 '@vue/devtools-shared@8.2.1': {}
60766130
6131+ '@vue/language-core@3.3.13':
6132+ dependencies:
6133+ '@volar/language-core': 2.4.28
6134+ '@vue/compiler-dom': 3.5.44
6135+ '@vue/shared': 3.5.44
6136+ alien-signals: 3.2.1
6137+ muggle-string: 0.4.1
6138+ path-browserify: 1.0.1
6139+ picomatch: 4.0.7
6140+
60776141 '@vue/reactivity@3.5.44':
60786142 dependencies:
60796143 '@vue/shared': 3.5.44
@@ -6152,6 +6216,8 @@ snapshots:
61526216
61536217 agent-base@7.1.4: {}
61546218
6219+ alien-signals@3.2.1: {}
6220+
61556221 ansi-regex@5.0.1: {}
61566222
61576223 ansi-regex@6.4.0: {}
@@ -7197,6 +7263,8 @@ snapshots:
71977263
71987264 marked@17.0.6: {}
71997265
7266+ marked@18.1.1: {}
7267+
72007268 mdn-data@2.27.1: {}
72017269
72027270 merge-stream@2.0.0: {}
@@ -7427,7 +7495,7 @@ snapshots:
74277495 path-key: 4.0.0
74287496 unicorn-magic: 0.3.0
74297497
7430- nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1):
7498+ nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue-tsc@3.3.13(typescript@5.9.3))(yaml@2.9.1):
74317499 dependencies:
74327500 '@dxup/nuxt': 0.5.10(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(jiti@2.7.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))
74337501 '@jridgewell/trace-mapping': 0.3.31
@@ -7435,11 +7503,11 @@ snapshots:
74357503 '@nuxt/devalue': 2.0.2
74367504 '@nuxt/devtools': 3.4.4(@nuxt/schema@4.6.2)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3))
74377505 '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))
7438- '@nuxt/nitro-server': 4.6.2(2361615bb0aa1e79f3cfd32ae4f54216)
7506+ '@nuxt/nitro-server': 4.6.2(e1cbd802f5c636043c2b5a4595875576)
74397507 '@nuxt/schema': 4.6.2
74407508 '@nuxt/telemetry': 2.9.1(@nuxt/kit@4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))))
7441- '@nuxt/vite-builder': 4.6.2(657d436dea0451b72d4de16847ab6ff5)
7442- '@nuxt/vite-server': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1))(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))
7509+ '@nuxt/vite-builder': 4.6.2(cba4483e2f4589592cec2b2715beb36b)
7510+ '@nuxt/vite-server': 4.6.2(ee8202584928b0f003c6ca97f6e69333)
74437511 '@unhead/vue': 3.4.3(@oxc-project/types@0.155.0)(esbuild@0.28.2)(lightningcss@1.33.0)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3))
74447512 '@vue/shared': 3.5.44
74457513 chokidar: 5.0.0
@@ -7626,6 +7694,8 @@ snapshots:
76267694
76277695 parseurl@1.3.3: {}
76287696
7697+ path-browserify@1.0.1: {}
7698+
76297699 path-key@3.1.1: {}
76307700
76317701 path-key@4.0.0: {}
@@ -8455,7 +8525,7 @@ snapshots:
84558525 - tsx
84568526 - yaml
84578527
8458- vite-plugin-checker@0.14.5(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)):
8528+ vite-plugin-checker@0.14.5(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue-tsc@3.3.13(typescript@5.9.3)):
84598529 dependencies:
84608530 '@babel/code-frame': 7.29.7
84618531 chokidar: 5.0.0
@@ -8467,6 +8537,7 @@ snapshots:
84678537 vite: 8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)
84688538 optionalDependencies:
84698539 typescript: 5.9.3
8540+ vue-tsc: 3.3.13(typescript@5.9.3)
84708541
84718542 vite-plugin-inspect@11.4.1(@nuxt/kit@4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))))(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)):
84728543 dependencies:
@@ -8536,6 +8607,8 @@ snapshots:
85368607 transitivePeerDependencies:
85378608 - msw
85388609
8610+ vscode-uri@3.2.0: {}
8611+
85398612 vue-bundle-renderer@2.4.0:
85408613 dependencies:
85418614 ufo: 1.6.4
@@ -8586,6 +8659,12 @@ snapshots:
85868659 - unloader
85878660 - webpack
85888661
8662+ vue-tsc@3.3.13(typescript@5.9.3):
8663+ dependencies:
8664+ '@volar/typescript': 2.4.28
8665+ '@vue/language-core': 3.3.13
8666+ typescript: 5.9.3
8667+
85898668 vue@3.5.44(typescript@5.9.3):
85908669 dependencies:
85918670 '@vue/compiler-dom': 3.5.44
@@ -65,9 +65,12 @@ importers:
65 '@skilly/core':65 '@skilly/core':
66 specifier: workspace:*66 specifier: workspace:*
67 version: link:../core67 version: link:../core
68+ marked:
69+ specifier: ^18.1.1
70+ version: 18.1.1
68 nuxt:71 nuxt:
69 specifier: ^4.6.072 specifier: ^4.6.0
70- version: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1)73+ version: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue-tsc@3.3.13(typescript@5.9.3))(yaml@2.9.1)
71 tailwindcss:74 tailwindcss:
72 specifier: ^4.1.075 specifier: ^4.1.0
73 version: 4.3.376 version: 4.3.3
@@ -77,6 +80,13 @@ importers:
77 vue-router:80 vue-router:
78 specifier: ^4.5.081 specifier: ^4.5.0
79 version: 4.6.4(vue@3.5.44(typescript@5.9.3))82 version: 4.6.4(vue@3.5.44(typescript@5.9.3))
83+ devDependencies:
84+ typescript:
85+ specifier: ~5.9.3
86+ version: 5.9.3
87+ vue-tsc:
88+ specifier: ^3.3.13
89+ version: 3.3.13(typescript@5.9.3)
80 90
81 packages:91 packages:
82 92
@@ -1631,6 +1641,15 @@ packages:
1631 '@vitest/utils@4.1.12':1641 '@vitest/utils@4.1.12':
1632 resolution: {integrity: sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==}1642 resolution: {integrity: sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==}
1633 1643
1644+ '@volar/language-core@2.4.28':
1645+ resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
1646+
1647+ '@volar/source-map@2.4.28':
1648+ resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==}
1649+
1650+ '@volar/typescript@2.4.28':
1651+ resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==}
1652+
1634 '@vue-macros/common@3.1.4':1653 '@vue-macros/common@3.1.4':
1635 resolution: {integrity: sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==}1654 resolution: {integrity: sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==}
1636 engines: {node: '>=20.19.0'}1655 engines: {node: '>=20.19.0'}
@@ -1685,6 +1704,9 @@ packages:
1685 '@vue/devtools-shared@8.2.1':1704 '@vue/devtools-shared@8.2.1':
1686 resolution: {integrity: sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==}1705 resolution: {integrity: sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==}
1687 1706
1707+ '@vue/language-core@3.3.13':
1708+ resolution: {integrity: sha512-axA0Xemn5LuUKuKWob0/kVq7xQ2mJJM6VqA9Exvt35nM9yRC31goWVve/G/Zb2ZCLsXC8FHYjjfXlnshknSCew==}
1709+
1688 '@vue/reactivity@3.5.44':1710 '@vue/reactivity@3.5.44':
1689 resolution: {integrity: sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw==}1711 resolution: {integrity: sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw==}
1690 1712
@@ -1786,6 +1808,9 @@ packages:
1786 resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}1808 resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
1787 engines: {node: '>= 14'}1809 engines: {node: '>= 14'}
1788 1810
1811+ alien-signals@3.2.1:
1812+ resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
1813+
1789 ansi-regex@5.0.1:1814 ansi-regex@5.0.1:
1790 resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}1815 resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
1791 engines: {node: '>=8'}1816 engines: {node: '>=8'}
@@ -2894,6 +2919,11 @@ packages:
2894 engines: {node: '>= 20'}2919 engines: {node: '>= 20'}
2895 hasBin: true2920 hasBin: true
2896 2921
2922+ marked@18.1.1:
2923+ resolution: {integrity: sha512-zEGgrtUG8crk+TrMacJSVABU4DYDDkS8uqgU3ohrn/2fW9C2AEfUrO3m6IhpIpQpl7YcVpiXXC9cSMWDANT3dg==}
2924+ engines: {node: '>= 20'}
2925+ hasBin: true
2926+
2897 mdn-data@2.27.1:2927 mdn-data@2.27.1:
2898 resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}2928 resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
2899 2929
@@ -3124,6 +3154,9 @@ packages:
3124 resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}3154 resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
3125 engines: {node: '>= 0.8'}3155 engines: {node: '>= 0.8'}
3126 3156
3157+ path-browserify@1.0.1:
3158+ resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
3159+
3127 path-key@3.1.1:3160 path-key@3.1.1:
3128 resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}3161 resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
3129 engines: {node: '>=8'}3162 engines: {node: '>=8'}
@@ -4037,6 +4070,9 @@ packages:
4037 jsdom:4070 jsdom:
4038 optional: true4071 optional: true
4039 4072
4073+ vscode-uri@3.2.0:
4074+ resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==}
4075+
4040 vue-bundle-renderer@2.4.0:4076 vue-bundle-renderer@2.4.0:
4041 resolution: {integrity: sha512-IxHopQZgzTGBiuGERrQrAS7Lr3dCv+qxvgirEgCUFGAD4gYNfiaGYzC1DDojeZmJAmehp9gG8+17PGuVP6Ab2Q==}4077 resolution: {integrity: sha512-IxHopQZgzTGBiuGERrQrAS7Lr3dCv+qxvgirEgCUFGAD4gYNfiaGYzC1DDojeZmJAmehp9gG8+17PGuVP6Ab2Q==}
4042 4078
@@ -4080,6 +4116,12 @@ packages:
4080 vite:4116 vite:
4081 optional: true4117 optional: true
4082 4118
4119+ vue-tsc@3.3.13:
4120+ resolution: {integrity: sha512-jE4/bXdOMSHVxEGgQCqNllJy+9N3Gx6tfSAsg0yhFK/MAN6eXqISihupnDEKSCt12VVnjN2gI3AqLYcqoh5x/A==}
4121+ hasBin: true
4122+ peerDependencies:
4123+ typescript: '>=5.0.0'
4124+
4083 vue@3.5.44:4125 vue@3.5.44:
4084 resolution: {integrity: sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ==}4126 resolution: {integrity: sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ==}
4085 peerDependencies:4127 peerDependencies:
@@ -4956,7 +4998,7 @@ snapshots:
4956 - magic-string4998 - magic-string
4957 - unplugin4999 - unplugin
4958 5000
4959- '@nuxt/nitro-server@4.6.2(2361615bb0aa1e79f3cfd32ae4f54216)':5001+ '@nuxt/nitro-server@4.6.2(e1cbd802f5c636043c2b5a4595875576)':
4960 dependencies:5002 dependencies:
4961 '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))5003 '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))
4962 '@unhead/vue': 3.4.3(@oxc-project/types@0.155.0)(esbuild@0.28.2)(lightningcss@1.33.0)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3))5004 '@unhead/vue': 3.4.3(@oxc-project/types@0.155.0)(esbuild@0.28.2)(lightningcss@1.33.0)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3))
@@ -4975,7 +5017,7 @@ snapshots:
4975 my-bad: 0.2.11(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))5017 my-bad: 0.2.11(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))
4976 nitropack: 2.13.5(rolldown@1.2.15)(srvx@1.0.6)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))5018 nitropack: 2.13.5(rolldown@1.2.15)(srvx@1.0.6)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))
4977 nostics: 1.3.05019 nostics: 1.3.0
4978- nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1)5020+ nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue-tsc@3.3.13(typescript@5.9.3))(yaml@2.9.1)
4979 ohash: 2.0.125021 ohash: 2.0.12
4980 pathe: 2.0.35022 pathe: 2.0.3
4981 radix3: 1.1.25023 radix3: 1.1.2
@@ -5191,7 +5233,7 @@ snapshots:
5191 - vue5233 - vue
5192 - webpack5234 - webpack
5193 5235
5194- '@nuxt/vite-builder@4.6.2(657d436dea0451b72d4de16847ab6ff5)':5236+ '@nuxt/vite-builder@4.6.2(cba4483e2f4589592cec2b2715beb36b)':
5195 dependencies:5237 dependencies:
5196 '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))5238 '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))
5197 '@vitejs/plugin-vue': 6.0.10(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3))5239 '@vitejs/plugin-vue': 6.0.10(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3))
@@ -5209,7 +5251,7 @@ snapshots:
5209 knitwork: 1.3.05251 knitwork: 1.3.0
5210 mocked-exports: 0.1.15252 mocked-exports: 0.1.1
5211 my-bad: 0.2.11(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))5253 my-bad: 0.2.11(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))
5212- nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1)5254+ nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue-tsc@3.3.13(typescript@5.9.3))(yaml@2.9.1)
5213 pathe: 2.0.35255 pathe: 2.0.3
5214 pkg-types: 2.4.05256 pkg-types: 2.4.0
5215 postcss: 8.5.315257 postcss: 8.5.31
@@ -5220,7 +5262,7 @@ snapshots:
5220 unenv: 2.0.0-rc.245262 unenv: 2.0.0-rc.24
5221 vite: 8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)5263 vite: 8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)
5222 vite-node: 6.0.0(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)5264 vite-node: 6.0.0(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)
5223- vite-plugin-checker: 0.14.5(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))5265+ vite-plugin-checker: 0.14.5(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue-tsc@3.3.13(typescript@5.9.3))
5224 vue: 3.5.44(typescript@5.9.3)5266 vue: 3.5.44(typescript@5.9.3)
5225 vue-bundle-renderer: 2.4.05267 vue-bundle-renderer: 2.4.0
5226 optionalDependencies:5268 optionalDependencies:
@@ -5258,7 +5300,7 @@ snapshots:
5258 - vue-tsc5300 - vue-tsc
5259 - yaml5301 - yaml
5260 5302
5261- '@nuxt/vite-server@4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1))(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))':5303+ '@nuxt/vite-server@4.6.2(ee8202584928b0f003c6ca97f6e69333)':
5262 dependencies:5304 dependencies:
5263 '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))5305 '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))
5264 clickable-path: 0.1.15306 clickable-path: 0.1.1
@@ -5266,7 +5308,7 @@ snapshots:
5266 exsolve: 1.1.35308 exsolve: 1.1.3
5267 mocked-exports: 0.1.15309 mocked-exports: 0.1.1
5268 my-bad: 0.2.11(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))5310 my-bad: 0.2.11(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))
5269- nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1)5311+ nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue-tsc@3.3.13(typescript@5.9.3))(yaml@2.9.1)
5270 pathe: 2.0.35312 pathe: 2.0.3
5271 rou3: 0.9.25313 rou3: 0.9.2
5272 srvx: 1.0.55314 srvx: 1.0.5
@@ -5984,6 +6026,18 @@ snapshots:
5984 convert-source-map: 2.0.06026 convert-source-map: 2.0.0
5985 tinyrainbow: 3.2.06027 tinyrainbow: 3.2.0
5986 6028
6029+ '@volar/language-core@2.4.28':
6030+ dependencies:
6031+ '@volar/source-map': 2.4.28
6032+
6033+ '@volar/source-map@2.4.28': {}
6034+
6035+ '@volar/typescript@2.4.28':
6036+ dependencies:
6037+ '@volar/language-core': 2.4.28
6038+ path-browserify: 1.0.1
6039+ vscode-uri: 3.2.0
6040+
5987 '@vue-macros/common@3.1.4(vue@3.5.44(typescript@5.9.3))':6041 '@vue-macros/common@3.1.4(vue@3.5.44(typescript@5.9.3))':
5988 dependencies:6042 dependencies:
5989 '@vue/compiler-sfc': 3.5.446043 '@vue/compiler-sfc': 3.5.44
@@ -6074,6 +6128,16 @@ snapshots:
6074 6128
6075 '@vue/devtools-shared@8.2.1': {}6129 '@vue/devtools-shared@8.2.1': {}
6076 6130
6131+ '@vue/language-core@3.3.13':
6132+ dependencies:
6133+ '@volar/language-core': 2.4.28
6134+ '@vue/compiler-dom': 3.5.44
6135+ '@vue/shared': 3.5.44
6136+ alien-signals: 3.2.1
6137+ muggle-string: 0.4.1
6138+ path-browserify: 1.0.1
6139+ picomatch: 4.0.7
6140+
6077 '@vue/reactivity@3.5.44':6141 '@vue/reactivity@3.5.44':
6078 dependencies:6142 dependencies:
6079 '@vue/shared': 3.5.446143 '@vue/shared': 3.5.44
@@ -6152,6 +6216,8 @@ snapshots:
6152 6216
6153 agent-base@7.1.4: {}6217 agent-base@7.1.4: {}
6154 6218
6219+ alien-signals@3.2.1: {}
6220+
6155 ansi-regex@5.0.1: {}6221 ansi-regex@5.0.1: {}
6156 6222
6157 ansi-regex@6.4.0: {}6223 ansi-regex@6.4.0: {}
@@ -7197,6 +7263,8 @@ snapshots:
7197 7263
7198 marked@17.0.6: {}7264 marked@17.0.6: {}
7199 7265
7266+ marked@18.1.1: {}
7267+
7200 mdn-data@2.27.1: {}7268 mdn-data@2.27.1: {}
7201 7269
7202 merge-stream@2.0.0: {}7270 merge-stream@2.0.0: {}
@@ -7427,7 +7495,7 @@ snapshots:
7427 path-key: 4.0.07495 path-key: 4.0.0
7428 unicorn-magic: 0.3.07496 unicorn-magic: 0.3.0
7429 7497
7430- nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1):7498+ nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue-tsc@3.3.13(typescript@5.9.3))(yaml@2.9.1):
7431 dependencies:7499 dependencies:
7432 '@dxup/nuxt': 0.5.10(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(jiti@2.7.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))7500 '@dxup/nuxt': 0.5.10(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(jiti@2.7.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))
7433 '@jridgewell/trace-mapping': 0.3.317501 '@jridgewell/trace-mapping': 0.3.31
@@ -7435,11 +7503,11 @@ snapshots:
7435 '@nuxt/devalue': 2.0.27503 '@nuxt/devalue': 2.0.2
7436 '@nuxt/devtools': 3.4.4(@nuxt/schema@4.6.2)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3))7504 '@nuxt/devtools': 3.4.4(@nuxt/schema@4.6.2)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3))
7437 '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))7505 '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))
7438- '@nuxt/nitro-server': 4.6.2(2361615bb0aa1e79f3cfd32ae4f54216)7506+ '@nuxt/nitro-server': 4.6.2(e1cbd802f5c636043c2b5a4595875576)
7439 '@nuxt/schema': 4.6.27507 '@nuxt/schema': 4.6.2
7440 '@nuxt/telemetry': 2.9.1(@nuxt/kit@4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))))7508 '@nuxt/telemetry': 2.9.1(@nuxt/kit@4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))))
7441- '@nuxt/vite-builder': 4.6.2(657d436dea0451b72d4de16847ab6ff5)7509+ '@nuxt/vite-builder': 4.6.2(cba4483e2f4589592cec2b2715beb36b)
7442- '@nuxt/vite-server': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.155.0)(@types/node@22.20.5)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.15)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.15)(rollup@4.65.0))(rollup@4.65.0)(srvx@1.0.6)(terser@5.51.2)(tsx@4.23.15)(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1))(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))7510+ '@nuxt/vite-server': 4.6.2(ee8202584928b0f003c6ca97f6e69333)
7443 '@unhead/vue': 3.4.3(@oxc-project/types@0.155.0)(esbuild@0.28.2)(lightningcss@1.33.0)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3))7511 '@unhead/vue': 3.4.3(@oxc-project/types@0.155.0)(esbuild@0.28.2)(lightningcss@1.33.0)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3))
7444 '@vue/shared': 3.5.447512 '@vue/shared': 3.5.44
7445 chokidar: 5.0.07513 chokidar: 5.0.0
@@ -7626,6 +7694,8 @@ snapshots:
7626 7694
7627 parseurl@1.3.3: {}7695 parseurl@1.3.3: {}
7628 7696
7697+ path-browserify@1.0.1: {}
7698+
7629 path-key@3.1.1: {}7699 path-key@3.1.1: {}
7630 7700
7631 path-key@4.0.0: {}7701 path-key@4.0.0: {}
@@ -8455,7 +8525,7 @@ snapshots:
8455 - tsx8525 - tsx
8456 - yaml8526 - yaml
8457 8527
8458- vite-plugin-checker@0.14.5(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)):8528+ vite-plugin-checker@0.14.5(typescript@5.9.3)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue-tsc@3.3.13(typescript@5.9.3)):
8459 dependencies:8529 dependencies:
8460 '@babel/code-frame': 7.29.78530 '@babel/code-frame': 7.29.7
8461 chokidar: 5.0.08531 chokidar: 5.0.0
@@ -8467,6 +8537,7 @@ snapshots:
8467 vite: 8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)8537 vite: 8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)
8468 optionalDependencies:8538 optionalDependencies:
8469 typescript: 5.9.38539 typescript: 5.9.3
8540+ vue-tsc: 3.3.13(typescript@5.9.3)
8470 8541
8471 vite-plugin-inspect@11.4.1(@nuxt/kit@4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))))(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)):8542 vite-plugin-inspect@11.4.1(@nuxt/kit@4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.15)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.15)(rollup@4.65.0)(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))))(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)):
8472 dependencies:8543 dependencies:
@@ -8536,6 +8607,8 @@ snapshots:
8536 transitivePeerDependencies:8607 transitivePeerDependencies:
8537 - msw8608 - msw
8538 8609
8610+ vscode-uri@3.2.0: {}
8611+
8539 vue-bundle-renderer@2.4.0:8612 vue-bundle-renderer@2.4.0:
8540 dependencies:8613 dependencies:
8541 ufo: 1.6.48614 ufo: 1.6.4
@@ -8586,6 +8659,12 @@ snapshots:
8586 - unloader8659 - unloader
8587 - webpack8660 - webpack
8588 8661
8662+ vue-tsc@3.3.13(typescript@5.9.3):
8663+ dependencies:
8664+ '@volar/typescript': 2.4.28
8665+ '@vue/language-core': 3.3.13
8666+ typescript: 5.9.3
8667+
8589 vue@3.5.44(typescript@5.9.3):8668 vue@3.5.44(typescript@5.9.3):
8590 dependencies:8669 dependencies:
8591 '@vue/compiler-dom': 3.5.448670 '@vue/compiler-dom': 3.5.44