@skillsgate/tui 0.1.10 → 0.1.13
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/agent-filter.tsx +2 -2
- package/src/components/help-overlay.tsx +4 -4
- package/src/components/layout.tsx +15 -40
- 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 +178 -3
- package/src/data/use-search.ts +14 -31
- package/src/data/use-skill-actions.ts +87 -18
- package/src/db/skills.ts +24 -0
- package/src/db/ssh.ts +45 -1
- package/src/store/types.ts +8 -0
- package/src/types/bun-sqlite.d.ts +12 -0
- package/src/views/add-server.tsx +3 -3
- package/src/views/discover.tsx +42 -130
- package/src/views/favorites.tsx +10 -349
- package/src/views/home.tsx +531 -9
- package/src/views/login.tsx +1 -1
- package/src/views/server-skills.tsx +61 -4
- package/src/views/servers.tsx +2 -2
- package/src/views/settings.tsx +0 -6
- package/src/views/skill-detail.tsx +17 -16
- 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.13",
|
|
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.10",
|
|
20
|
-
"@skillsgate/tui-darwin-x64": "0.1.10",
|
|
21
|
-
"@skillsgate/tui-linux-x64": "0.1.10",
|
|
22
|
-
"@skillsgate/tui-linux-arm64": "0.1.10",
|
|
23
|
-
"@skillsgate/tui-win32-x64": "0.1.10"
|
|
24
|
-
},
|
|
25
18
|
"peerDependencies": {
|
|
26
19
|
"react": "19.1.5"
|
|
27
20
|
},
|
|
@@ -58,11 +58,11 @@ export function AgentFilter() {
|
|
|
58
58
|
style={{
|
|
59
59
|
flexDirection: "column",
|
|
60
60
|
width: 22,
|
|
61
|
-
|
|
61
|
+
border: true,
|
|
62
62
|
borderColor: isFocused ? colors.primary : colors.border,
|
|
63
63
|
backgroundColor: colors.bg,
|
|
64
64
|
paddingTop: 0,
|
|
65
|
-
}}
|
|
65
|
+
} as any}
|
|
66
66
|
>
|
|
67
67
|
{/* Library section header */}
|
|
68
68
|
<box style={{ paddingLeft: 1, height: 1, backgroundColor: colors.bgAlt }}>
|
|
@@ -10,20 +10,20 @@ const SHORTCUTS_LEFT: ShortcutEntry[] = [
|
|
|
10
10
|
{ key: "g", description: "Jump to first item" },
|
|
11
11
|
{ key: "G", description: "Jump to last item" },
|
|
12
12
|
{ key: "v", description: "View skill detail" },
|
|
13
|
+
{ key: "n", description: "Create local skill (home)" },
|
|
14
|
+
{ key: "c", description: "Manage collections (home)" },
|
|
13
15
|
{ key: "/", description: "Focus search input" },
|
|
14
16
|
{ key: "Tab", description: "Cycle focus: agents > search > list" },
|
|
15
17
|
{ key: "Esc", description: "Clear search / go back" },
|
|
16
18
|
]
|
|
17
19
|
|
|
18
20
|
const SHORTCUTS_RIGHT: ShortcutEntry[] = [
|
|
19
|
-
{ key: "1/2/3
|
|
21
|
+
{ key: "1/2/3", description: "Switch tabs" },
|
|
20
22
|
{ key: "s", description: "Open settings" },
|
|
21
23
|
{ key: "r", description: "Refresh installed skills" },
|
|
22
|
-
{ key: "l", description: "Login / auth status" },
|
|
23
24
|
{ key: "i", description: "Install selected skill" },
|
|
24
25
|
{ key: "d", description: "Remove selected skill" },
|
|
25
|
-
{ key: "
|
|
26
|
-
{ key: "m", description: "Toggle keyword/AI search" },
|
|
26
|
+
{ key: "", description: "" },
|
|
27
27
|
{ key: "?", description: "Toggle this help" },
|
|
28
28
|
{ key: "Ctrl+Q", description: "Quit" },
|
|
29
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" })
|
|
@@ -95,21 +78,21 @@ export function Layout() {
|
|
|
95
78
|
if (state.showHelp) return
|
|
96
79
|
|
|
97
80
|
// Tab switching (only when not in detail/form views)
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
||
|
|
81
|
+
const activeView = state.activeView as string
|
|
82
|
+
const inFormView = activeView === "detail" || activeView === "add-server"
|
|
83
|
+
|| activeView === "edit-server" || activeView === "settings"
|
|
84
|
+
|| activeView === "server-skills"
|
|
101
85
|
if (!inFormView) {
|
|
102
86
|
if (key.name === "1") dispatch({ type: "NAVIGATE", view: "home" })
|
|
103
87
|
if (key.name === "2") dispatch({ type: "NAVIGATE", view: "discover" })
|
|
104
|
-
if (key.name === "3")
|
|
105
|
-
if (key.name === "4") {
|
|
88
|
+
if (key.name === "3") {
|
|
106
89
|
setServerCount(servers.list().length)
|
|
107
90
|
dispatch({ type: "NAVIGATE", view: "servers" })
|
|
108
91
|
}
|
|
109
92
|
}
|
|
110
93
|
|
|
111
|
-
// "s" to open settings (only from home/
|
|
112
|
-
if (key.name === "s" && state.focusedPane !== "search"
|
|
94
|
+
// "s" to open settings (only from home/servers views when not in search)
|
|
95
|
+
if (key.name === "s" && (state.focusedPane as string) !== "search"
|
|
113
96
|
&& state.activeView !== "discover" && state.activeView !== "detail"
|
|
114
97
|
&& !inFormView) {
|
|
115
98
|
dispatch({ type: "NAVIGATE", view: "settings" })
|
|
@@ -139,26 +122,20 @@ export function Layout() {
|
|
|
139
122
|
if (state.installedFilter) {
|
|
140
123
|
dispatch({ type: "SET_INSTALLED_FILTER", filter: "" })
|
|
141
124
|
}
|
|
142
|
-
if (state.focusedPane === "search") {
|
|
125
|
+
if ((state.focusedPane as string) === "search") {
|
|
143
126
|
dispatch({ type: "SET_FOCUSED_PANE", pane: "list" })
|
|
144
127
|
}
|
|
145
128
|
return
|
|
146
129
|
}
|
|
147
130
|
|
|
148
|
-
// "
|
|
149
|
-
if (key.name === "
|
|
150
|
-
dispatch({ type: "NAVIGATE", view: "login" })
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// "r" to refresh installed skills (when not typing in search, not on login view)
|
|
155
|
-
if (key.name === "r" && state.focusedPane !== "search" && state.activeView !== "detail" && state.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") {
|
|
156
133
|
dispatch({ type: "REFRESH_SKILLS" })
|
|
157
134
|
return
|
|
158
135
|
}
|
|
159
136
|
})
|
|
160
137
|
|
|
161
|
-
const TAB_OPTIONS = getTabOptions(
|
|
138
|
+
const TAB_OPTIONS = getTabOptions(serverCount)
|
|
162
139
|
|
|
163
140
|
const activeTabIndex = TAB_OPTIONS.findIndex(
|
|
164
141
|
(t) => t.value === state.activeView
|
|
@@ -186,7 +163,7 @@ export function Layout() {
|
|
|
186
163
|
}}
|
|
187
164
|
>
|
|
188
165
|
<text fg={colors.primary}>
|
|
189
|
-
<strong>SkillsGate TUI</strong> <span fg={colors.textDim}>v0.1.
|
|
166
|
+
<strong>SkillsGate TUI</strong> <span fg={colors.textDim}>v0.1.12</span>
|
|
190
167
|
</text>
|
|
191
168
|
</box>
|
|
192
169
|
|
|
@@ -194,7 +171,7 @@ export function Layout() {
|
|
|
194
171
|
<tab-select
|
|
195
172
|
options={TAB_OPTIONS}
|
|
196
173
|
focused={state.activeView !== "detail" && !state.showHelp}
|
|
197
|
-
selectedIndex
|
|
174
|
+
{...({ selectedIndex: activeTabIndex >= 0 ? activeTabIndex : 0 } as any)}
|
|
198
175
|
selectedBackgroundColor={colors.tabActive}
|
|
199
176
|
selectedTextColor={colors.tabText}
|
|
200
177
|
textColor={colors.textDim}
|
|
@@ -216,7 +193,6 @@ export function Layout() {
|
|
|
216
193
|
<>
|
|
217
194
|
{state.activeView === "home" && <HomeView />}
|
|
218
195
|
{state.activeView === "discover" && <DiscoverView />}
|
|
219
|
-
{state.activeView === "favorites" && <FavoritesView />}
|
|
220
196
|
{state.activeView === "servers" && <ServersView onServerCountChange={setServerCount} />}
|
|
221
197
|
{(state.activeView === "add-server" || state.activeView === "edit-server") && (
|
|
222
198
|
<AddServerView
|
|
@@ -228,7 +204,6 @@ export function Layout() {
|
|
|
228
204
|
<ServerSkillsView serverId={state.selectedServerId} />
|
|
229
205
|
)}
|
|
230
206
|
{state.activeView === "settings" && <SettingsView />}
|
|
231
|
-
{state.activeView === "login" && <LoginView />}
|
|
232
207
|
{state.activeView === "detail" && state.selectedSkill && (
|
|
233
208
|
<SkillDetailView />
|
|
234
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
|