skillhub/cli/src/commands/remove.ts
dongmucat 351dddc912 feat(cli): add SkillHub CLI v1 with full command suite
Implement complete CLI tool for SkillHub with 12 commands, 7 backend API endpoints, and comprehensive documentation.

CLI Commands:
- help, version: Basic information
- login, logout, whoami: Authentication management
- search: Discover published skills
- install: Install skills to agent directories (14 Tier 1 agents supported)
- list, remove, doctor: Local skill management
- publish: Publish skill packages
- update: Self-update mechanism

Backend API:
- Add /api/cli/v1 endpoints for auth, search, resolve, download, delete, publish
- Implement CliAuthController and CliSkillController
- Add security policies for CLI routes
- Full test coverage (19 backend tests)

CLI Implementation:
- TypeScript with strict mode, Bun runtime
- Pure JS zip handling (fflate) for cross-platform compatibility
- 15 agent profiles (14 Tier 1 + generic fallback)
- Secure token storage (0600 permissions)
- Path safety validation for remove operations
- Comprehensive error handling (404/403/network distinction)
- 41 unit and integration tests

Documentation:
- CLI user guide (Chinese and English)
- README updates with quick start
- GitHub Actions workflow for cross-platform CI

Quality:
- lint: 0 errors
- typecheck: pass
- test: 41/41 pass
- build: 0.30 MB (target=node for npm/npx compatibility)
2026-04-29 15:37:04 +08:00

75 lines
2.7 KiB
TypeScript

import { ConfigStore } from '../stores/config-store'
import { CredentialsStore } from '../stores/credentials-store'
import { SkillHubClient } from '../clients/skillhub-client'
import { resolveRegistry, resolveToken } from '../services/registry-service'
import { removeLocalSkill } from '../services/remove-service'
import { CliError } from '../shared/errors'
import { EXIT } from '../shared/constants'
export interface RemoveCommandOptions {
agent?: string[] | undefined
all?: boolean | undefined
remote?: boolean | undefined
hard?: boolean | undefined
namespace?: string | undefined
registry?: string | undefined
token?: string | undefined
json?: boolean | undefined
}
export async function removeCommand(slug: string, options: RemoveCommandOptions): Promise<string> {
if (options.all && options.agent?.length) {
throw new CliError('--all cannot be used with --agent', EXIT.usage)
}
if (options.remote && (options.agent?.length || options.all)) {
throw new CliError('--remote cannot be used with --agent or --all', EXIT.usage)
}
const configStore = new ConfigStore()
const credentialsStore = new CredentialsStore()
const registry = resolveRegistry(options, process.env, await configStore.read())
if (options.remote) {
const token = resolveToken(options, process.env, await credentialsStore.getToken(registry))
const namespace = options.namespace ?? 'global'
if (!options.hard && process.stdout.isTTY) {
const prompts = await import('prompts')
const { confirm } = await prompts.default({
type: 'confirm',
name: 'confirm',
message: `Delete remote skill ${namespace}/${slug}?`,
initial: false
})
if (!confirm) {
throw new CliError('remote delete cancelled', EXIT.generic)
}
} else if (!options.hard && !process.stdout.isTTY) {
throw new CliError('non-interactive remote delete requires --hard', EXIT.usage)
}
const client = new SkillHubClient(registry, token)
await client.deleteRemote(namespace, slug)
if (options.json) {
return JSON.stringify({ ok: true, scope: 'remote', action: 'hard-delete', namespace, slug })
}
return `Removed remote skill: ${namespace}/${slug}\nAction: remote-hard-delete`
}
// Local remove
const result = await removeLocalSkill({
registry, slug,
agents: options.agent,
all: options.all
})
if (options.json) {
return JSON.stringify({ ok: true, scope: 'local', removed: result.removed })
}
return result.removed.map(r =>
r.existed
? `Removed ${r.namespace}/${slug} from ${r.dir} (${r.agent})`
: `Cleaned stale record for ${r.namespace}/${slug} at ${r.dir} (${r.agent}, directory already missing)`
).join('\n')
}