@skillsgate/tui 0.1.12 → 0.1.14
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/package.json +1 -8
- package/src/components/help-overlay.tsx +2 -4
- package/src/components/layout.tsx +8 -34
- package/src/components/status-bar.tsx +4 -7
- package/src/data/api-client.ts +78 -109
- package/src/data/use-favorites.ts +18 -4
- package/src/data/use-installed-skills.ts +173 -85
- package/src/data/use-search.ts +14 -31
- package/src/data/use-skill-actions.ts +68 -16
- package/src/db/migrations.ts +20 -0
- package/src/db/skills-cache.ts +89 -0
- package/src/views/discover.tsx +33 -126
- package/src/views/favorites.tsx +10 -354
- package/src/views/settings.tsx +0 -6
- package/src/views/skill-detail.tsx +15 -14
- package/tmp.json +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skillsgate/tui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"skillsgate-tui": "bin/skillsgate-tui"
|
|
@@ -15,13 +15,6 @@
|
|
|
15
15
|
"@opentui/react": "0.1.90",
|
|
16
16
|
"gray-matter": "4.0.3"
|
|
17
17
|
},
|
|
18
|
-
"optionalDependencies": {
|
|
19
|
-
"@skillsgate/tui-darwin-arm64": "0.1.12",
|
|
20
|
-
"@skillsgate/tui-darwin-x64": "0.1.12",
|
|
21
|
-
"@skillsgate/tui-linux-x64": "0.1.12",
|
|
22
|
-
"@skillsgate/tui-linux-arm64": "0.1.12",
|
|
23
|
-
"@skillsgate/tui-win32-x64": "0.1.12"
|
|
24
|
-
},
|
|
25
18
|
"peerDependencies": {
|
|
26
19
|
"react": "19.1.5"
|
|
27
20
|
},
|
|
@@ -18,14 +18,12 @@ const SHORTCUTS_LEFT: ShortcutEntry[] = [
|
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
const SHORTCUTS_RIGHT: ShortcutEntry[] = [
|
|
21
|
-
{ key: "1/2/3
|
|
21
|
+
{ key: "1/2/3", description: "Switch tabs" },
|
|
22
22
|
{ key: "s", description: "Open settings" },
|
|
23
23
|
{ key: "r", description: "Refresh installed skills" },
|
|
24
|
-
{ key: "l", description: "Login / auth status" },
|
|
25
24
|
{ key: "i", description: "Install selected skill" },
|
|
26
25
|
{ key: "d", description: "Remove selected skill" },
|
|
27
|
-
{ key: "
|
|
28
|
-
{ key: "m", description: "Toggle keyword/AI search" },
|
|
26
|
+
{ key: "", description: "" },
|
|
29
27
|
{ key: "?", description: "Toggle this help" },
|
|
30
28
|
{ key: "Ctrl+Q", description: "Quit" },
|
|
31
29
|
{ key: "", description: "" },
|
|
@@ -4,30 +4,22 @@ import { useStore, useDispatch } from "../store/context.js"
|
|
|
4
4
|
import { useDb } from "../db/context.js"
|
|
5
5
|
import { useDetectedAgents } from "../data/use-agents.js"
|
|
6
6
|
import { useInstalledSkills } from "../data/use-installed-skills.js"
|
|
7
|
-
import { useAuth } from "../data/use-auth.js"
|
|
8
7
|
import { StatusBar } from "./status-bar.js"
|
|
9
8
|
import { HelpOverlay } from "./help-overlay.js"
|
|
10
9
|
import { HomeView } from "../views/home.js"
|
|
11
10
|
import { SkillDetailView } from "../views/skill-detail.js"
|
|
12
11
|
import { DiscoverView } from "../views/discover.js"
|
|
13
|
-
import { FavoritesView } from "../views/favorites.js"
|
|
14
12
|
import { ServersView } from "../views/servers.js"
|
|
15
13
|
import { AddServerView } from "../views/add-server.js"
|
|
16
14
|
import { ServerSkillsView } from "../views/server-skills.js"
|
|
17
15
|
import { SettingsView } from "../views/settings.js"
|
|
18
|
-
import { LoginView } from "../views/login.js"
|
|
19
16
|
import { colors } from "../utils/colors.js"
|
|
20
17
|
import type { ViewName } from "../store/types.js"
|
|
21
18
|
|
|
22
|
-
function getTabOptions(
|
|
19
|
+
function getTabOptions(serverCount: number) {
|
|
23
20
|
return [
|
|
24
21
|
{ name: "Installed", description: "Locally installed skills", value: "home" },
|
|
25
22
|
{ name: "Discover", description: "Search the registry", value: "discover" },
|
|
26
|
-
{
|
|
27
|
-
name: favCount > 0 ? `Favorites (${favCount})` : "Favorites",
|
|
28
|
-
description: "Your starred skills",
|
|
29
|
-
value: "favorites",
|
|
30
|
-
},
|
|
31
23
|
{
|
|
32
24
|
name: serverCount > 0 ? `Servers (${serverCount})` : "Servers",
|
|
33
25
|
description: "Remote SSH servers",
|
|
@@ -43,8 +35,7 @@ export function Layout() {
|
|
|
43
35
|
const { servers } = useDb()
|
|
44
36
|
const [serverCount, setServerCount] = useState(() => servers.list().length)
|
|
45
37
|
|
|
46
|
-
// Load
|
|
47
|
-
useAuth()
|
|
38
|
+
// Load agent + skill data on mount
|
|
48
39
|
useDetectedAgents()
|
|
49
40
|
useInstalledSkills()
|
|
50
41
|
|
|
@@ -71,14 +62,6 @@ export function Layout() {
|
|
|
71
62
|
return
|
|
72
63
|
}
|
|
73
64
|
|
|
74
|
-
// When on login view, only handle Escape -- let input keys pass through
|
|
75
|
-
if (state.activeView === "login") {
|
|
76
|
-
if (key.name === "escape") {
|
|
77
|
-
dispatch({ type: "GO_BACK" })
|
|
78
|
-
}
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
|
|
82
65
|
// Help overlay toggle
|
|
83
66
|
if (key.name === "?" || (key.shift && key.name === "/")) {
|
|
84
67
|
dispatch({ type: "TOGGLE_HELP" })
|
|
@@ -98,18 +81,17 @@ export function Layout() {
|
|
|
98
81
|
const activeView = state.activeView as string
|
|
99
82
|
const inFormView = activeView === "detail" || activeView === "add-server"
|
|
100
83
|
|| activeView === "edit-server" || activeView === "settings"
|
|
101
|
-
|| activeView === "server-skills"
|
|
84
|
+
|| activeView === "server-skills"
|
|
102
85
|
if (!inFormView) {
|
|
103
86
|
if (key.name === "1") dispatch({ type: "NAVIGATE", view: "home" })
|
|
104
87
|
if (key.name === "2") dispatch({ type: "NAVIGATE", view: "discover" })
|
|
105
|
-
if (key.name === "3")
|
|
106
|
-
if (key.name === "4") {
|
|
88
|
+
if (key.name === "3") {
|
|
107
89
|
setServerCount(servers.list().length)
|
|
108
90
|
dispatch({ type: "NAVIGATE", view: "servers" })
|
|
109
91
|
}
|
|
110
92
|
}
|
|
111
93
|
|
|
112
|
-
// "s" to open settings (only from home/
|
|
94
|
+
// "s" to open settings (only from home/servers views when not in search)
|
|
113
95
|
if (key.name === "s" && (state.focusedPane as string) !== "search"
|
|
114
96
|
&& state.activeView !== "discover" && state.activeView !== "detail"
|
|
115
97
|
&& !inFormView) {
|
|
@@ -146,20 +128,14 @@ export function Layout() {
|
|
|
146
128
|
return
|
|
147
129
|
}
|
|
148
130
|
|
|
149
|
-
// "
|
|
150
|
-
if (key.name === "
|
|
151
|
-
dispatch({ type: "NAVIGATE", view: "login" })
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// "r" to refresh installed skills (when not typing in search, not on login view)
|
|
156
|
-
if (key.name === "r" && (state.focusedPane as string) !== "search" && activeView !== "detail" && activeView !== "login") {
|
|
131
|
+
// "r" to refresh installed skills (when not typing in search)
|
|
132
|
+
if (key.name === "r" && (state.focusedPane as string) !== "search" && activeView !== "detail") {
|
|
157
133
|
dispatch({ type: "REFRESH_SKILLS" })
|
|
158
134
|
return
|
|
159
135
|
}
|
|
160
136
|
})
|
|
161
137
|
|
|
162
|
-
const TAB_OPTIONS = getTabOptions(
|
|
138
|
+
const TAB_OPTIONS = getTabOptions(serverCount)
|
|
163
139
|
|
|
164
140
|
const activeTabIndex = TAB_OPTIONS.findIndex(
|
|
165
141
|
(t) => t.value === state.activeView
|
|
@@ -217,7 +193,6 @@ export function Layout() {
|
|
|
217
193
|
<>
|
|
218
194
|
{state.activeView === "home" && <HomeView />}
|
|
219
195
|
{state.activeView === "discover" && <DiscoverView />}
|
|
220
|
-
{state.activeView === "favorites" && <FavoritesView />}
|
|
221
196
|
{state.activeView === "servers" && <ServersView onServerCountChange={setServerCount} />}
|
|
222
197
|
{(state.activeView === "add-server" || state.activeView === "edit-server") && (
|
|
223
198
|
<AddServerView
|
|
@@ -229,7 +204,6 @@ export function Layout() {
|
|
|
229
204
|
<ServerSkillsView serverId={state.selectedServerId} />
|
|
230
205
|
)}
|
|
231
206
|
{state.activeView === "settings" && <SettingsView />}
|
|
232
|
-
{state.activeView === "login" && <LoginView />}
|
|
233
207
|
{state.activeView === "detail" && state.selectedSkill && (
|
|
234
208
|
<SkillDetailView />
|
|
235
209
|
)}
|
|
@@ -6,16 +6,13 @@ export function StatusBar() {
|
|
|
6
6
|
|
|
7
7
|
const skillCount = state.installedSkills.length
|
|
8
8
|
const agentCount = state.detectedAgents.length
|
|
9
|
-
const user = state.auth?.user?.name ?? "not logged in (l=login)"
|
|
10
9
|
const focusHint = state.activeView === "detail"
|
|
11
10
|
? "q=back"
|
|
12
|
-
: state.
|
|
13
|
-
? "Esc=
|
|
14
|
-
:
|
|
15
|
-
? "Tab=results Esc=exit search"
|
|
16
|
-
: "/=search Tab=switch pane"
|
|
11
|
+
: state.focusedPane === "search"
|
|
12
|
+
? "Tab=results Esc=exit search"
|
|
13
|
+
: "/=search Tab=switch pane"
|
|
17
14
|
|
|
18
|
-
const statusText = `Skills: ${skillCount} | Agents: ${agentCount} | ${
|
|
15
|
+
const statusText = `Skills: ${skillCount} | Agents: ${agentCount} | ${focusHint} | ?=help 1/2/3=tabs`
|
|
19
16
|
|
|
20
17
|
return (
|
|
21
18
|
<box
|
package/src/data/api-client.ts
CHANGED
|
@@ -1,151 +1,120 @@
|
|
|
1
|
-
const
|
|
1
|
+
const SKILLSGATE_API_BASE = process.env.SKILLSGATE_SEARCH_API_URL ?? "https://api.skillsgate.ai"
|
|
2
|
+
const SKILLS_SH_BASE = "https://skills.sh"
|
|
3
|
+
const GITHUB_API_BASE = "https://api.github.com"
|
|
4
|
+
const GITHUB_RAW_BASE = "https://raw.githubusercontent.com"
|
|
2
5
|
|
|
3
6
|
// ---------- Types ----------
|
|
4
7
|
|
|
5
8
|
export interface CatalogSkill {
|
|
6
9
|
id: string
|
|
7
|
-
|
|
10
|
+
skillId: string
|
|
8
11
|
name: string
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
categories: string[]
|
|
12
|
-
capabilities?: string[]
|
|
13
|
-
keywords?: string[]
|
|
14
|
-
githubUrl?: string
|
|
15
|
-
githubStars?: number | null
|
|
16
|
-
installCommand?: string | null
|
|
17
|
-
score?: number
|
|
18
|
-
username?: string
|
|
19
|
-
urlPath?: string
|
|
12
|
+
installs: number
|
|
13
|
+
source: string
|
|
20
14
|
}
|
|
21
15
|
|
|
22
|
-
interface
|
|
16
|
+
interface SkillsShSearchResponse {
|
|
23
17
|
skills: CatalogSkill[]
|
|
24
|
-
|
|
25
|
-
total: number
|
|
26
|
-
limit: number
|
|
27
|
-
offset: number
|
|
28
|
-
hasMore?: boolean
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface KeywordSearchResponse {
|
|
33
|
-
skills: CatalogSkill[]
|
|
34
|
-
meta: {
|
|
35
|
-
total: number
|
|
36
|
-
limit: number
|
|
37
|
-
offset: number
|
|
38
|
-
hasMore?: boolean
|
|
39
|
-
}
|
|
18
|
+
count: number
|
|
40
19
|
}
|
|
41
20
|
|
|
42
|
-
interface SemanticSearchResponse {
|
|
43
|
-
results: CatalogSkill[]
|
|
44
|
-
meta: {
|
|
45
|
-
query: string
|
|
46
|
-
total: number
|
|
47
|
-
limit: number
|
|
48
|
-
remainingSearches: number
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export type SearchMode = "keyword" | "semantic"
|
|
53
|
-
|
|
54
21
|
export interface SearchResult {
|
|
55
22
|
skills: CatalogSkill[]
|
|
56
23
|
total: number
|
|
57
|
-
remainingSearches?: number
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ---------- Catalog ----------
|
|
61
|
-
|
|
62
|
-
export async function fetchCatalog(
|
|
63
|
-
limit: number = 20,
|
|
64
|
-
offset: number = 0
|
|
65
|
-
): Promise<{ skills: CatalogSkill[]; total: number }> {
|
|
66
|
-
const url = `${API_BASE}/api/v1/skills?limit=${limit}&offset=${offset}`
|
|
67
|
-
|
|
68
|
-
const response = await fetch(url, {
|
|
69
|
-
headers: { "Content-Type": "application/json" },
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
if (!response.ok) {
|
|
73
|
-
throw new Error(`Catalog fetch failed (HTTP ${response.status})`)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const data = (await response.json()) as CatalogResponse
|
|
77
|
-
return { skills: data.skills ?? [], total: data.meta?.total ?? 0 }
|
|
78
24
|
}
|
|
79
25
|
|
|
80
|
-
// ----------
|
|
26
|
+
// ---------- Search (skills.sh) ----------
|
|
81
27
|
|
|
82
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Search for skills or load popular skills when query is empty.
|
|
30
|
+
* Public endpoint, no authentication required.
|
|
31
|
+
*/
|
|
32
|
+
export async function searchSkills(
|
|
83
33
|
query: string,
|
|
84
34
|
limit: number = 20,
|
|
85
35
|
offset: number = 0
|
|
86
36
|
): Promise<SearchResult> {
|
|
87
|
-
|
|
37
|
+
// skills.sh requires a minimum 2-char query
|
|
38
|
+
const q = query.trim() || "skill"
|
|
39
|
+
const params = new URLSearchParams({ q, limit: String(limit), offset: String(offset) })
|
|
88
40
|
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
})
|
|
41
|
+
const url = `${SKILLS_SH_BASE}/api/search?${params}`
|
|
42
|
+
const response = await fetch(url)
|
|
92
43
|
|
|
93
44
|
if (!response.ok) {
|
|
94
|
-
throw new Error(`
|
|
45
|
+
throw new Error(`Search failed (HTTP ${response.status})`)
|
|
95
46
|
}
|
|
96
47
|
|
|
97
|
-
const data = (await response.json()) as
|
|
48
|
+
const data = (await response.json()) as SkillsShSearchResponse
|
|
98
49
|
return {
|
|
99
50
|
skills: data.skills ?? [],
|
|
100
|
-
total: data.
|
|
51
|
+
total: data.count ?? 0,
|
|
101
52
|
}
|
|
102
53
|
}
|
|
103
54
|
|
|
104
|
-
// ----------
|
|
55
|
+
// ---------- Skill Content (GitHub raw) ----------
|
|
105
56
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
57
|
+
/**
|
|
58
|
+
* In-memory cache for GitHub default branch lookups.
|
|
59
|
+
*/
|
|
60
|
+
const branchCache = new Map<string, string>()
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Fetches the default branch for a GitHub repository.
|
|
64
|
+
*/
|
|
65
|
+
async function getDefaultBranch(source: string): Promise<string> {
|
|
66
|
+
const cached = branchCache.get(source)
|
|
67
|
+
if (cached) return cached
|
|
68
|
+
|
|
69
|
+
const response = await fetch(`${GITHUB_API_BASE}/repos/${source}`, {
|
|
70
|
+
headers: { Accept: "application/vnd.github.v3+json" },
|
|
120
71
|
})
|
|
121
72
|
|
|
122
73
|
if (!response.ok) {
|
|
123
|
-
|
|
124
|
-
throw new Error("RATE_LIMIT")
|
|
125
|
-
}
|
|
126
|
-
if (response.status === 401) {
|
|
127
|
-
throw new Error("AUTH_EXPIRED")
|
|
128
|
-
}
|
|
129
|
-
throw new Error(`Semantic search failed (HTTP ${response.status})`)
|
|
74
|
+
throw new Error(`GitHub API error (HTTP ${response.status})`)
|
|
130
75
|
}
|
|
131
76
|
|
|
132
|
-
const data = (await response.json()) as
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
remainingSearches: data.meta?.remainingSearches,
|
|
137
|
-
}
|
|
77
|
+
const data = (await response.json()) as { default_branch: string }
|
|
78
|
+
const branch = data.default_branch
|
|
79
|
+
branchCache.set(source, branch)
|
|
80
|
+
return branch
|
|
138
81
|
}
|
|
139
82
|
|
|
140
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Candidate paths where a SKILL.md file might be located in a repository.
|
|
85
|
+
*/
|
|
86
|
+
function candidatePaths(skillId: string): string[] {
|
|
87
|
+
return [
|
|
88
|
+
`skills/${skillId}/SKILL.md`,
|
|
89
|
+
`skills/.curated/${skillId}/SKILL.md`,
|
|
90
|
+
`skills/.experimental/${skillId}/SKILL.md`,
|
|
91
|
+
`${skillId}/SKILL.md`,
|
|
92
|
+
`SKILL.md`,
|
|
93
|
+
]
|
|
94
|
+
}
|
|
141
95
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Fetches the SKILL.md content for a skill by trying multiple path candidates
|
|
98
|
+
* against the GitHub raw content API.
|
|
99
|
+
*/
|
|
100
|
+
export async function fetchSkillContent(
|
|
101
|
+
source: string,
|
|
102
|
+
skillId: string
|
|
103
|
+
): Promise<string | null> {
|
|
104
|
+
const branch = await getDefaultBranch(source)
|
|
105
|
+
const paths = candidatePaths(skillId)
|
|
106
|
+
|
|
107
|
+
for (const p of paths) {
|
|
108
|
+
const url = `${GITHUB_RAW_BASE}/${source}/${branch}/${p}`
|
|
109
|
+
const response = await fetch(url)
|
|
110
|
+
if (response.ok) {
|
|
111
|
+
return response.text()
|
|
112
|
+
}
|
|
149
113
|
}
|
|
150
|
-
|
|
114
|
+
|
|
115
|
+
return null
|
|
151
116
|
}
|
|
117
|
+
|
|
118
|
+
// ---------- SkillsGate API (auth, favorites, private skills) ----------
|
|
119
|
+
|
|
120
|
+
export { SKILLSGATE_API_BASE }
|
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from "react"
|
|
2
2
|
import { useStore, useDispatch } from "../store/context.js"
|
|
3
|
-
import
|
|
3
|
+
import { SKILLSGATE_API_BASE } from "./api-client.js"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Shape returned by the favorites API endpoint.
|
|
7
|
+
*/
|
|
8
|
+
export interface FavoriteSkill {
|
|
9
|
+
id: string
|
|
10
|
+
name: string
|
|
11
|
+
description: string
|
|
12
|
+
summary?: string
|
|
13
|
+
categories: string[]
|
|
14
|
+
capabilities?: string[]
|
|
15
|
+
keywords?: string[]
|
|
16
|
+
githubUrl?: string
|
|
17
|
+
githubStars?: number | null
|
|
18
|
+
installCommand?: string | null
|
|
19
|
+
slug?: string
|
|
8
20
|
favoriteId?: string
|
|
9
21
|
}
|
|
10
22
|
|
|
23
|
+
const API_BASE = SKILLSGATE_API_BASE
|
|
24
|
+
|
|
11
25
|
interface UseFavoritesResult {
|
|
12
26
|
favorites: FavoriteSkill[]
|
|
13
27
|
loading: boolean
|