@skillsgate/tui 0.1.12 → 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.
@@ -1,11 +1,13 @@
1
1
  import { useCallback } from "react"
2
+ import { exec as execCb } from "node:child_process"
3
+ import { promisify } from "node:util"
2
4
  import { useStore, useDispatch } from "../store/context.js"
3
5
  import { useDb } from "../db/context.js"
4
- import type { EnrichedSkill } from "../store/types.js"
6
+ import type { EnrichedSkill, Action } from "../store/types.js"
5
7
 
6
8
  // CLI core imports -- these share the same Bun runtime
7
9
  import { parseSource } from "../../../cli/src/core/source-parser.js"
8
- import { cloneRepo, cleanupTempDir, fetchTreeSha } from "../../../cli/src/core/git.js"
10
+ import { cleanupTempDir, fetchTreeSha } from "../../../cli/src/core/git.js"
9
11
  import { discoverSkills } from "../../../cli/src/core/skill-discovery.js"
10
12
  import {
11
13
  installSkillForAgent,
@@ -16,12 +18,11 @@ import {
16
18
  import {
17
19
  addSkillToLock,
18
20
  removeSkillFromLock,
19
- readSkillLock,
20
21
  } from "../../../cli/src/core/skill-lock.js"
21
22
  import { agents, detectInstalledAgents } from "../../../cli/src/core/agents.js"
22
23
  import { downloadSkill } from "../../../cli/src/core/skillsgate-client.js"
23
24
  import { getToken } from "../../../cli/src/utils/auth-store.js"
24
- import type { Skill, AgentConfig, ParsedSource } from "../../../cli/src/types.js"
25
+ import type { Skill, AgentConfig } from "../../../cli/src/types.js"
25
26
 
26
27
  interface UseSkillActionsResult {
27
28
  installSkill: (skill: EnrichedSkill) => Promise<void>
@@ -40,7 +41,9 @@ export function useSkillActions(): UseSkillActionsResult {
40
41
  const { settings } = useDb()
41
42
 
42
43
  /**
43
- * Install a skill from its source (GitHub URL, SkillsGate slug, or install command).
44
+ * Install a skill from its source.
45
+ * For public skills (with a source in owner/repo format), runs `npx skills add`.
46
+ * For private skills, uses the existing download flow.
44
47
  */
45
48
  const installSkill = useCallback(async (skill: EnrichedSkill) => {
46
49
  dispatch({
@@ -61,7 +64,16 @@ export function useSkillActions(): UseSkillActionsResult {
61
64
 
62
65
  const source = parseSource(sourceStr)
63
66
 
64
- // Detect installed agents to install to
67
+ // Public skills (owner/repo format): use `npx skills add`
68
+ if (source.type === "github" || isOwnerRepoFormat(sourceStr)) {
69
+ const repo = source.type === "github"
70
+ ? `${source.owner}/${source.repo}`
71
+ : sourceStr
72
+ await runSkillsAdd(repo, dispatch)
73
+ return
74
+ }
75
+
76
+ // Private skills: use the existing download flow
65
77
  const installedAgents = await detectInstalledAgents()
66
78
  if (installedAgents.length === 0) {
67
79
  dispatch({
@@ -88,19 +100,16 @@ export function useSkillActions(): UseSkillActionsResult {
88
100
 
89
101
  let tmpDir: string
90
102
  if (source.type === "skillsgate") {
91
- // Download from SkillsGate API
103
+ // Download from private API
92
104
  const token = await getToken()
93
105
  tmpDir = await downloadSkill(source.username!, source.slug!, token)
94
- } else if (source.type === "github") {
95
- // Clone from GitHub
96
- tmpDir = await cloneRepo(source)
97
106
  } else {
98
107
  // Local path -- use directly
99
108
  tmpDir = source.localPath!
100
109
  }
101
110
 
102
111
  try {
103
- // Discover skills in the cloned/downloaded directory
112
+ // Discover skills in the downloaded directory
104
113
  const skills = await discoverSkills(tmpDir, source.subpath)
105
114
 
106
115
  if (skills.length === 0) {
@@ -320,9 +329,49 @@ export function useSkillActions(): UseSkillActionsResult {
320
329
 
321
330
  // ---------- Helpers ----------
322
331
 
332
+ const execAsync = promisify(execCb)
333
+
334
+ /**
335
+ * Checks if a string matches the owner/repo format (e.g. "vercel/skills").
336
+ */
337
+ function isOwnerRepoFormat(str: string): boolean {
338
+ return /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(str)
339
+ }
340
+
341
+ /**
342
+ * Runs `npx skills add <source> --all -y` as a child process to install
343
+ * all skills from a public repository.
344
+ */
345
+ async function runSkillsAdd(
346
+ source: string,
347
+ dispatch: (action: Action) => void
348
+ ): Promise<void> {
349
+ try {
350
+ await execAsync(
351
+ `npx skills add ${source} --all -y`,
352
+ { timeout: 60_000 }
353
+ )
354
+
355
+ dispatch({ type: "REFRESH_SKILLS" })
356
+ dispatch({
357
+ type: "SHOW_NOTIFICATION",
358
+ notification: {
359
+ type: "success",
360
+ message: `Installed skills from ${source}`,
361
+ },
362
+ })
363
+ } catch (err) {
364
+ const msg = err instanceof Error ? err.message : String(err)
365
+ dispatch({
366
+ type: "SHOW_NOTIFICATION",
367
+ notification: { type: "error", message: `Install failed: ${msg}` },
368
+ })
369
+ }
370
+ }
371
+
323
372
  /**
324
373
  * Resolves the source string for a skill from its metadata.
325
- * Checks: lock.source, metadata.githubUrl, metadata.installCommand
374
+ * Checks: lock.source, metadata.source, metadata.githubUrl, metadata.installCommand
326
375
  */
327
376
  function resolveSource(skill: EnrichedSkill): string | null {
328
377
  // From lock file entry
@@ -330,17 +379,20 @@ function resolveSource(skill: EnrichedSkill): string | null {
330
379
  return skill.lock.source
331
380
  }
332
381
 
333
- // From metadata (catalog skills)
382
+ // From metadata (catalog skills -- owner/repo format)
334
383
  const meta = skill.metadata
384
+ if (meta?.source && typeof meta.source === "string") {
385
+ return meta.source
386
+ }
387
+
335
388
  if (meta?.githubUrl && typeof meta.githubUrl === "string") {
336
389
  return meta.githubUrl
337
390
  }
338
391
 
339
- // From install command (e.g. "skillsgate add @user/slug")
392
+ // From install command (e.g. "skills add <source>")
340
393
  if (meta?.installCommand && typeof meta.installCommand === "string") {
341
394
  const cmd = meta.installCommand as string
342
- // Extract the source from "skillsgate add <source>" or "skillsgate install <source>"
343
- const match = cmd.match(/skillsgate\s+(?:add|install)\s+(.+)/)
395
+ const match = cmd.match(/skills?\s+(?:add|install)\s+(.+)/)
344
396
  if (match) {
345
397
  return match[1].trim()
346
398
  }
@@ -4,31 +4,22 @@ import { useStore, useDispatch } from "../store/context.js"
4
4
  import { useSearch } from "../data/use-search.js"
5
5
  import { useSkillActions } from "../data/use-skill-actions.js"
6
6
  import { ConfirmDialog } from "../components/confirm-dialog.js"
7
- import type { CatalogSkill, SearchMode } from "../data/api-client.js"
7
+ import type { CatalogSkill } from "../data/api-client.js"
8
8
  import { colors } from "../utils/colors.js"
9
9
 
10
10
  /**
11
- * Discover view: two-column layout with search mode toggle.
12
- * LEFT - Search input + mode toggle + results list (40%)
11
+ * Discover view: two-column layout with search results.
12
+ * LEFT - Search input + results list (40%)
13
13
  * RIGHT - Selected result detail (flexGrow)
14
- *
15
- * Search modes:
16
- * - Keyword (public, no auth): ILIKE pattern matching, unlimited
17
- * - Semantic (auth required): AI-powered vector search, 30/day limit
18
- * Toggle with 'm' key.
19
14
  */
20
15
  export function DiscoverView() {
21
16
  const state = useStore()
22
17
  const dispatch = useDispatch()
23
18
  const [query, setQuery] = useState("")
24
- const [searchMode, setSearchMode] = useState<SearchMode>("keyword")
25
19
  const [selectedIndex, setSelectedIndex] = useState(0)
26
20
  const [installTarget, setInstallTarget] = useState<CatalogSkill | null>(null)
27
21
  const [previewSkill, setPreviewSkill] = useState<CatalogSkill | null>(null)
28
22
 
29
- const token = state.auth?.token ?? null
30
- const isAuthenticated = !!state.auth
31
-
32
23
  // Auto-focus search input when Discover view mounts
33
24
  useEffect(() => {
34
25
  if (state.activeView === "discover") {
@@ -36,8 +27,8 @@ export function DiscoverView() {
36
27
  }
37
28
  }, [state.activeView])
38
29
 
39
- const { results, loading, error, total, hasMore, loadMore, remainingSearches } =
40
- useSearch(query, searchMode, token)
30
+ const { results, loading, error, total, hasMore, loadMore } =
31
+ useSearch(query)
41
32
  const { installSkill } = useSkillActions()
42
33
 
43
34
  // Update preview when selection changes
@@ -94,24 +85,6 @@ export function DiscoverView() {
94
85
  setInstallTarget(results[selectedIndex])
95
86
  return
96
87
  }
97
-
98
- // m to toggle search mode
99
- if (key.name === "m") {
100
- if (searchMode === "keyword") {
101
- if (!isAuthenticated) {
102
- dispatch({
103
- type: "SHOW_NOTIFICATION",
104
- notification: { type: "info", message: "Sign in to use AI search (press l)" },
105
- })
106
- return
107
- }
108
- setSearchMode("semantic")
109
- } else {
110
- setSearchMode("keyword")
111
- }
112
- setSelectedIndex(0)
113
- return
114
- }
115
88
  })
116
89
 
117
90
  // Confirm dialog for install
@@ -142,17 +115,11 @@ export function DiscoverView() {
142
115
  paddingLeft: 1,
143
116
  paddingRight: 1,
144
117
  }}
145
- title={state.focusedPane === "search"
146
- ? (searchMode === "semantic" ? "AI Search" : "Keyword Search")
147
- : "/ to search"}
118
+ title={state.focusedPane === "search" ? "Search" : "/ to search"}
148
119
  >
149
120
  {state.focusedPane === "search" ? (
150
121
  <input
151
- placeholder={
152
- searchMode === "semantic"
153
- ? 'AI search -- try "audit website performance" (Enter to search)'
154
- : "Search by keyword... (Enter to search)"
155
- }
122
+ placeholder="Search skills... (Enter to search)"
156
123
  focused={state.activeView === "discover" && !state.showHelp}
157
124
  onSubmit={((value: string) => {
158
125
  setQuery(value)
@@ -164,7 +131,7 @@ export function DiscoverView() {
164
131
  )}
165
132
  </box>
166
133
 
167
- {/* Status line: mode toggle + results + remaining */}
134
+ {/* Status line */}
168
135
  <box
169
136
  style={{
170
137
  height: 1,
@@ -174,37 +141,16 @@ export function DiscoverView() {
174
141
  flexDirection: "row",
175
142
  }}
176
143
  >
177
- {/* Mode toggle: show both options, highlight active */}
178
- <text fg={searchMode === "keyword" ? colors.primary : colors.textDim}>
179
- {searchMode === "keyword" ? "[Keyword]" : " Keyword "}
180
- </text>
181
- <text fg={colors.textDim}>{" | "}</text>
182
- <text fg={searchMode === "semantic" ? colors.warning : colors.textDim}>
183
- {searchMode === "semantic" ? "[AI Search]" : (isAuthenticated ? " AI Search " : " AI Search (login) ")}
184
- </text>
185
- <text fg={colors.textDim}>{" m=switch "}</text>
186
-
187
144
  {/* Results info */}
188
145
  <text fg={colors.textDim}>
189
146
  {loading
190
147
  ? "Loading..."
191
148
  : error
192
- ? error === "AUTH_EXPIRED"
193
- ? "Session expired -- press l to re-login"
194
- : error === "RATE_LIMIT"
195
- ? "Daily limit reached -- switch to keyword (m)"
196
- : `Error: ${error}`
149
+ ? `Error: ${error}`
197
150
  : query.trim()
198
151
  ? `${results.length} result${results.length !== 1 ? "s" : ""}`
199
152
  : `${results.length}/${total} skills`}
200
153
  </text>
201
-
202
- {/* Remaining searches (semantic only) */}
203
- {searchMode === "semantic" && remainingSearches !== null ? (
204
- <text fg={remainingSearches <= 5 ? colors.error : colors.textDim}>
205
- {` ${remainingSearches} left today`}
206
- </text>
207
- ) : null}
208
154
  </box>
209
155
 
210
156
  {/* Two-column content: results list | detail */}
@@ -250,7 +196,7 @@ export function DiscoverView() {
250
196
  >
251
197
  {results.map((skill, i) => (
252
198
  <box
253
- key={skill.id ?? `${skill.slug}-${i}`}
199
+ key={skill.id ?? `${skill.skillId}-${i}`}
254
200
  style={{
255
201
  width: "100%",
256
202
  paddingLeft: 1,
@@ -297,11 +243,6 @@ interface DiscoverDetailPanelProps {
297
243
  }
298
244
 
299
245
  function DiscoverDetailPanel({ skill }: DiscoverDetailPanelProps) {
300
- const description = skill.summary || skill.description || ""
301
- const categories = skill.categories?.join(", ") ?? ""
302
- const capabilities = skill.capabilities?.join(", ") ?? ""
303
- const keywords = skill.keywords?.join(", ") ?? ""
304
-
305
246
  return (
306
247
  <scrollbox
307
248
  focused={false}
@@ -325,58 +266,25 @@ function DiscoverDetailPanel({ skill }: DiscoverDetailPanelProps) {
325
266
  <strong>{skill.name}</strong>
326
267
  </text>
327
268
 
328
- {/* Description */}
329
- <text fg={colors.text}>{description}</text>
330
- <text>{" "}</text>
331
-
332
- {/* Categories */}
333
- {categories ? (
334
- <box style={{ flexDirection: "row", height: 1 }}>
335
- <text fg={colors.textDim}>Categories: </text>
336
- <text fg={colors.secondary}>{categories}</text>
337
- </box>
338
- ) : null}
339
-
340
- {/* Capabilities */}
341
- {capabilities ? (
342
- <box style={{ flexDirection: "row", height: 1 }}>
343
- <text fg={colors.textDim}>Capabilities: </text>
344
- <text fg={colors.secondary}>{capabilities}</text>
345
- </box>
346
- ) : null}
347
-
348
- {/* Keywords */}
349
- {keywords ? (
350
- <box style={{ flexDirection: "row", height: 1 }}>
351
- <text fg={colors.textDim}>Keywords: </text>
352
- <text fg={colors.secondary}>{keywords}</text>
353
- </box>
354
- ) : null}
355
-
356
- {/* Score (semantic search only) */}
357
- {skill.score ? (
358
- <box style={{ flexDirection: "row", height: 1 }}>
359
- <text fg={colors.textDim}>Score: </text>
360
- <text fg={colors.success}>{skill.score.toFixed(3)}</text>
361
- </box>
362
- ) : null}
269
+ {/* Skill ID */}
270
+ <box style={{ flexDirection: "row", height: 1 }}>
271
+ <text fg={colors.textDim}>ID: </text>
272
+ <text fg={colors.text}>{skill.skillId}</text>
273
+ </box>
363
274
 
364
- {/* GitHub URL */}
365
- {skill.githubUrl ? (
275
+ {/* Source (owner/repo) */}
276
+ {skill.source ? (
366
277
  <box style={{ flexDirection: "row", height: 1 }}>
367
- <text fg={colors.textDim}>GitHub: </text>
368
- <text fg={colors.primary}>{skill.githubUrl}</text>
278
+ <text fg={colors.textDim}>Source: </text>
279
+ <text fg={colors.primary}>{skill.source}</text>
369
280
  </box>
370
281
  ) : null}
371
282
 
372
- {/* Install command */}
373
- {skill.installCommand ? (
374
- <>
375
- <text>{" "}</text>
376
- <text fg={colors.textDim}>Install:</text>
377
- <text fg={colors.success}> {skill.installCommand}</text>
378
- </>
379
- ) : null}
283
+ {/* Installs */}
284
+ <box style={{ flexDirection: "row", height: 1 }}>
285
+ <text fg={colors.textDim}>Installs: </text>
286
+ <text fg={colors.success}>{skill.installs.toLocaleString()}</text>
287
+ </box>
380
288
 
381
289
  <text>{" "}</text>
382
290
  <text fg={colors.textDim}>v=full detail i=install Tab=switch pane</text>
@@ -392,9 +300,11 @@ function DiscoverDetailPanel({ skill }: DiscoverDetailPanelProps) {
392
300
  * Since catalog skills don't have a local file, we provide a placeholder.
393
301
  */
394
302
  function catalogSkillToEnriched(skill: CatalogSkill): import("../store/types.js").EnrichedSkill {
303
+ const githubUrl = skill.source ? `https://github.com/${skill.source}` : undefined
304
+
395
305
  return {
396
306
  name: skill.name,
397
- description: skill.summary || skill.description || "",
307
+ description: "",
398
308
  filePath: "", // No local file for catalog items
399
309
  canonicalPath: "",
400
310
  agents: [],
@@ -403,18 +313,15 @@ function catalogSkillToEnriched(skill: CatalogSkill): import("../store/types.js"
403
313
  hasSupportingFiles: false,
404
314
  supportingFiles: [],
405
315
  metadata: {
406
- categories: skill.categories,
407
- capabilities: skill.capabilities,
408
- keywords: skill.keywords,
409
- githubUrl: skill.githubUrl,
410
- installCommand: skill.installCommand,
411
- urlPath: skill.urlPath,
316
+ source: skill.source,
317
+ skillId: skill.skillId,
318
+ installs: skill.installs,
412
319
  },
413
- lock: skill.githubUrl
320
+ lock: skill.source
414
321
  ? {
415
- source: skill.githubUrl,
322
+ source: skill.source,
416
323
  sourceType: "github" as const,
417
- originalUrl: skill.githubUrl,
324
+ originalUrl: githubUrl ?? "",
418
325
  skillFolderHash: "",
419
326
  installedAt: "",
420
327
  updatedAt: "",