feat(ui): read-only inventory UI (dashboard, scope view, item details, add project)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') | |
| 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') | ||
| 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 @@ | ||
| 12 | 12 | "@iconify-json/lucide": "^1.2.0", |
| 13 | 13 | "@nuxt/ui": "^4.12.0", |
| 14 | 14 | "@skilly/core": "workspace:*", |
| 15 | + "marked": "^18.1.1", | |
| 15 | 16 | "nuxt": "^4.6.0", |
| 16 | 17 | "tailwindcss": "^4.1.0", |
| 17 | 18 | "vue": "^3.5.0", |
| 18 | 19 | "vue-router": "^4.5.0" |
| 20 | + }, | |
| 21 | + "devDependencies": { | |
| 22 | + "typescript": "~5.9.3", | |
| 23 | + "vue-tsc": "^3.3.13" | |
| 19 | 24 | } |
| 20 | 25 | } |
| @@ -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: | ||
| 65 | 65 | '@skilly/core': |
| 66 | 66 | specifier: workspace:* |
| 67 | 67 | version: link:../core |
| 68 | + marked: | |
| 69 | + specifier: ^18.1.1 | |
| 70 | + version: 18.1.1 | |
| 68 | 71 | nuxt: |
| 69 | 72 | 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 | 74 | tailwindcss: |
| 72 | 75 | specifier: ^4.1.0 |
| 73 | 76 | version: 4.3.3 |
| @@ -77,6 +80,13 @@ importers: | ||
| 77 | 80 | vue-router: |
| 78 | 81 | specifier: ^4.5.0 |
| 79 | 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 | 91 | packages: |
| 82 | 92 | |
| @@ -1631,6 +1641,15 @@ packages: | ||
| 1631 | 1641 | '@vitest/utils@4.1.12': |
| 1632 | 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 | 1653 | '@vue-macros/common@3.1.4': |
| 1635 | 1654 | resolution: {integrity: sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==} |
| 1636 | 1655 | engines: {node: '>=20.19.0'} |
| @@ -1685,6 +1704,9 @@ packages: | ||
| 1685 | 1704 | '@vue/devtools-shared@8.2.1': |
| 1686 | 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 | 1710 | '@vue/reactivity@3.5.44': |
| 1689 | 1711 | resolution: {integrity: sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw==} |
| 1690 | 1712 | |
| @@ -1786,6 +1808,9 @@ packages: | ||
| 1786 | 1808 | resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} |
| 1787 | 1809 | engines: {node: '>= 14'} |
| 1788 | 1810 | |
| 1811 | + alien-signals@3.2.1: | |
| 1812 | + resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} | |
| 1813 | + | |
| 1789 | 1814 | ansi-regex@5.0.1: |
| 1790 | 1815 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} |
| 1791 | 1816 | engines: {node: '>=8'} |
| @@ -2894,6 +2919,11 @@ packages: | ||
| 2894 | 2919 | engines: {node: '>= 20'} |
| 2895 | 2920 | 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 | 2927 | mdn-data@2.27.1: |
| 2898 | 2928 | resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} |
| 2899 | 2929 | |
| @@ -3124,6 +3154,9 @@ packages: | ||
| 3124 | 3154 | resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} |
| 3125 | 3155 | engines: {node: '>= 0.8'} |
| 3126 | 3156 | |
| 3157 | + path-browserify@1.0.1: | |
| 3158 | + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} | |
| 3159 | + | |
| 3127 | 3160 | path-key@3.1.1: |
| 3128 | 3161 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} |
| 3129 | 3162 | engines: {node: '>=8'} |
| @@ -4037,6 +4070,9 @@ packages: | ||
| 4037 | 4070 | jsdom: |
| 4038 | 4071 | optional: true |
| 4039 | 4072 | |
| 4073 | + vscode-uri@3.2.0: | |
| 4074 | + resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==} | |
| 4075 | + | |
| 4040 | 4076 | vue-bundle-renderer@2.4.0: |
| 4041 | 4077 | resolution: {integrity: sha512-IxHopQZgzTGBiuGERrQrAS7Lr3dCv+qxvgirEgCUFGAD4gYNfiaGYzC1DDojeZmJAmehp9gG8+17PGuVP6Ab2Q==} |
| 4042 | 4078 | |
| @@ -4080,6 +4116,12 @@ packages: | ||
| 4080 | 4116 | vite: |
| 4081 | 4117 | 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 | 4125 | vue@3.5.44: |
| 4084 | 4126 | resolution: {integrity: sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ==} |
| 4085 | 4127 | peerDependencies: |
| @@ -4956,7 +4998,7 @@ snapshots: | ||
| 4956 | 4998 | - magic-string |
| 4957 | 4999 | - unplugin |
| 4958 | 5000 | |
| 4959 | - '@nuxt/nitro-server@4.6.2(2361615bb0aa1e79f3cfd32ae4f54216)': | |
| 5001 | + '@nuxt/nitro-server@4.6.2(e1cbd802f5c636043c2b5a4595875576)': | |
| 4960 | 5002 | dependencies: |
| 4961 | 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 | 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 | 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 | 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 | 5019 | 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 | 5021 | ohash: 2.0.12 |
| 4980 | 5022 | pathe: 2.0.3 |
| 4981 | 5023 | radix3: 1.1.2 |
| @@ -5191,7 +5233,7 @@ snapshots: | ||
| 5191 | 5233 | - vue |
| 5192 | 5234 | - webpack |
| 5193 | 5235 | |
| 5194 | - '@nuxt/vite-builder@4.6.2(657d436dea0451b72d4de16847ab6ff5)': | |
| 5236 | + '@nuxt/vite-builder@4.6.2(cba4483e2f4589592cec2b2715beb36b)': | |
| 5195 | 5237 | dependencies: |
| 5196 | 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 | 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 | 5251 | knitwork: 1.3.0 |
| 5210 | 5252 | mocked-exports: 0.1.1 |
| 5211 | 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 | 5255 | pathe: 2.0.3 |
| 5214 | 5256 | pkg-types: 2.4.0 |
| 5215 | 5257 | postcss: 8.5.31 |
| @@ -5220,7 +5262,7 @@ snapshots: | ||
| 5220 | 5262 | unenv: 2.0.0-rc.24 |
| 5221 | 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 | 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 | 5266 | vue: 3.5.44(typescript@5.9.3) |
| 5225 | 5267 | vue-bundle-renderer: 2.4.0 |
| 5226 | 5268 | optionalDependencies: |
| @@ -5258,7 +5300,7 @@ snapshots: | ||
| 5258 | 5300 | - vue-tsc |
| 5259 | 5301 | - 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 | 5304 | dependencies: |
| 5263 | 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 | 5306 | clickable-path: 0.1.1 |
| @@ -5266,7 +5308,7 @@ snapshots: | ||
| 5266 | 5308 | exsolve: 1.1.3 |
| 5267 | 5309 | mocked-exports: 0.1.1 |
| 5268 | 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 | 5312 | pathe: 2.0.3 |
| 5271 | 5313 | rou3: 0.9.2 |
| 5272 | 5314 | srvx: 1.0.5 |
| @@ -5984,6 +6026,18 @@ snapshots: | ||
| 5984 | 6026 | convert-source-map: 2.0.0 |
| 5985 | 6027 | 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 | 6041 | '@vue-macros/common@3.1.4(vue@3.5.44(typescript@5.9.3))': |
| 5988 | 6042 | dependencies: |
| 5989 | 6043 | '@vue/compiler-sfc': 3.5.44 |
| @@ -6074,6 +6128,16 @@ snapshots: | ||
| 6074 | 6128 | |
| 6075 | 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 | 6141 | '@vue/reactivity@3.5.44': |
| 6078 | 6142 | dependencies: |
| 6079 | 6143 | '@vue/shared': 3.5.44 |
| @@ -6152,6 +6216,8 @@ snapshots: | ||
| 6152 | 6216 | |
| 6153 | 6217 | agent-base@7.1.4: {} |
| 6154 | 6218 | |
| 6219 | + alien-signals@3.2.1: {} | |
| 6220 | + | |
| 6155 | 6221 | ansi-regex@5.0.1: {} |
| 6156 | 6222 | |
| 6157 | 6223 | ansi-regex@6.4.0: {} |
| @@ -7197,6 +7263,8 @@ snapshots: | ||
| 7197 | 7263 | |
| 7198 | 7264 | marked@17.0.6: {} |
| 7199 | 7265 | |
| 7266 | + marked@18.1.1: {} | |
| 7267 | + | |
| 7200 | 7268 | mdn-data@2.27.1: {} |
| 7201 | 7269 | |
| 7202 | 7270 | merge-stream@2.0.0: {} |
| @@ -7427,7 +7495,7 @@ snapshots: | ||
| 7427 | 7495 | path-key: 4.0.0 |
| 7428 | 7496 | 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 | 7499 | dependencies: |
| 7432 | 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 | 7501 | '@jridgewell/trace-mapping': 0.3.31 |
| @@ -7435,11 +7503,11 @@ snapshots: | ||
| 7435 | 7503 | '@nuxt/devalue': 2.0.2 |
| 7436 | 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 | 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 | 7507 | '@nuxt/schema': 4.6.2 |
| 7440 | 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) | |
| 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) | |
| 7443 | 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 | 7512 | '@vue/shared': 3.5.44 |
| 7445 | 7513 | chokidar: 5.0.0 |
| @@ -7626,6 +7694,8 @@ snapshots: | ||
| 7626 | 7694 | |
| 7627 | 7695 | parseurl@1.3.3: {} |
| 7628 | 7696 | |
| 7697 | + path-browserify@1.0.1: {} | |
| 7698 | + | |
| 7629 | 7699 | path-key@3.1.1: {} |
| 7630 | 7700 | |
| 7631 | 7701 | path-key@4.0.0: {} |
| @@ -8455,7 +8525,7 @@ snapshots: | ||
| 8455 | 8525 | - tsx |
| 8456 | 8526 | - 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 | 8529 | dependencies: |
| 8460 | 8530 | '@babel/code-frame': 7.29.7 |
| 8461 | 8531 | chokidar: 5.0.0 |
| @@ -8467,6 +8537,7 @@ snapshots: | ||
| 8467 | 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 | 8538 | optionalDependencies: |
| 8469 | 8539 | typescript: 5.9.3 |
| 8540 | + vue-tsc: 3.3.13(typescript@5.9.3) | |
| 8470 | 8541 | |
| 8471 | 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 | 8543 | dependencies: |
| @@ -8536,6 +8607,8 @@ snapshots: | ||
| 8536 | 8607 | transitivePeerDependencies: |
| 8537 | 8608 | - msw |
| 8538 | 8609 | |
| 8610 | + vscode-uri@3.2.0: {} | |
| 8611 | + | |
| 8539 | 8612 | vue-bundle-renderer@2.4.0: |
| 8540 | 8613 | dependencies: |
| 8541 | 8614 | ufo: 1.6.4 |
| @@ -8586,6 +8659,12 @@ snapshots: | ||
| 8586 | 8659 | - unloader |
| 8587 | 8660 | - 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 | 8668 | vue@3.5.44(typescript@5.9.3): |
| 8590 | 8669 | dependencies: |
| 8591 | 8670 | '@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:../core | 67 | version: link:../core |
| 68 | + marked: | ||
| 69 | + specifier: ^18.1.1 | ||
| 70 | + version: 18.1.1 | ||
| 68 | nuxt: | 71 | nuxt: |
| 69 | specifier: ^4.6.0 | 72 | 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.0 | 75 | specifier: ^4.1.0 |
| 73 | version: 4.3.3 | 76 | version: 4.3.3 |
| @@ -77,6 +80,13 @@ importers: | |||
| 77 | vue-router: | 80 | vue-router: |
| 78 | specifier: ^4.5.0 | 81 | 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: true | 2920 | 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: true | 4071 | 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: true | 4117 | 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-string | 4998 | - magic-string |
| 4957 | - unplugin | 4999 | - 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.0 | 5019 | 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.12 | 5021 | ohash: 2.0.12 |
| 4980 | pathe: 2.0.3 | 5022 | pathe: 2.0.3 |
| 4981 | radix3: 1.1.2 | 5023 | radix3: 1.1.2 |
| @@ -5191,7 +5233,7 @@ snapshots: | |||
| 5191 | - vue | 5233 | - vue |
| 5192 | - webpack | 5234 | - 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.0 | 5251 | knitwork: 1.3.0 |
| 5210 | mocked-exports: 0.1.1 | 5252 | 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.3 | 5255 | pathe: 2.0.3 |
| 5214 | pkg-types: 2.4.0 | 5256 | pkg-types: 2.4.0 |
| 5215 | postcss: 8.5.31 | 5257 | postcss: 8.5.31 |
| @@ -5220,7 +5262,7 @@ snapshots: | |||
| 5220 | unenv: 2.0.0-rc.24 | 5262 | 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.0 | 5267 | vue-bundle-renderer: 2.4.0 |
| 5226 | optionalDependencies: | 5268 | optionalDependencies: |
| @@ -5258,7 +5300,7 @@ snapshots: | |||
| 5258 | - vue-tsc | 5300 | - vue-tsc |
| 5259 | - yaml | 5301 | - 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.1 | 5306 | clickable-path: 0.1.1 |
| @@ -5266,7 +5308,7 @@ snapshots: | |||
| 5266 | exsolve: 1.1.3 | 5308 | exsolve: 1.1.3 |
| 5267 | mocked-exports: 0.1.1 | 5309 | 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.3 | 5312 | pathe: 2.0.3 |
| 5271 | rou3: 0.9.2 | 5313 | rou3: 0.9.2 |
| 5272 | srvx: 1.0.5 | 5314 | srvx: 1.0.5 |
| @@ -5984,6 +6026,18 @@ snapshots: | |||
| 5984 | convert-source-map: 2.0.0 | 6026 | convert-source-map: 2.0.0 |
| 5985 | tinyrainbow: 3.2.0 | 6027 | 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.44 | 6043 | '@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.44 | 6143 | '@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.0 | 7495 | path-key: 4.0.0 |
| 7428 | unicorn-magic: 0.3.0 | 7496 | 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.31 | 7501 | '@jridgewell/trace-mapping': 0.3.31 |
| @@ -7435,11 +7503,11 @@ snapshots: | |||
| 7435 | '@nuxt/devalue': 2.0.2 | 7503 | '@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.2 | 7507 | '@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.44 | 7512 | '@vue/shared': 3.5.44 |
| 7445 | chokidar: 5.0.0 | 7513 | 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 | - tsx | 8525 | - tsx |
| 8456 | - yaml | 8526 | - 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.7 | 8530 | '@babel/code-frame': 7.29.7 |
| 8461 | chokidar: 5.0.0 | 8531 | 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.3 | 8539 | 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 | - msw | 8608 | - 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.4 | 8614 | ufo: 1.6.4 |
| @@ -8586,6 +8659,12 @@ snapshots: | |||
| 8586 | - unloader | 8659 | - unloader |
| 8587 | - webpack | 8660 | - 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.44 | 8670 | '@vue/compiler-dom': 3.5.44 |