skillhub/cli/src/commands/list.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

57 lines
1.8 KiB
TypeScript

import { stat } from 'node:fs/promises'
import { ConfigStore } from '../stores/config-store'
import { InventoryStore } from '../stores/inventory-store'
import { resolveRegistry } from '../services/registry-service'
export interface ListCommandOptions {
agent?: string[] | undefined
dir?: string | undefined
registry?: string | undefined
json?: boolean | undefined
}
export async function listCommand(options: ListCommandOptions): Promise<string> {
const configStore = new ConfigStore()
const registry = resolveRegistry(options, process.env, await configStore.read())
const store = new InventoryStore()
const inventory = await store.read()
// Flatten targets
type FlatTarget = { namespace: string; slug: string; version: string; agent: string; installDir: string; installedAt: string; status: string }
const flat: FlatTarget[] = []
for (const item of inventory.items) {
if (item.registry !== registry) continue
for (const target of item.targets) {
if (options.agent?.length && !options.agent.includes(target.agent)) continue
if (options.dir && !target.installDir.startsWith(options.dir)) continue
let status = 'ok'
try {
await stat(target.installDir)
} catch {
status = 'missing'
}
flat.push({
namespace: item.namespace,
slug: item.slug,
version: item.version,
agent: target.agent,
installDir: target.installDir,
installedAt: target.installedAt,
status
})
}
}
if (options.json) {
return JSON.stringify({ ok: true, items: flat })
}
if (flat.length === 0) return 'No skills installed.'
return flat.map(t =>
`${t.namespace}/${t.slug}@${t.version} ${t.agent} ${t.installDir} ${t.installedAt} ${t.status}`
).join('\n')
}