mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-27 11:14:59 +00:00
fix(cli): add token auth to search
Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
parent
b56973fb80
commit
9520cf63e0
5 changed files with 170 additions and 4 deletions
|
|
@ -112,6 +112,9 @@ Logout only removes the token for the specified registry, preserving registry co
|
|||
# Keyword search
|
||||
skillhub search pdf
|
||||
|
||||
# Search with a one-off token
|
||||
skillhub search pdf --token sk_xxx
|
||||
|
||||
# List all skills (empty query)
|
||||
skillhub search "" --limit 50
|
||||
|
||||
|
|
@ -333,7 +336,7 @@ Update mechanism:
|
|||
| `skillhub login --token <token> [--registry <url>] [--json]` | Save token and registry configuration |
|
||||
| `skillhub logout [--registry <url>] [--json]` | Remove token for specified registry |
|
||||
| `skillhub whoami [--registry <url>] [--token <token>] [--json]` | Validate current token and display user information |
|
||||
| `skillhub search <query> [--registry <url>] [--limit <n>] [--json]` | Search published skills |
|
||||
| `skillhub search <query> [--registry <url>] [--token <token>] [--limit <n>] [--json]` | Search published skills |
|
||||
| `skillhub install <slug> [--scope <user\|project>] [--namespace <slug>] [--version <v>] [--agent <profile>] [--dir <path>] [--force] [--registry <url>] [--token <token>] [--json]` | Install a skill |
|
||||
| `skillhub list [--agent <profile>] [--dir <path>] [--registry <url>] [--json]` | List installed skills |
|
||||
| `skillhub remove <slug> [--agent <profile>] [--all] [--remote] [--hard] [--namespace <slug>] [--registry <url>] [--token <token>] [--json]` | Remove a skill |
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ export const commands = {
|
|||
},
|
||||
search: {
|
||||
summary: 'Search published skills',
|
||||
usage: 'skillhub search [query] [--limit <n>] [--registry <url>] [--json]',
|
||||
examples: ['skillhub search', 'skillhub search pdf']
|
||||
usage: 'skillhub search [query] [--limit <n>] [--registry <url>] [--token <token>] [--json]',
|
||||
examples: ['skillhub search', 'skillhub search pdf', 'skillhub search pdf --token sk_xxx']
|
||||
},
|
||||
install: {
|
||||
summary: 'Install a skill locally',
|
||||
|
|
|
|||
|
|
@ -223,9 +223,10 @@ cli
|
|||
cli
|
||||
.command('search [query]', 'Search published skills')
|
||||
.option('--registry <url>', 'Registry URL')
|
||||
.option('--token <token>', 'API token')
|
||||
.option('--limit <n>', 'Max results', { default: 20 })
|
||||
.option('--json', 'Output JSON')
|
||||
.action((query: string | undefined, options: { registry?: string; limit?: number; json?: boolean }) => {
|
||||
.action((query: string | undefined, options: { registry?: string; token?: string; limit?: number; json?: boolean }) => {
|
||||
return runCommand(() => searchCommand(query ?? '', options), Boolean(options.json))
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -218,6 +218,65 @@ describe('install command — P1', () => {
|
|||
expect(result.stderr.toLowerCase()).toMatch(/auth|unauthorized|401/)
|
||||
})
|
||||
|
||||
test('bad token stops on 401 without retrying resolve anonymously', async () => {
|
||||
const env = await createTempHome()
|
||||
const installDir = join(env.cwd, 'skills-no-anon-retry')
|
||||
await mkdir(installDir, { recursive: true })
|
||||
|
||||
const resolveAuthHeaders: Array<string | null> = []
|
||||
let downloadRequests = 0
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req) {
|
||||
const url = new URL(req.url)
|
||||
const resolveMatch = url.pathname.match(/^\/api\/cli\/v1\/skills\/([^/]+)\/([^/]+)\/resolve$/)
|
||||
if (resolveMatch) {
|
||||
const auth = req.headers.get('authorization')
|
||||
resolveAuthHeaders.push(auth)
|
||||
if (auth === 'Bearer sk_bad') {
|
||||
return Response.json({ code: 401, message: 'unauthorized' }, { status: 401 })
|
||||
}
|
||||
return Response.json({
|
||||
code: 0,
|
||||
data: {
|
||||
namespace: resolveMatch[1],
|
||||
slug: resolveMatch[2],
|
||||
version: '1.0.0',
|
||||
versionId: 1,
|
||||
fingerprint: 'abc123',
|
||||
downloadUrl: `${url.protocol}//${url.host}/api/cli/v1/skills/${resolveMatch[1]}/${resolveMatch[2]}/download`
|
||||
}
|
||||
})
|
||||
}
|
||||
if (url.pathname.endsWith('/download')) {
|
||||
downloadRequests += 1
|
||||
return new Response(makeSkillZip() as BodyInit, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/zip' }
|
||||
})
|
||||
}
|
||||
return Response.json({ code: 404 }, { status: 404 })
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const registryUrl = `http://localhost:${server.port}`
|
||||
const result = await runCli(
|
||||
['install', 'pdf-parser', '--dir', installDir, '--registry', registryUrl, '--token', 'sk_bad'],
|
||||
{ HOME: env.home, USERPROFILE: env.home }
|
||||
)
|
||||
|
||||
expect(result.exitCode).toBe(2)
|
||||
expect(result.stderr).toContain('Error: authentication failed')
|
||||
expect(result.stderr).toContain(`Context: registry ${registryUrl}`)
|
||||
expect(result.stderr).toContain('Next:')
|
||||
expect(resolveAuthHeaders).toEqual(['Bearer sk_bad'])
|
||||
expect(downloadRequests).toBe(0)
|
||||
} finally {
|
||||
server.stop()
|
||||
}
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// P1 — --namespace override
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -10,6 +10,109 @@ afterEach(() => {
|
|||
})
|
||||
|
||||
describe('search command', () => {
|
||||
test('--token sends bearer auth and takes priority over SKILLHUB_TOKEN', async () => {
|
||||
let capturedAuth = ''
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req) {
|
||||
const url = new URL(req.url)
|
||||
if (url.pathname === '/api/cli/v1/skills/search') {
|
||||
capturedAuth = req.headers.get('authorization') ?? ''
|
||||
return Response.json({
|
||||
code: 0,
|
||||
data: {
|
||||
items: [{ namespace: 'global', slug: 'pdf-parser', latestVersion: '1.2.0', summary: 'Parse PDFs' }],
|
||||
total: 1,
|
||||
limit: 20
|
||||
}
|
||||
})
|
||||
}
|
||||
return Response.json({ code: 404 }, { status: 404 })
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await runCli(
|
||||
['search', 'pdf', '--registry', `http://localhost:${server.port}`, '--token', 'sk_ok'],
|
||||
{ SKILLHUB_TOKEN: 'sk_bad' }
|
||||
)
|
||||
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(capturedAuth).toBe('Bearer sk_ok')
|
||||
expect(result.stdout).toContain('global/pdf-parser')
|
||||
} finally {
|
||||
server.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test('bad --token fails with auth output and does not retry anonymously', async () => {
|
||||
const authHeaders: Array<string | null> = []
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req) {
|
||||
const url = new URL(req.url)
|
||||
if (url.pathname === '/api/cli/v1/skills/search') {
|
||||
const auth = req.headers.get('authorization')
|
||||
authHeaders.push(auth)
|
||||
if (auth === 'Bearer sk_bad') {
|
||||
return Response.json({ code: 401, message: 'unauthorized' }, { status: 401 })
|
||||
}
|
||||
return Response.json({
|
||||
code: 0,
|
||||
data: {
|
||||
items: [{ namespace: 'global', slug: 'anonymous-only', latestVersion: '1.0.0', summary: 'anonymous fallback' }],
|
||||
total: 1,
|
||||
limit: 20
|
||||
}
|
||||
})
|
||||
}
|
||||
return Response.json({ code: 404 }, { status: 404 })
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const registryUrl = `http://localhost:${server.port}`
|
||||
const result = await runCli(['search', 'pdf', '--registry', registryUrl, '--token', 'sk_bad'])
|
||||
|
||||
expect(result.exitCode).toBe(2)
|
||||
expect(result.stderr).toContain('Error: authentication failed')
|
||||
expect(result.stderr).toContain(`Context: registry ${registryUrl}`)
|
||||
expect(result.stderr).toContain('Next:')
|
||||
expect(authHeaders).toEqual(['Bearer sk_bad'])
|
||||
} finally {
|
||||
server.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test('bad --token returns structured json auth error', async () => {
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req) {
|
||||
const url = new URL(req.url)
|
||||
if (url.pathname === '/api/cli/v1/skills/search') {
|
||||
return Response.json({ code: 401, message: 'unauthorized' }, { status: 401 })
|
||||
}
|
||||
return Response.json({ code: 404 }, { status: 404 })
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const registryUrl = `http://localhost:${server.port}`
|
||||
const result = await runCli(['search', 'pdf', '--registry', registryUrl, '--token', 'sk_bad', '--json'])
|
||||
|
||||
expect(result.exitCode).toBe(2)
|
||||
const parsed = JSON.parse(result.stderr)
|
||||
expect(parsed.ok).toBe(false)
|
||||
expect(parsed.message).toBe('authentication failed')
|
||||
expect(parsed.exitCode).toBe(2)
|
||||
expect(parsed.details.registry).toBe(registryUrl)
|
||||
expect(typeof parsed.details.next).toBe('string')
|
||||
expect(parsed.details.next).toContain('skillhub login')
|
||||
} finally {
|
||||
server.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test('prints compact search table', async () => {
|
||||
registry = await startFakeRegistry({
|
||||
searchItems: [{ namespace: 'global', slug: 'pdf-parser', latestVersion: '1.2.0', summary: 'Parse PDFs' }]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue