cognova 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Claude/CLAUDE.md +9 -4
- package/Claude/skills/environment/SKILL.md +6 -0
- package/Claude/skills/memory/SKILL.md +6 -0
- package/Claude/skills/project/SKILL.md +6 -0
- package/Claude/skills/secret/SKILL.md +85 -0
- package/Claude/skills/secret/secret.py +146 -0
- package/Claude/skills/skill-creator/SKILL.md +30 -0
- package/Claude/skills/task/SKILL.md +6 -0
- package/app/components/skills/Card.vue +82 -0
- package/app/components/skills/CreateModal.vue +156 -0
- package/app/components/skills/Editor.vue +135 -0
- package/app/components/skills/FileTree.vue +336 -0
- package/app/components/skills/LibraryCard.vue +122 -0
- package/app/components/skills/RenameModal.vue +84 -0
- package/app/layouts/dashboard.vue +7 -0
- package/app/pages/skills/[name].vue +198 -0
- package/app/pages/skills/index.vue +157 -0
- package/app/pages/skills/library.vue +209 -0
- package/dist/cli/index.js +23 -23
- package/nuxt.config.ts +9 -0
- package/package.json +1 -1
- package/server/api/skills/[name]/files/create.post.ts +45 -0
- package/server/api/skills/[name]/files/delete.post.ts +45 -0
- package/server/api/skills/[name]/files/index.get.ts +28 -0
- package/server/api/skills/[name]/files/read.post.ts +41 -0
- package/server/api/skills/[name]/files/write.post.ts +42 -0
- package/server/api/skills/[name]/index.get.ts +54 -0
- package/server/api/skills/[name]/rename.post.ts +64 -0
- package/server/api/skills/[name]/toggle.post.ts +32 -0
- package/server/api/skills/create.post.ts +51 -0
- package/server/api/skills/generate.post.ts +126 -0
- package/server/api/skills/index.get.ts +57 -0
- package/server/api/skills/library/check-updates.get.ts +46 -0
- package/server/api/skills/library/index.get.ts +56 -0
- package/server/api/skills/library/install.post.ts +73 -0
- package/server/db/schema.ts +17 -0
- package/server/drizzle/migrations/0012_good_deadpool.sql +12 -0
- package/server/drizzle/migrations/0013_swift_snowbird.sql +1 -0
- package/server/drizzle/migrations/meta/0012_snapshot.json +1713 -0
- package/server/drizzle/migrations/meta/0013_snapshot.json +1720 -0
- package/server/drizzle/migrations/meta/_journal.json +14 -0
- package/server/middleware/auth.ts +0 -1
- package/server/plugins/05.skills-catalog.ts +105 -0
- package/server/utils/skills-path.ts +197 -0
- package/shared/types/index.ts +63 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CodeLanguage } from '~~/shared/types'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
skillName: string
|
|
6
|
+
filePath: string
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const emit = defineEmits<{
|
|
10
|
+
saved: []
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
const toast = useToast()
|
|
14
|
+
const content = ref('')
|
|
15
|
+
const originalContent = ref('')
|
|
16
|
+
const loading = ref(true)
|
|
17
|
+
const saving = ref(false)
|
|
18
|
+
|
|
19
|
+
const isDirty = computed(() => content.value !== originalContent.value)
|
|
20
|
+
|
|
21
|
+
const language = computed<CodeLanguage>(() => {
|
|
22
|
+
const ext = props.filePath.split('.').pop()?.toLowerCase()
|
|
23
|
+
switch (ext) {
|
|
24
|
+
case 'py': return 'python'
|
|
25
|
+
case 'md': return 'markdown'
|
|
26
|
+
case 'json': return 'json'
|
|
27
|
+
case 'ts': return 'typescript'
|
|
28
|
+
case 'js': return 'javascript'
|
|
29
|
+
case 'yaml':
|
|
30
|
+
case 'yml': return 'yaml'
|
|
31
|
+
case 'sh':
|
|
32
|
+
case 'bash': return 'bash'
|
|
33
|
+
default: return 'plaintext'
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
async function loadFile() {
|
|
38
|
+
loading.value = true
|
|
39
|
+
try {
|
|
40
|
+
const res = await $fetch<{ data: { content: string } }>(`/api/skills/${props.skillName}/files/read`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
body: { path: props.filePath }
|
|
43
|
+
})
|
|
44
|
+
content.value = res.data.content
|
|
45
|
+
originalContent.value = res.data.content
|
|
46
|
+
} catch {
|
|
47
|
+
toast.add({ title: 'Failed to load file', color: 'error' })
|
|
48
|
+
} finally {
|
|
49
|
+
loading.value = false
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function save() {
|
|
54
|
+
saving.value = true
|
|
55
|
+
try {
|
|
56
|
+
await $fetch(`/api/skills/${props.skillName}/files/write`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
body: { path: props.filePath, content: content.value }
|
|
59
|
+
})
|
|
60
|
+
originalContent.value = content.value
|
|
61
|
+
toast.add({ title: 'File saved', color: 'success' })
|
|
62
|
+
emit('saved')
|
|
63
|
+
} catch {
|
|
64
|
+
toast.add({ title: 'Failed to save file', color: 'error' })
|
|
65
|
+
} finally {
|
|
66
|
+
saving.value = false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Ctrl+S / Cmd+S keyboard shortcut
|
|
71
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
72
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
|
73
|
+
e.preventDefault()
|
|
74
|
+
if (isDirty.value) save()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
onMounted(() => {
|
|
79
|
+
loadFile()
|
|
80
|
+
window.addEventListener('keydown', handleKeydown)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
onUnmounted(() => {
|
|
84
|
+
window.removeEventListener('keydown', handleKeydown)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
watch(() => props.filePath, () => loadFile())
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<template>
|
|
91
|
+
<div class="flex flex-col h-full">
|
|
92
|
+
<div class="flex items-center justify-between px-4 py-2 border-b border-default">
|
|
93
|
+
<div class="flex items-center gap-2 text-sm text-muted">
|
|
94
|
+
<UIcon
|
|
95
|
+
name="i-lucide-file-code"
|
|
96
|
+
class="size-4"
|
|
97
|
+
/>
|
|
98
|
+
<span>{{ filePath }}</span>
|
|
99
|
+
<UBadge
|
|
100
|
+
v-if="isDirty"
|
|
101
|
+
variant="soft"
|
|
102
|
+
color="warning"
|
|
103
|
+
size="xs"
|
|
104
|
+
>
|
|
105
|
+
Modified
|
|
106
|
+
</UBadge>
|
|
107
|
+
</div>
|
|
108
|
+
<UButton
|
|
109
|
+
size="xs"
|
|
110
|
+
:loading="saving"
|
|
111
|
+
:disabled="!isDirty"
|
|
112
|
+
@click="save"
|
|
113
|
+
>
|
|
114
|
+
Save
|
|
115
|
+
</UButton>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div
|
|
119
|
+
v-if="loading"
|
|
120
|
+
class="flex-1 flex items-center justify-center"
|
|
121
|
+
>
|
|
122
|
+
<UIcon
|
|
123
|
+
name="i-lucide-loader-2"
|
|
124
|
+
class="size-6 animate-spin text-dimmed"
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<EditorCodeEditor
|
|
129
|
+
v-else
|
|
130
|
+
v-model="content"
|
|
131
|
+
:language="language"
|
|
132
|
+
class="flex-1"
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
</template>
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TreeItem } from '~/composables/useFileTree'
|
|
3
|
+
import type { SkillFile } from '~~/shared/types'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
skillName: string
|
|
7
|
+
files: SkillFile[]
|
|
8
|
+
selectedPath?: string
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const emit = defineEmits<{
|
|
12
|
+
select: [path: string]
|
|
13
|
+
refresh: []
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const toast = useToast()
|
|
17
|
+
|
|
18
|
+
// Convert SkillFile[] to TreeItem[] for UTree
|
|
19
|
+
function toTreeItems(files: SkillFile[], parentPath = ''): TreeItem[] {
|
|
20
|
+
return files.map((f) => {
|
|
21
|
+
const fullPath = parentPath ? `${parentPath}/${f.path}` : f.path
|
|
22
|
+
const item: TreeItem = {
|
|
23
|
+
id: fullPath,
|
|
24
|
+
label: f.name,
|
|
25
|
+
path: fullPath,
|
|
26
|
+
type: f.type,
|
|
27
|
+
defaultExpanded: true
|
|
28
|
+
}
|
|
29
|
+
if (f.type === 'directory') {
|
|
30
|
+
item.children = f.children ? toTreeItems(f.children, fullPath) : []
|
|
31
|
+
} else {
|
|
32
|
+
if (f.name.endsWith('.py')) item.icon = 'i-lucide-file-code'
|
|
33
|
+
else if (f.name.endsWith('.md')) item.icon = 'i-lucide-file-text'
|
|
34
|
+
else if (f.name.endsWith('.json')) item.icon = 'i-lucide-file-json'
|
|
35
|
+
else item.icon = 'i-lucide-file'
|
|
36
|
+
}
|
|
37
|
+
return item
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const treeItems = computed(() => toTreeItems(props.files))
|
|
42
|
+
|
|
43
|
+
const contextMenuTarget = ref<TreeItem | null>(null)
|
|
44
|
+
const showNewFileModal = ref(false)
|
|
45
|
+
const showNewFolderModal = ref(false)
|
|
46
|
+
const showDeleteConfirm = ref(false)
|
|
47
|
+
const newItemName = ref('')
|
|
48
|
+
|
|
49
|
+
const contextMenuItems = computed(() => [
|
|
50
|
+
[{
|
|
51
|
+
label: 'New File',
|
|
52
|
+
icon: 'i-lucide-file-plus',
|
|
53
|
+
onSelect: () => {
|
|
54
|
+
newItemName.value = ''
|
|
55
|
+
showNewFileModal.value = true
|
|
56
|
+
}
|
|
57
|
+
}, {
|
|
58
|
+
label: 'New Folder',
|
|
59
|
+
icon: 'i-lucide-folder-plus',
|
|
60
|
+
onSelect: () => {
|
|
61
|
+
newItemName.value = ''
|
|
62
|
+
showNewFolderModal.value = true
|
|
63
|
+
}
|
|
64
|
+
}],
|
|
65
|
+
[{
|
|
66
|
+
label: 'Delete',
|
|
67
|
+
icon: 'i-lucide-trash-2',
|
|
68
|
+
color: 'error' as const,
|
|
69
|
+
onSelect: () => {
|
|
70
|
+
if (contextMenuTarget.value && contextMenuTarget.value.label !== 'SKILL.md')
|
|
71
|
+
showDeleteConfirm.value = true
|
|
72
|
+
}
|
|
73
|
+
}]
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
function handleSelect(_e: unknown, item: TreeItem) {
|
|
77
|
+
if (item.type === 'file') {
|
|
78
|
+
emit('select', item.path)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function handleCreateFile() {
|
|
83
|
+
if (!newItemName.value.trim()) return
|
|
84
|
+
|
|
85
|
+
let filename = newItemName.value.trim()
|
|
86
|
+
if (!filename.includes('.')) filename += '.py'
|
|
87
|
+
|
|
88
|
+
const parentPath = contextMenuTarget.value?.type === 'directory'
|
|
89
|
+
? contextMenuTarget.value.path
|
|
90
|
+
: ''
|
|
91
|
+
|
|
92
|
+
const filePath = parentPath ? `${parentPath}/${filename}` : filename
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
await $fetch(`/api/skills/${props.skillName}/files/create`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
body: { path: filePath, type: 'file' }
|
|
98
|
+
})
|
|
99
|
+
emit('refresh')
|
|
100
|
+
emit('select', filePath)
|
|
101
|
+
toast.add({ title: `Created ${filename}`, color: 'success' })
|
|
102
|
+
} catch {
|
|
103
|
+
toast.add({ title: 'Failed to create file', color: 'error' })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
showNewFileModal.value = false
|
|
107
|
+
newItemName.value = ''
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function handleCreateFolder() {
|
|
111
|
+
if (!newItemName.value.trim()) return
|
|
112
|
+
|
|
113
|
+
const folderName = newItemName.value.trim()
|
|
114
|
+
const parentPath = contextMenuTarget.value?.type === 'directory'
|
|
115
|
+
? contextMenuTarget.value.path
|
|
116
|
+
: ''
|
|
117
|
+
|
|
118
|
+
const folderPath = parentPath ? `${parentPath}/${folderName}` : folderName
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await $fetch(`/api/skills/${props.skillName}/files/create`, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
body: { path: folderPath, type: 'directory' }
|
|
124
|
+
})
|
|
125
|
+
emit('refresh')
|
|
126
|
+
toast.add({ title: `Created ${folderName}/`, color: 'success' })
|
|
127
|
+
} catch {
|
|
128
|
+
toast.add({ title: 'Failed to create folder', color: 'error' })
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
showNewFolderModal.value = false
|
|
132
|
+
newItemName.value = ''
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function handleDelete() {
|
|
136
|
+
if (!contextMenuTarget.value) return
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await $fetch(`/api/skills/${props.skillName}/files/delete`, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
body: { path: contextMenuTarget.value.path }
|
|
142
|
+
})
|
|
143
|
+
emit('refresh')
|
|
144
|
+
toast.add({ title: `Deleted ${contextMenuTarget.value.label}`, color: 'success' })
|
|
145
|
+
} catch {
|
|
146
|
+
toast.add({ title: 'Failed to delete', color: 'error' })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
showDeleteConfirm.value = false
|
|
150
|
+
}
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<template>
|
|
154
|
+
<div class="h-full flex flex-col">
|
|
155
|
+
<UContextMenu
|
|
156
|
+
:items="contextMenuItems"
|
|
157
|
+
>
|
|
158
|
+
<div
|
|
159
|
+
class="flex-1 h-full overflow-auto p-2"
|
|
160
|
+
@contextmenu.self="contextMenuTarget = null"
|
|
161
|
+
>
|
|
162
|
+
<div
|
|
163
|
+
v-if="files.length === 0"
|
|
164
|
+
class="h-full min-h-32 flex flex-col items-center justify-center text-dimmed text-sm"
|
|
165
|
+
>
|
|
166
|
+
<UIcon
|
|
167
|
+
name="i-lucide-folder-open"
|
|
168
|
+
class="size-8 mx-auto mb-2 opacity-50"
|
|
169
|
+
/>
|
|
170
|
+
<p>No files found</p>
|
|
171
|
+
<p class="text-xs mt-1">
|
|
172
|
+
Right-click to create a file
|
|
173
|
+
</p>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<UTree
|
|
177
|
+
v-else
|
|
178
|
+
:items="treeItems"
|
|
179
|
+
:get-key="(item: TreeItem) => item.id"
|
|
180
|
+
color="primary"
|
|
181
|
+
expanded-icon="i-lucide-folder-open"
|
|
182
|
+
collapsed-icon="i-lucide-folder"
|
|
183
|
+
@select="handleSelect"
|
|
184
|
+
>
|
|
185
|
+
<template #item="{ item, expanded }">
|
|
186
|
+
<div
|
|
187
|
+
class="flex items-center gap-2 w-full"
|
|
188
|
+
@contextmenu="contextMenuTarget = item"
|
|
189
|
+
>
|
|
190
|
+
<UIcon
|
|
191
|
+
v-if="item.children?.length"
|
|
192
|
+
:name="expanded ? 'i-lucide-folder-open' : 'i-lucide-folder'"
|
|
193
|
+
class="size-4 shrink-0 text-dimmed"
|
|
194
|
+
/>
|
|
195
|
+
<UIcon
|
|
196
|
+
v-else-if="item.icon"
|
|
197
|
+
:name="item.icon"
|
|
198
|
+
class="size-4 shrink-0 text-dimmed"
|
|
199
|
+
/>
|
|
200
|
+
<UIcon
|
|
201
|
+
v-else
|
|
202
|
+
name="i-lucide-file"
|
|
203
|
+
class="size-4 shrink-0 text-dimmed"
|
|
204
|
+
/>
|
|
205
|
+
<span class="truncate">{{ item.label }}</span>
|
|
206
|
+
</div>
|
|
207
|
+
</template>
|
|
208
|
+
</UTree>
|
|
209
|
+
</div>
|
|
210
|
+
</UContextMenu>
|
|
211
|
+
|
|
212
|
+
<!-- New File Modal -->
|
|
213
|
+
<UModal v-model:open="showNewFileModal">
|
|
214
|
+
<template #content>
|
|
215
|
+
<UCard>
|
|
216
|
+
<template #header>
|
|
217
|
+
<div class="flex items-center gap-2">
|
|
218
|
+
<UIcon
|
|
219
|
+
name="i-lucide-file-plus"
|
|
220
|
+
class="size-5"
|
|
221
|
+
/>
|
|
222
|
+
<span class="font-semibold">New File</span>
|
|
223
|
+
</div>
|
|
224
|
+
</template>
|
|
225
|
+
|
|
226
|
+
<UInput
|
|
227
|
+
v-model="newItemName"
|
|
228
|
+
placeholder="filename.py"
|
|
229
|
+
autofocus
|
|
230
|
+
@keyup.enter="handleCreateFile"
|
|
231
|
+
/>
|
|
232
|
+
|
|
233
|
+
<template #footer>
|
|
234
|
+
<div class="flex justify-end gap-2">
|
|
235
|
+
<UButton
|
|
236
|
+
color="neutral"
|
|
237
|
+
variant="ghost"
|
|
238
|
+
@click="showNewFileModal = false"
|
|
239
|
+
>
|
|
240
|
+
Cancel
|
|
241
|
+
</UButton>
|
|
242
|
+
<UButton @click="handleCreateFile">
|
|
243
|
+
Create
|
|
244
|
+
</UButton>
|
|
245
|
+
</div>
|
|
246
|
+
</template>
|
|
247
|
+
</UCard>
|
|
248
|
+
</template>
|
|
249
|
+
</UModal>
|
|
250
|
+
|
|
251
|
+
<!-- New Folder Modal -->
|
|
252
|
+
<UModal v-model:open="showNewFolderModal">
|
|
253
|
+
<template #content>
|
|
254
|
+
<UCard>
|
|
255
|
+
<template #header>
|
|
256
|
+
<div class="flex items-center gap-2">
|
|
257
|
+
<UIcon
|
|
258
|
+
name="i-lucide-folder-plus"
|
|
259
|
+
class="size-5"
|
|
260
|
+
/>
|
|
261
|
+
<span class="font-semibold">New Folder</span>
|
|
262
|
+
</div>
|
|
263
|
+
</template>
|
|
264
|
+
|
|
265
|
+
<UInput
|
|
266
|
+
v-model="newItemName"
|
|
267
|
+
placeholder="folder-name"
|
|
268
|
+
autofocus
|
|
269
|
+
@keyup.enter="handleCreateFolder"
|
|
270
|
+
/>
|
|
271
|
+
|
|
272
|
+
<template #footer>
|
|
273
|
+
<div class="flex justify-end gap-2">
|
|
274
|
+
<UButton
|
|
275
|
+
color="neutral"
|
|
276
|
+
variant="ghost"
|
|
277
|
+
@click="showNewFolderModal = false"
|
|
278
|
+
>
|
|
279
|
+
Cancel
|
|
280
|
+
</UButton>
|
|
281
|
+
<UButton @click="handleCreateFolder">
|
|
282
|
+
Create
|
|
283
|
+
</UButton>
|
|
284
|
+
</div>
|
|
285
|
+
</template>
|
|
286
|
+
</UCard>
|
|
287
|
+
</template>
|
|
288
|
+
</UModal>
|
|
289
|
+
|
|
290
|
+
<!-- Delete Confirmation -->
|
|
291
|
+
<UModal v-model:open="showDeleteConfirm">
|
|
292
|
+
<template #content>
|
|
293
|
+
<UCard>
|
|
294
|
+
<template #header>
|
|
295
|
+
<div class="flex items-center gap-2 text-error">
|
|
296
|
+
<UIcon
|
|
297
|
+
name="i-lucide-trash-2"
|
|
298
|
+
class="size-5"
|
|
299
|
+
/>
|
|
300
|
+
<span class="font-semibold">Delete</span>
|
|
301
|
+
</div>
|
|
302
|
+
</template>
|
|
303
|
+
|
|
304
|
+
<p>
|
|
305
|
+
Are you sure you want to delete
|
|
306
|
+
<strong>{{ contextMenuTarget?.label }}</strong>?
|
|
307
|
+
</p>
|
|
308
|
+
<p
|
|
309
|
+
v-if="contextMenuTarget?.type === 'directory'"
|
|
310
|
+
class="text-sm text-dimmed mt-1"
|
|
311
|
+
>
|
|
312
|
+
This will delete all files and folders inside.
|
|
313
|
+
</p>
|
|
314
|
+
|
|
315
|
+
<template #footer>
|
|
316
|
+
<div class="flex justify-end gap-2">
|
|
317
|
+
<UButton
|
|
318
|
+
color="neutral"
|
|
319
|
+
variant="ghost"
|
|
320
|
+
@click="showDeleteConfirm = false"
|
|
321
|
+
>
|
|
322
|
+
Cancel
|
|
323
|
+
</UButton>
|
|
324
|
+
<UButton
|
|
325
|
+
color="error"
|
|
326
|
+
@click="handleDelete"
|
|
327
|
+
>
|
|
328
|
+
Delete
|
|
329
|
+
</UButton>
|
|
330
|
+
</div>
|
|
331
|
+
</template>
|
|
332
|
+
</UCard>
|
|
333
|
+
</template>
|
|
334
|
+
</UModal>
|
|
335
|
+
</div>
|
|
336
|
+
</template>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SkillCatalogItem } from '~~/shared/types'
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
skill: SkillCatalogItem
|
|
6
|
+
}>()
|
|
7
|
+
|
|
8
|
+
const emit = defineEmits<{
|
|
9
|
+
install: [name: string]
|
|
10
|
+
update: [name: string]
|
|
11
|
+
}>()
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div class="p-4 rounded-lg border border-default bg-elevated/50 hover:bg-elevated transition-colors">
|
|
16
|
+
<div class="flex items-start justify-between gap-2 mb-2">
|
|
17
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
18
|
+
<UIcon
|
|
19
|
+
name="i-lucide-puzzle"
|
|
20
|
+
class="size-4 shrink-0 text-primary"
|
|
21
|
+
/>
|
|
22
|
+
<span class="font-medium truncate">{{ skill.name }}</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="flex items-center gap-1.5 shrink-0">
|
|
25
|
+
<UBadge
|
|
26
|
+
v-if="skill.installed && !skill.hasUpdate"
|
|
27
|
+
variant="subtle"
|
|
28
|
+
color="success"
|
|
29
|
+
size="xs"
|
|
30
|
+
>
|
|
31
|
+
Installed
|
|
32
|
+
</UBadge>
|
|
33
|
+
<UBadge
|
|
34
|
+
v-if="skill.hasUpdate"
|
|
35
|
+
variant="subtle"
|
|
36
|
+
color="warning"
|
|
37
|
+
size="xs"
|
|
38
|
+
>
|
|
39
|
+
Update available
|
|
40
|
+
</UBadge>
|
|
41
|
+
<UBadge
|
|
42
|
+
variant="subtle"
|
|
43
|
+
color="neutral"
|
|
44
|
+
size="xs"
|
|
45
|
+
>
|
|
46
|
+
v{{ skill.version }}
|
|
47
|
+
</UBadge>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<p class="text-sm text-muted line-clamp-2 mb-3">
|
|
52
|
+
{{ skill.description }}
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
<div
|
|
56
|
+
v-if="skill.tags.length > 0"
|
|
57
|
+
class="flex flex-wrap gap-1 mb-3"
|
|
58
|
+
>
|
|
59
|
+
<UBadge
|
|
60
|
+
v-for="tag in skill.tags"
|
|
61
|
+
:key="tag"
|
|
62
|
+
variant="subtle"
|
|
63
|
+
:color="tag === 'official' ? 'primary' : 'neutral'"
|
|
64
|
+
size="xs"
|
|
65
|
+
>
|
|
66
|
+
{{ tag }}
|
|
67
|
+
</UBadge>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div
|
|
71
|
+
v-if="skill.requiresSecrets.length > 0"
|
|
72
|
+
class="flex items-center gap-1 mb-3 text-xs text-dimmed"
|
|
73
|
+
>
|
|
74
|
+
<UIcon
|
|
75
|
+
name="i-lucide-key-round"
|
|
76
|
+
class="size-3 shrink-0"
|
|
77
|
+
/>
|
|
78
|
+
<span>Requires: {{ skill.requiresSecrets.join(', ') }}</span>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="flex items-center justify-between">
|
|
82
|
+
<span
|
|
83
|
+
v-if="skill.author"
|
|
84
|
+
class="text-xs text-dimmed"
|
|
85
|
+
>
|
|
86
|
+
by {{ skill.author }}
|
|
87
|
+
</span>
|
|
88
|
+
<span v-else />
|
|
89
|
+
|
|
90
|
+
<div class="flex items-center gap-1.5">
|
|
91
|
+
<UButton
|
|
92
|
+
size="xs"
|
|
93
|
+
variant="ghost"
|
|
94
|
+
color="neutral"
|
|
95
|
+
icon="i-lucide-external-link"
|
|
96
|
+
:to="`https://github.com/Patrity/cognova-skills/tree/main/${skill.name}`"
|
|
97
|
+
target="_blank"
|
|
98
|
+
>
|
|
99
|
+
View
|
|
100
|
+
</UButton>
|
|
101
|
+
<UButton
|
|
102
|
+
v-if="!skill.installed"
|
|
103
|
+
size="xs"
|
|
104
|
+
icon="i-lucide-download"
|
|
105
|
+
@click="emit('install', skill.name)"
|
|
106
|
+
>
|
|
107
|
+
Install
|
|
108
|
+
</UButton>
|
|
109
|
+
<UButton
|
|
110
|
+
v-else-if="skill.hasUpdate"
|
|
111
|
+
size="xs"
|
|
112
|
+
color="warning"
|
|
113
|
+
variant="soft"
|
|
114
|
+
icon="i-lucide-refresh-cw"
|
|
115
|
+
@click="emit('update', skill.name)"
|
|
116
|
+
>
|
|
117
|
+
Update
|
|
118
|
+
</UButton>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</template>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = defineProps<{
|
|
3
|
+
open: boolean
|
|
4
|
+
currentName: string
|
|
5
|
+
}>()
|
|
6
|
+
|
|
7
|
+
const emit = defineEmits<{
|
|
8
|
+
'update:open': [value: boolean]
|
|
9
|
+
'renamed': [newName: string]
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const toast = useToast()
|
|
13
|
+
const newName = ref(props.currentName)
|
|
14
|
+
const loading = ref(false)
|
|
15
|
+
|
|
16
|
+
watch(() => props.currentName, (val) => {
|
|
17
|
+
newName.value = val
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
function close() {
|
|
21
|
+
emit('update:open', false)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function handleRename() {
|
|
25
|
+
if (!newName.value.trim() || newName.value.trim() === props.currentName) return
|
|
26
|
+
|
|
27
|
+
loading.value = true
|
|
28
|
+
try {
|
|
29
|
+
await $fetch(`/api/skills/${props.currentName}/rename`, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
body: { newName: newName.value.trim() }
|
|
32
|
+
})
|
|
33
|
+
toast.add({ title: `Skill renamed to '${newName.value.trim()}'`, color: 'success' })
|
|
34
|
+
emit('renamed', newName.value.trim())
|
|
35
|
+
close()
|
|
36
|
+
} catch (e: unknown) {
|
|
37
|
+
const message = e instanceof Error ? e.message : 'Failed to rename skill'
|
|
38
|
+
toast.add({ title: message, color: 'error' })
|
|
39
|
+
} finally {
|
|
40
|
+
loading.value = false
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<UModal
|
|
47
|
+
:open="props.open"
|
|
48
|
+
@update:open="emit('update:open', $event)"
|
|
49
|
+
>
|
|
50
|
+
<template #header>
|
|
51
|
+
<span class="font-semibold">Rename Skill</span>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<template #body>
|
|
55
|
+
<UFormField label="New Name">
|
|
56
|
+
<UInput
|
|
57
|
+
v-model="newName"
|
|
58
|
+
placeholder="my-skill"
|
|
59
|
+
:disabled="loading"
|
|
60
|
+
/>
|
|
61
|
+
</UFormField>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<template #footer>
|
|
65
|
+
<div class="flex justify-end gap-2">
|
|
66
|
+
<UButton
|
|
67
|
+
variant="ghost"
|
|
68
|
+
color="neutral"
|
|
69
|
+
:disabled="loading"
|
|
70
|
+
@click="close"
|
|
71
|
+
>
|
|
72
|
+
Cancel
|
|
73
|
+
</UButton>
|
|
74
|
+
<UButton
|
|
75
|
+
:loading="loading"
|
|
76
|
+
:disabled="!newName.trim() || newName.trim() === currentName"
|
|
77
|
+
@click="handleRename"
|
|
78
|
+
>
|
|
79
|
+
Rename
|
|
80
|
+
</UButton>
|
|
81
|
+
</div>
|
|
82
|
+
</template>
|
|
83
|
+
</UModal>
|
|
84
|
+
</template>
|