Skip to content

Commit dd33662

Browse files
Merge pull request #87 from laststance/remove-delete-btn
fix(marketplace): remove uninstall button + harden migration & CLI parser (v0.11.2)
2 parents a34b56a + 9e95fcf commit dd33662

30 files changed

Lines changed: 609 additions & 259 deletions

SPEC.md

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ GUI wrapper for `npx skills` CLI commands:
9494
- [x] Install skills via `npx skills add <repo>`
9595
- [x] Select target agents for installation
9696
- [x] Installation progress tracking
97-
- [ ] Remove skills via `npx skills remove <name>`
9897

9998
### Symlink Status
10099

@@ -146,14 +145,14 @@ Each skill displays:
146145

147146
### Actions
148147

149-
| Action | Status | Notes |
150-
| ---------------------- | ---------- | -------------------------------- |
151-
| View skill details | ✅ Done | - |
152-
| View symlink status | ✅ Done | - |
153-
| Search skills | ✅ Done | Marketplace tab |
154-
| Install skill | ✅ Done | With agent selection |
155-
| Remove skill | 🚧 Planned | UI exists, backend not connected |
156-
| Repair broken symlinks | 🚧 Planned | - |
148+
| Action | Status | Notes |
149+
| ---------------------- | ---------- | ------------------------------------------------------ |
150+
| View skill details | ✅ Done | - |
151+
| View symlink status | ✅ Done | - |
152+
| Search skills | ✅ Done | Marketplace tab |
153+
| Install skill | ✅ Done | With agent selection |
154+
| Uninstall skill | ✅ CLI | `npx skills remove <name> --global` (no in-app button) |
155+
| Repair broken symlinks | 🚧 Planned | - |
157156

158157
## Tech Stack
159158

