@skillsgate/tui 0.1.1
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/bin/skillsgate-tui +28 -0
- package/bunfig.toml +3 -0
- package/package.json +24 -0
- package/src/app.tsx +18 -0
- package/src/components/agent-filter.tsx +162 -0
- package/src/components/confirm-dialog.tsx +56 -0
- package/src/components/help-overlay.tsx +101 -0
- package/src/components/layout.tsx +272 -0
- package/src/components/search-input.tsx +48 -0
- package/src/components/skill-list-item.tsx +45 -0
- package/src/components/skill-list.tsx +245 -0
- package/src/components/status-bar.tsx +34 -0
- package/src/data/api-client.ts +151 -0
- package/src/data/use-agents.ts +41 -0
- package/src/data/use-auth.ts +136 -0
- package/src/data/use-favorites.ts +147 -0
- package/src/data/use-installed-skills.ts +128 -0
- package/src/data/use-search.ts +118 -0
- package/src/data/use-skill-actions.ts +333 -0
- package/src/db/context.tsx +38 -0
- package/src/db/index.ts +19 -0
- package/src/db/migrations.ts +72 -0
- package/src/db/servers.ts +154 -0
- package/src/db/settings.ts +43 -0
- package/src/db/skills.ts +138 -0
- package/src/db/ssh.ts +319 -0
- package/src/index.tsx +37 -0
- package/src/store/context.tsx +26 -0
- package/src/store/reducers.ts +126 -0
- package/src/store/types.ts +124 -0
- package/src/utils/colors.ts +42 -0
- package/src/views/add-server.tsx +240 -0
- package/src/views/discover.tsx +419 -0
- package/src/views/favorites.tsx +358 -0
- package/src/views/home.tsx +218 -0
- package/src/views/login.tsx +202 -0
- package/src/views/server-skills.tsx +269 -0
- package/src/views/servers.tsx +449 -0
- package/src/views/settings.tsx +185 -0
- package/src/views/skill-detail.tsx +497 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { useState, useEffect } from "react"
|
|
2
|
+
import fs from "node:fs"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { exec } from "node:child_process"
|
|
5
|
+
import { useKeyboard } from "@opentui/react"
|
|
6
|
+
import { useStore, useDispatch } from "../store/context.js"
|
|
7
|
+
import { useSkillActions } from "../data/use-skill-actions.js"
|
|
8
|
+
import { ConfirmDialog } from "../components/confirm-dialog.js"
|
|
9
|
+
import { colors, agentBadges as badgeMap } from "../utils/colors.js"
|
|
10
|
+
import { agents } from "../../../cli/src/core/agents.js"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reads the full SKILL.md content for display.
|
|
14
|
+
* Uses synchronous read since we need it immediately and it's a local file.
|
|
15
|
+
*/
|
|
16
|
+
function readSkillContent(filePath: string): string {
|
|
17
|
+
try {
|
|
18
|
+
return fs.readFileSync(filePath, "utf-8")
|
|
19
|
+
} catch {
|
|
20
|
+
return "(Could not read skill file)"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Strips frontmatter (--- delimited block at the top) from markdown content
|
|
26
|
+
* so we display only the body.
|
|
27
|
+
*/
|
|
28
|
+
function stripFrontmatter(content: string): string {
|
|
29
|
+
const lines = content.split("\n")
|
|
30
|
+
if (lines[0]?.trim() !== "---") return content
|
|
31
|
+
|
|
32
|
+
let endIndex = -1
|
|
33
|
+
for (let i = 1; i < lines.length; i++) {
|
|
34
|
+
if (lines[i].trim() === "---") {
|
|
35
|
+
endIndex = i
|
|
36
|
+
break
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (endIndex === -1) return content
|
|
41
|
+
return lines.slice(endIndex + 1).join("\n").trimStart()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the display name for an agent key (e.g. "claude-code" -> "Claude Code").
|
|
46
|
+
*/
|
|
47
|
+
function agentDisplayName(agentName: string): string {
|
|
48
|
+
return agents[agentName]?.displayName ?? agentName
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type DetailPendingAction = "remove" | "install" | null
|
|
52
|
+
type RemoveMode = null | "confirm" | "select-agent"
|
|
53
|
+
|
|
54
|
+
export function SkillDetailView() {
|
|
55
|
+
const state = useStore()
|
|
56
|
+
const dispatch = useDispatch()
|
|
57
|
+
const { installSkill, removeSkill, removeSkillFromOneAgent } = useSkillActions()
|
|
58
|
+
const skill = state.selectedSkill
|
|
59
|
+
|
|
60
|
+
const [content, setContent] = useState("")
|
|
61
|
+
const [rawContent, setRawContent] = useState("") // full file content for editing
|
|
62
|
+
const [contentLoading, setContentLoading] = useState(false)
|
|
63
|
+
const [editMode, setEditMode] = useState(false)
|
|
64
|
+
const [pendingAction, setPendingAction] = useState<DetailPendingAction>(null)
|
|
65
|
+
const [removeMode, setRemoveMode] = useState<RemoveMode>(null)
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!skill) return
|
|
69
|
+
|
|
70
|
+
// Local skill: read from disk
|
|
71
|
+
if (skill.filePath) {
|
|
72
|
+
const raw = readSkillContent(skill.filePath)
|
|
73
|
+
setRawContent(raw)
|
|
74
|
+
setContent(stripFrontmatter(raw))
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Catalog skill: fetch content from API
|
|
79
|
+
const githubUrl = skill.metadata?.githubUrl as string | undefined
|
|
80
|
+
const urlPath = skill.metadata?.urlPath as string | undefined
|
|
81
|
+
if (githubUrl || urlPath) {
|
|
82
|
+
setContentLoading(true)
|
|
83
|
+
const detailPath = urlPath
|
|
84
|
+
? `/api/v1/skills/detail?path=${encodeURIComponent(urlPath)}`
|
|
85
|
+
: `/api/v1/skills/detail?path=${encodeURIComponent(skill.name)}`
|
|
86
|
+
fetch(`https://api.skillsgate.ai${detailPath}`)
|
|
87
|
+
.then(res => res.ok ? res.json() : null)
|
|
88
|
+
.then(data => {
|
|
89
|
+
if (data?.content) {
|
|
90
|
+
setContent(stripFrontmatter(data.content))
|
|
91
|
+
} else {
|
|
92
|
+
setContent(skill.description || "(No content available)")
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
.catch(() => setContent(skill.description || "(Could not load content)"))
|
|
96
|
+
.finally(() => setContentLoading(false))
|
|
97
|
+
} else {
|
|
98
|
+
setContent(skill.description || "(No content available)")
|
|
99
|
+
}
|
|
100
|
+
}, [skill?.name, skill?.filePath])
|
|
101
|
+
|
|
102
|
+
// Detail view keyboard handling
|
|
103
|
+
useKeyboard((key) => {
|
|
104
|
+
if (state.activeView !== "detail") return
|
|
105
|
+
if (state.showHelp) return
|
|
106
|
+
|
|
107
|
+
// Handle agent selection menu for per-agent delete
|
|
108
|
+
if (removeMode === "select-agent" && skill) {
|
|
109
|
+
if (key.name === "n" || key.name === "escape") {
|
|
110
|
+
setRemoveMode(null)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
if (key.name === "a") {
|
|
114
|
+
// Remove from all agents
|
|
115
|
+
setRemoveMode(null)
|
|
116
|
+
setPendingAction("remove")
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
// Number keys 1-9 to select a specific agent
|
|
120
|
+
const num = parseInt(key.raw ?? "", 10)
|
|
121
|
+
if (num >= 1 && num <= skill.agents.length) {
|
|
122
|
+
const agentName = skill.agents[num - 1]
|
|
123
|
+
setRemoveMode(null)
|
|
124
|
+
removeSkillFromOneAgent(skill, agentName).then(() => {
|
|
125
|
+
// If that was the last agent, go back to list
|
|
126
|
+
if (skill.agents.length <= 1) {
|
|
127
|
+
dispatch({ type: "GO_BACK" })
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (pendingAction) return // Block during confirm dialog
|
|
136
|
+
|
|
137
|
+
// q or Esc to go back
|
|
138
|
+
if (key.name === "q" || key.name === "escape") {
|
|
139
|
+
dispatch({ type: "GO_BACK" })
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// e to toggle between rendered view and raw source (only for local skills)
|
|
144
|
+
if (key.name === "e" && skill?.filePath) {
|
|
145
|
+
setEditMode(!editMode)
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// o to open folder (local skills) or source URL (catalog/github skills)
|
|
150
|
+
if (key.name === "o" && skill) {
|
|
151
|
+
const cmd = process.platform === "darwin" ? "open" : "xdg-open"
|
|
152
|
+
|
|
153
|
+
if (skill.filePath) {
|
|
154
|
+
// Local skill: open the containing folder
|
|
155
|
+
const dir = path.dirname(skill.filePath)
|
|
156
|
+
try {
|
|
157
|
+
exec(`${cmd} "${dir}"`)
|
|
158
|
+
dispatch({
|
|
159
|
+
type: "SHOW_NOTIFICATION",
|
|
160
|
+
notification: { type: "info", message: `Opening ${dir}` },
|
|
161
|
+
})
|
|
162
|
+
} catch {
|
|
163
|
+
dispatch({
|
|
164
|
+
type: "SHOW_NOTIFICATION",
|
|
165
|
+
notification: { type: "error", message: "Failed to open folder" },
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
} else if (skill.lock?.sourceType === "github") {
|
|
169
|
+
// Catalog/GitHub skill: open the source URL
|
|
170
|
+
const url = skill.lock.originalUrl
|
|
171
|
+
if (url) {
|
|
172
|
+
try {
|
|
173
|
+
exec(`${cmd} "${url}"`)
|
|
174
|
+
dispatch({
|
|
175
|
+
type: "SHOW_NOTIFICATION",
|
|
176
|
+
notification: { type: "info", message: `Opening ${url}` },
|
|
177
|
+
})
|
|
178
|
+
} catch {
|
|
179
|
+
dispatch({
|
|
180
|
+
type: "SHOW_NOTIFICATION",
|
|
181
|
+
notification: { type: "error", message: "Failed to open URL" },
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// d to remove skill
|
|
190
|
+
if (key.name === "d" && skill && skill.agents.length > 0) {
|
|
191
|
+
if (skill.agents.length > 1) {
|
|
192
|
+
// Multiple agents: show selection menu
|
|
193
|
+
setRemoveMode("select-agent")
|
|
194
|
+
} else {
|
|
195
|
+
// Single agent: simple confirm
|
|
196
|
+
setPendingAction("remove")
|
|
197
|
+
}
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// i to install (for catalog skills not yet installed)
|
|
202
|
+
if (key.name === "i" && skill && skill.agents.length === 0) {
|
|
203
|
+
setPendingAction("install")
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// Agent selection menu for per-agent delete
|
|
209
|
+
if (removeMode === "select-agent" && skill) {
|
|
210
|
+
return (
|
|
211
|
+
<box
|
|
212
|
+
style={{
|
|
213
|
+
width: "100%",
|
|
214
|
+
height: "100%",
|
|
215
|
+
justifyContent: "center",
|
|
216
|
+
alignItems: "center",
|
|
217
|
+
backgroundColor: colors.bg,
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
<box
|
|
221
|
+
style={{
|
|
222
|
+
width: 60,
|
|
223
|
+
border: true,
|
|
224
|
+
borderColor: colors.primary,
|
|
225
|
+
backgroundColor: "#1a1a2e",
|
|
226
|
+
paddingLeft: 2,
|
|
227
|
+
paddingRight: 2,
|
|
228
|
+
paddingTop: 1,
|
|
229
|
+
paddingBottom: 1,
|
|
230
|
+
flexDirection: "column",
|
|
231
|
+
}}
|
|
232
|
+
title="Remove"
|
|
233
|
+
>
|
|
234
|
+
<text fg={colors.text}>
|
|
235
|
+
Remove "<span fg={colors.primary}>{skill.name}</span>" from:
|
|
236
|
+
</text>
|
|
237
|
+
<text>{" "}</text>
|
|
238
|
+
{skill.agents.map((agentName, i) => {
|
|
239
|
+
const badge = badgeMap[agentName]
|
|
240
|
+
return (
|
|
241
|
+
<text key={agentName} fg={colors.text}>
|
|
242
|
+
{" "}<span fg={colors.primary}>{i + 1}</span>{" "}<span fg={badge?.color ?? colors.agent}>{agentDisplayName(agentName)}</span>
|
|
243
|
+
</text>
|
|
244
|
+
)
|
|
245
|
+
})}
|
|
246
|
+
<text>{" "}</text>
|
|
247
|
+
<text fg={colors.text}>
|
|
248
|
+
{" "}<span fg={colors.error}>a</span>{" "}All agents (removes completely)
|
|
249
|
+
</text>
|
|
250
|
+
<text fg={colors.text}>
|
|
251
|
+
{" "}<span fg={colors.textDim}>n</span>{" "}Cancel
|
|
252
|
+
</text>
|
|
253
|
+
</box>
|
|
254
|
+
</box>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Confirm dialog for remove/install
|
|
259
|
+
if (pendingAction && skill) {
|
|
260
|
+
const actionLabel = pendingAction === "remove" ? "Remove" : "Install"
|
|
261
|
+
return (
|
|
262
|
+
<ConfirmDialog
|
|
263
|
+
message={`${actionLabel} "${skill.name}"?`}
|
|
264
|
+
onConfirm={async () => {
|
|
265
|
+
const action = pendingAction
|
|
266
|
+
setPendingAction(null)
|
|
267
|
+
if (action === "remove") {
|
|
268
|
+
await removeSkill(skill)
|
|
269
|
+
dispatch({ type: "GO_BACK" })
|
|
270
|
+
} else if (action === "install") {
|
|
271
|
+
await installSkill(skill)
|
|
272
|
+
}
|
|
273
|
+
}}
|
|
274
|
+
onCancel={() => setPendingAction(null)}
|
|
275
|
+
/>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!skill) {
|
|
280
|
+
return (
|
|
281
|
+
<box style={{ padding: 1 }}>
|
|
282
|
+
<text fg={colors.textDim}>No skill selected</text>
|
|
283
|
+
</box>
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Build metadata lines
|
|
288
|
+
const sourceType = skill.lock?.sourceType ?? "unknown"
|
|
289
|
+
const sourceUrl = skill.lock?.originalUrl ?? ""
|
|
290
|
+
const agentBadgeElements = skill.agents.map((a, i) => {
|
|
291
|
+
const badge = badgeMap[a]
|
|
292
|
+
return (
|
|
293
|
+
<text key={a} fg={badge?.color ?? colors.agent}>
|
|
294
|
+
{i > 0 ? " " : ""}{badge?.label ?? a.slice(0, 2).toUpperCase()}
|
|
295
|
+
</text>
|
|
296
|
+
)
|
|
297
|
+
})
|
|
298
|
+
const isInstalled = skill.agents.length > 0
|
|
299
|
+
const isLocal = !!skill.filePath
|
|
300
|
+
const installedAt = skill.lock?.installedAt
|
|
301
|
+
? new Date(skill.lock.installedAt).toLocaleDateString()
|
|
302
|
+
: null
|
|
303
|
+
const updatedAt = skill.lock?.updatedAt
|
|
304
|
+
? new Date(skill.lock.updatedAt).toLocaleDateString()
|
|
305
|
+
: null
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<box style={{ flexDirection: "row", width: "100%", flexGrow: 1 }}>
|
|
309
|
+
{/* Left side: Content (70%) - view or edit mode */}
|
|
310
|
+
{editMode ? (
|
|
311
|
+
<box style={{ width: "70%", flexGrow: 1, flexDirection: "column" }}>
|
|
312
|
+
<box style={{ height: 1, paddingLeft: 1, backgroundColor: colors.bgAlt }}>
|
|
313
|
+
<text fg={colors.warning}>RAW: {skill.filePath} (o=open folder Esc=back to view)</text>
|
|
314
|
+
</box>
|
|
315
|
+
<scrollbox
|
|
316
|
+
focused={false}
|
|
317
|
+
style={{
|
|
318
|
+
width: "100%",
|
|
319
|
+
flexGrow: 1,
|
|
320
|
+
rootOptions: { backgroundColor: colors.bg },
|
|
321
|
+
viewportOptions: { backgroundColor: colors.bg },
|
|
322
|
+
contentOptions: { backgroundColor: colors.bg },
|
|
323
|
+
}}
|
|
324
|
+
>
|
|
325
|
+
<box style={{ paddingLeft: 1, paddingRight: 1, paddingTop: 1, flexDirection: "column" }}>
|
|
326
|
+
{rawContent.split("\n").map((line, i) => (
|
|
327
|
+
<text key={i} fg={colors.text}>{line || " "}</text>
|
|
328
|
+
))}
|
|
329
|
+
</box>
|
|
330
|
+
</scrollbox>
|
|
331
|
+
</box>
|
|
332
|
+
) : (
|
|
333
|
+
<scrollbox
|
|
334
|
+
focused={false}
|
|
335
|
+
style={{
|
|
336
|
+
width: "70%",
|
|
337
|
+
flexGrow: 1,
|
|
338
|
+
rootOptions: { backgroundColor: colors.bg },
|
|
339
|
+
viewportOptions: { backgroundColor: colors.bg },
|
|
340
|
+
contentOptions: { backgroundColor: colors.bg },
|
|
341
|
+
scrollbarOptions: {
|
|
342
|
+
trackOptions: {
|
|
343
|
+
foregroundColor: colors.primary,
|
|
344
|
+
backgroundColor: colors.border,
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
}}
|
|
348
|
+
>
|
|
349
|
+
<box style={{ paddingLeft: 1, paddingRight: 1, paddingTop: 1, flexDirection: "column" }}>
|
|
350
|
+
{contentLoading && (
|
|
351
|
+
<text fg={colors.textDim}>Loading content...</text>
|
|
352
|
+
)}
|
|
353
|
+
{!contentLoading && content.split("\n").map((line, i) => {
|
|
354
|
+
// Style headings differently
|
|
355
|
+
if (line.startsWith("### ")) {
|
|
356
|
+
return (
|
|
357
|
+
<text key={i} fg={colors.primary}>
|
|
358
|
+
{line}
|
|
359
|
+
</text>
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
if (line.startsWith("## ")) {
|
|
363
|
+
return (
|
|
364
|
+
<text key={i} fg={colors.primary}>
|
|
365
|
+
<strong>{line}</strong>
|
|
366
|
+
</text>
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
if (line.startsWith("# ")) {
|
|
370
|
+
return (
|
|
371
|
+
<text key={i} fg={colors.primary}>
|
|
372
|
+
<strong>{line}</strong>
|
|
373
|
+
</text>
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
// Code blocks
|
|
377
|
+
if (line.startsWith("```")) {
|
|
378
|
+
return (
|
|
379
|
+
<text key={i} fg={colors.textDim}>
|
|
380
|
+
{line}
|
|
381
|
+
</text>
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
// Bullet points
|
|
385
|
+
if (line.trimStart().startsWith("- ") || line.trimStart().startsWith("* ")) {
|
|
386
|
+
return (
|
|
387
|
+
<text key={i} fg={colors.text}>
|
|
388
|
+
{line}
|
|
389
|
+
</text>
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
// Empty line
|
|
393
|
+
if (!line.trim()) {
|
|
394
|
+
return <text key={i}>{" "}</text>
|
|
395
|
+
}
|
|
396
|
+
// Normal text
|
|
397
|
+
return (
|
|
398
|
+
<text key={i} fg={colors.text}>
|
|
399
|
+
{line}
|
|
400
|
+
</text>
|
|
401
|
+
)
|
|
402
|
+
})}
|
|
403
|
+
</box>
|
|
404
|
+
</scrollbox>
|
|
405
|
+
)}
|
|
406
|
+
|
|
407
|
+
{/* Right side: Metadata panel (30%) */}
|
|
408
|
+
<box
|
|
409
|
+
style={{
|
|
410
|
+
width: "30%",
|
|
411
|
+
flexDirection: "column",
|
|
412
|
+
backgroundColor: colors.bgAlt,
|
|
413
|
+
borderLeft: true,
|
|
414
|
+
borderColor: colors.border,
|
|
415
|
+
paddingLeft: 1,
|
|
416
|
+
paddingRight: 1,
|
|
417
|
+
paddingTop: 1,
|
|
418
|
+
}}
|
|
419
|
+
>
|
|
420
|
+
{/* Skill name */}
|
|
421
|
+
<text fg={colors.primary}>
|
|
422
|
+
<strong>{skill.name}</strong>
|
|
423
|
+
</text>
|
|
424
|
+
<text>{" "}</text>
|
|
425
|
+
|
|
426
|
+
{/* Description */}
|
|
427
|
+
<text fg={colors.text}>{skill.description}</text>
|
|
428
|
+
<text>{" "}</text>
|
|
429
|
+
|
|
430
|
+
{/* Source */}
|
|
431
|
+
<text fg={colors.textDim}>Source</text>
|
|
432
|
+
<text fg={colors.text}> {sourceType}</text>
|
|
433
|
+
<text>{" "}</text>
|
|
434
|
+
|
|
435
|
+
{/* Source URL */}
|
|
436
|
+
{sourceUrl ? (
|
|
437
|
+
<>
|
|
438
|
+
<text fg={colors.textDim}>URL</text>
|
|
439
|
+
<text fg={colors.primary}> {sourceUrl}</text>
|
|
440
|
+
<text>{" "}</text>
|
|
441
|
+
</>
|
|
442
|
+
) : null}
|
|
443
|
+
|
|
444
|
+
{/* Status */}
|
|
445
|
+
<text fg={colors.textDim}>Status</text>
|
|
446
|
+
<text fg={isInstalled ? colors.success : colors.textDim}>
|
|
447
|
+
{" "}{isInstalled ? "Installed" : "Not installed"}
|
|
448
|
+
</text>
|
|
449
|
+
<text>{" "}</text>
|
|
450
|
+
|
|
451
|
+
{/* Agents (only if installed) */}
|
|
452
|
+
{isInstalled && agentBadgeElements.length > 0 && (
|
|
453
|
+
<>
|
|
454
|
+
<text fg={colors.textDim}>Agents</text>
|
|
455
|
+
<box style={{ flexDirection: "row", paddingLeft: 2 }}>
|
|
456
|
+
{agentBadgeElements}
|
|
457
|
+
</box>
|
|
458
|
+
<text>{" "}</text>
|
|
459
|
+
</>
|
|
460
|
+
)}
|
|
461
|
+
|
|
462
|
+
{/* Dates (only if installed) */}
|
|
463
|
+
{installedAt && (
|
|
464
|
+
<>
|
|
465
|
+
<text fg={colors.textDim}>Installed</text>
|
|
466
|
+
<text fg={colors.text}> {installedAt}</text>
|
|
467
|
+
<text>{" "}</text>
|
|
468
|
+
</>
|
|
469
|
+
)}
|
|
470
|
+
{updatedAt && (
|
|
471
|
+
<>
|
|
472
|
+
<text fg={colors.textDim}>Last updated</text>
|
|
473
|
+
<text fg={colors.text}> {updatedAt}</text>
|
|
474
|
+
<text>{" "}</text>
|
|
475
|
+
</>
|
|
476
|
+
)}
|
|
477
|
+
|
|
478
|
+
{/* Shortcut hints -- contextual based on skill type */}
|
|
479
|
+
<text fg={colors.border}>---</text>
|
|
480
|
+
<text fg={colors.textDim}>q/Esc Go back</text>
|
|
481
|
+
{isLocal && (
|
|
482
|
+
<text fg={colors.textDim}>e {editMode ? "Back to view" : "View raw source"}</text>
|
|
483
|
+
)}
|
|
484
|
+
{isLocal ? (
|
|
485
|
+
<text fg={colors.textDim}>o Open folder</text>
|
|
486
|
+
) : sourceType === "github" ? (
|
|
487
|
+
<text fg={colors.textDim}>o Open URL</text>
|
|
488
|
+
) : null}
|
|
489
|
+
{isInstalled ? (
|
|
490
|
+
<text fg={colors.textDim}>d Remove skill</text>
|
|
491
|
+
) : (
|
|
492
|
+
<text fg={colors.textDim}>i Install skill</text>
|
|
493
|
+
)}
|
|
494
|
+
</box>
|
|
495
|
+
</box>
|
|
496
|
+
)
|
|
497
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "esnext",
|
|
4
|
+
"lib": ["esnext"],
|
|
5
|
+
"module": "esnext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"jsxImportSource": "@opentui/react",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"paths": {
|
|
14
|
+
"@cli/*": ["../cli/src/*"]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
|
18
|
+
}
|