@@ -357,7 +356,6 @@ listenerMiddleware.startListening({
357356
// Skills CLI (Marketplace)
358357
'skills:cli:search'Promise<SkillSearchResult[]>
359358
'skills:cli:install'Promise<CliCommandResult>
360-
'skills:cli:remove'Promise<CliCommandResult>
361359
'skills:cli:cancel'void
362360
'skills:cli:progress' → (MainRenderer event)
363361
```
@@ -442,12 +440,7 @@ interface InstallProgress {
442440
percent?: number
443441
}
444442

445-
type MarketplaceStatus =
446-
| 'idle'
447-
| 'searching'
448-
| 'installing'
449-
| 'removing'
450-
| 'error'
443+
type MarketplaceStatus = 'idle' | 'searching' | 'installing' | 'error'
451444
```
452445
453446
## Redux State
@@ -488,7 +481,6 @@ interface MarketplaceState {
488481
searchResults: SkillSearchResult[]
489482
selectedSkill: SkillSearchResult | null
490483
installProgress: InstallProgress | null
491-
skillToRemove: string | null
492484
error: string | null
493485
}
494486
```
@@ -543,8 +535,7 @@ skills-desktop/
543535
│ │ │ │ ├── SkillsMarketplace.tsx
544536
│ │ │ │ ├── MarketplaceSearch.tsx
545537
│ │ │ │ ├── SkillRowMarketplace.tsx
546-
│ │ │ │ ├── InstallModal.tsx
547-
│ │ │ │ └── RemoveDialog.tsx
538+
│ │ │ │ └── InstallModal.tsx
548539
│ │ │ └── ui/ # shadcn/ui components
549540
│ │ ├── views/
550541
│ │ ├── hooks/
@@ -659,11 +650,10 @@ APPLE_KEYCHAIN_PROFILE=skills-desktop pnpm build:mac
659650

660651
The Marketplace feature wraps `npx skills@<SKILLS_CLI_VERSION>` CLI commands (version pinned in `src/shared/constants.ts`):
661652

662-
| Feature | CLI Command | Options |
663-
| ------- | ----------------------------------------------- | -------------------------------- |
664-
| Search | `npx skills@<SKILLS_CLI_VERSION> find <query>` | - |
665-
| Install | `npx skills@<SKILLS_CLI_VERSION> add <repo>` | `-y`, `-g`, `--agent`, `--skill` |
666-
| Remove | `npx skills@<SKILLS_CLI_VERSION> remove <name>` | - |
653+
| Feature | CLI Command | Options |
654+
| ------- | ---------------------------------------------- | -------------------------------- |
655+
| Search | `npx skills@<SKILLS_CLI_VERSION> find <query>` | - |
656+
| Install | `npx skills@<SKILLS_CLI_VERSION> add <repo>` | `-y`, `-g`, `--agent`, `--skill` |
667657

668658
**CLI Output Parsing:**
669659

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "skills-desktop",
3-
"version": "0.11.1",
3+
"version": "0.11.2",
44
"description": "Visualize installed Skills and symlink status across AI agents",
55
"main": "./out/main/index.mjs",
66
"author": "Laststance.io",

src/main/ipc/ipc-schemas.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ export const IPC_ARG_SCHEMAS: Partial<Record<IpcInvokeChannel, z.ZodTuple>> = {
6565

6666
// CLI operations
6767
'skills:cli:search': z.tuple([z.string()]),
68-
'skills:cli:remove': z.tuple([nonEmptyString]),
6968
'skills:cli:install': z.tuple([
7069
z.object({
7170
repo: nonEmptyString,

src/main/ipc/skillsCli.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@ export function registerSkillsCliHandlers(): void {
3232
}
3333
})
3434

35-
typedHandle(IPC_CHANNELS.SKILLS_CLI_REMOVE, async (_, skillName) => {
36-
return skillsCliService.remove(skillName)
37-
})
38-
3935
typedHandle(IPC_CHANNELS.SKILLS_CLI_CANCEL, () => {
4036
skillsCliService.cancel()
4137
})

src/main/services/leaderboardService.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { repositoryId } from '../../shared/types'
22
import type { RankingFilter, SkillSearchResult } from '../../shared/types'
3+
import { REPO_PATTERN, SKILL_NAME_PATTERN } from '../utils/skillIdentifiers'
34

45
/** Maps ranking filter to skills.sh URL */
56
const LEADERBOARD_URLS: Record<RankingFilter, string> = {
@@ -84,6 +85,13 @@ export function parseLeaderboardHtml(html: string): SkillSearchResult[] {
8485

8586
const name = h3Match[1].trim()
8687

88+
// Defense-in-depth: skills.sh HTML is upstream-controlled. Without this
89+
// whitelist, a malformed `<h3>` could land in the installed-badge
90+
// aria-label/title (`npx skills remove <name> --global`) and become a
91+
// copy-paste hazard. Mirrors `parseSearchOutput`'s CLI-side guard so both
92+
// ingestion paths enforce the same identifier contract.
93+
if (!REPO_PATTERN.test(repo) || !SKILL_NAME_PATTERN.test(name)) continue
94+
8795
// Extract install count: look for numbers with optional K/M/B suffix.
8896
// Excludes rank numbers (preceded by #) and delta numbers (preceded by +/-)
8997
// This appears as standalone text like "731.2K", "20.0K", "927"

src/main/services/skillsCliService.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
CliCommandResult,
1212
InstallProgress,
1313
} from '../../shared/types'
14+
import { REPO_PATTERN, SKILL_NAME_PATTERN } from '../utils/skillIdentifiers'
1415

1516
/**
1617
* Build agent ID to CLI name mapping from AGENT_DEFINITIONS
@@ -93,19 +94,6 @@ class SkillsCliService extends EventEmitter {
9394
return result
9495
}
9596

96-
/**
97-
* Remove a skill using `npx skills remove <name> -g -y`
98-
* @param skillName - Name of the skill to remove
99-
* @returns CLI command result
100-
* @example
101-
* remove('vercel-react-best-practices')
102-
*/
103-
async remove(skillName: string): Promise<CliCommandResult> {
104-
// -g flag for global scope (skills are installed globally)
105-
// -y flag skips interactive confirmation prompts
106-
return this.execCli(['remove', skillName, '-g', '-y'])
107-
}
108-
10997
/**
11098
* Cancel the current CLI operation
11199
*/
@@ -193,6 +181,12 @@ class SkillsCliService extends EventEmitter {
193181
const match = line.match(/^([^@\s]+)@([^\s]+)$/)
194182
if (match) {
195183
const [, repo, name] = match
184+
// Reject anything that isn't a plain npm/GitHub identifier — see
185+
// SKILL_NAME_PATTERN comment. Without this, a malformed CLI line
186+
// could land in aria-labels and copy-paste hints downstream.
187+
if (!REPO_PATTERN.test(repo) || !SKILL_NAME_PATTERN.test(name)) {
188+
continue
189+
}
196190
// Next line should be the URL
197191
const urlLine = lines[i + 1]?.trim()
198192
const urlMatch = urlLine?.match(/^[]\s*(https?:\/\/[^\s]+)$/)

src/main/utils/skillIdentifiers.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Whitelist patterns for skill identifiers parsed from external sources.
3+
* Both `parseSearchOutput` (skills CLI text) and `parseLeaderboardHtml`
4+
* (skills.sh HTML) feed names into UI strings — aria-labels, titles,
5+
* copy-paste hints. A malformed upstream value could carry shell
6+
* metacharacters, whitespace, or `..` traversal-shaped tokens that
7+
* surface as broken UI or, worse, get pasted into a terminal verbatim.
8+
*
9+
* Each segment must:
10+
* - start with an alphanumeric (rejects `.`, `..`, `.git`, `-x`)
11+
* - contain only `[a-zA-Z0-9._-]` thereafter
12+
*
13+
* Sharing one source means both parsers stay in lockstep — bumping
14+
* the rule in one place can't silently leave the other parser laxer.
15+
*/
16+
const SEGMENT = /[a-zA-Z0-9][a-zA-Z0-9._-]*/
17+
export const SKILL_NAME_PATTERN = new RegExp(`^${SEGMENT.source}$`)
18+
export const REPO_PATTERN = new RegExp(
19+
`^${SEGMENT.source}\\/${SEGMENT.source}$`,
20+
)

src/preload/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import type {
1616
RemoveAllFromAgentOptions,
1717
RestoreDeletedSkillOptions,
1818
SearchQuery,
19-
SkillName,
2019
SyncExecuteOptions,
2120
UnlinkFromAgentOptions,
2221
UnlinkManyFromAgentOptions,
@@ -99,8 +98,6 @@ contextBridge.exposeInMainWorld('electron', {
9998
typedInvoke('skills:cli:search', query),
10099
install: async (options: InstallOptions) =>
101100
typedInvoke('skills:cli:install', options),
102-
remove: async (skillName: SkillName) =>
103-
typedInvoke('skills:cli:remove', skillName),
104101
cancel: async () => typedInvoke('skills:cli:cancel'),
105102
onProgress: createIpcListener<InstallProgress>(
106103
IPC_CHANNELS.SKILLS_CLI_PROGRESS,

src/renderer/src/components/dashboard/utils/gridConstants.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@
33
*
44
* The detail panel is narrow (often 300–500px), so a 6-column grid gives enough
55
* resolution for two small widgets side-by-side without forcing each widget
6-
* into a sliver. Row height is intentionally small (48px) so widgets can
7-
* express their height in rounded increments (2=header+counter, 3=small list,
8-
* 4+=scrollable list).
6+
* into a sliver.
7+
*
8+
* Row height is 64px. `WidgetShell` burns a fixed 36px header on every widget
9+
* (see WidgetShell.tsx — `h-9` Tailwind class; keep both sites in sync if the
10+
* header height ever changes). A 48px row left h=2 widgets with only ~68px of
11+
* body — not enough for stat tiles (icon+number+label ≈ 72px) or the health
12+
* widget's label/bar/legend stack (≈90px). Bumping to 64 gives h=2 ≈ 100px
13+
* body (fits the tightest content + breathing room) and keeps h=3/h=4 generous
14+
* for lists and cards.
915
*/
1016
export const GRID_COLS = 6
11-
export const GRID_ROW_HEIGHT_PX = 48
17+
export const GRID_ROW_HEIGHT_PX = 64
1218
export const GRID_MARGIN_PX: readonly [number, number] = [8, 8]
1319
export const GRID_CONTAINER_PADDING_PX: readonly [number, number] = [12, 12]
1420

src/renderer/src/components/dashboard/utils/widgetPresets.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const PAGE_SPECS: readonly PageSpec[] = [
4444
},
4545
{
4646
name: 'Actions',
47-
widgets: [{ type: 'quick-actions', x: 0, y: 0, w: 6, h: 2 }],
47+
widgets: [{ type: 'quick-actions', x: 0, y: 0, w: 6, h: 3 }],
4848
},
4949
{
5050
name: 'Personal',

0 commit comments

Comments
 (0)