refactor(cli): centralize auth token lookup

This commit is contained in:
Peter Steinberger
2026-02-14 15:25:57 +01:00
parent 73b18a883e
commit 2e20b3bda5
16 changed files with 50 additions and 73 deletions
+14
View File
@@ -0,0 +1,14 @@
import { readGlobalConfig } from '../config.js'
import { fail } from './ui.js'
export async function getOptionalAuthToken(): Promise<string | undefined> {
const cfg = await readGlobalConfig()
return cfg?.token ?? undefined
}
export async function requireAuthToken(): Promise<string> {
const token = await getOptionalAuthToken()
if (!token) fail('Not logged in. Run: clawhub login')
return token
}
+2 -3
View File
@@ -3,6 +3,7 @@ import { readGlobalConfig, writeGlobalConfig } from '../../config.js'
import { discoverRegistryFromSite } from '../../discovery.js'
import { apiRequest } from '../../http.js'
import { ApiRoutes, ApiV1WhoamiResponseSchema } from '../../schema/index.js'
import { requireAuthToken } from '../authToken.js'
import { getRegistry } from '../registry.js'
import type { GlobalOpts } from '../types.js'
import { createSpinner, fail, formatError, openInBrowser, promptHidden } from '../ui.js'
@@ -78,9 +79,7 @@ export async function cmdLogout(opts: GlobalOpts) {
}
export async function cmdWhoami(opts: GlobalOpts) {
const cfg = await readGlobalConfig()
const token = cfg?.token
if (!token) fail('Not logged in. Run: clawhub login')
const token = await requireAuthToken()
const registry = await getRegistry(opts, { cache: true })
const spinner = createSpinner('Checking token')
@@ -3,8 +3,8 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { GlobalOpts } from '../types'
vi.mock('../../config.js', () => ({
readGlobalConfig: vi.fn(async () => ({ registry: 'https://clawhub.ai', token: 'tkn' })),
vi.mock('../authToken.js', () => ({
requireAuthToken: vi.fn(async () => 'tkn'),
}))
vi.mock('../registry.js', () => ({
+3 -10
View File
@@ -1,6 +1,6 @@
import { readGlobalConfig } from '../../config.js'
import { apiRequest } from '../../http.js'
import { ApiRoutes, ApiV1DeleteResponseSchema, parseArk } from '../../schema/index.js'
import { requireAuthToken } from '../authToken.js'
import { getRegistry } from '../registry.js'
import type { GlobalOpts } from '../types.js'
import { createSpinner, fail, formatError, isInteractive, promptConfirm } from '../ui.js'
@@ -40,13 +40,6 @@ const unhideLabels: SkillActionLabels = {
promptSuffix: 'requires moderator/admin',
}
async function requireToken() {
const cfg = await readGlobalConfig()
const token = cfg?.token
if (!token) fail('Not logged in. Run: clawhub login')
return token
}
export async function cmdDeleteSkill(
opts: GlobalOpts,
slugArg: string,
@@ -64,7 +57,7 @@ export async function cmdDeleteSkill(
if (!ok) return
}
const token = await requireToken()
const token = await requireAuthToken()
const registry = await getRegistry(opts, { cache: true })
const spinner = createSpinner(`${labels.progress} ${slug}`)
try {
@@ -98,7 +91,7 @@ export async function cmdUndeleteSkill(
if (!ok) return
}
const token = await requireToken()
const token = await requireAuthToken()
const registry = await getRegistry(opts, { cache: true })
const spinner = createSpinner(`${labels.progress} ${slug}`)
try {
@@ -16,9 +16,9 @@ vi.mock('../registry.js', () => ({
getRegistry: () => mockGetRegistry(),
}))
const mockReadGlobalConfig = vi.fn(async () => null as { token?: string } | null)
vi.mock('../../config.js', () => ({
readGlobalConfig: () => mockReadGlobalConfig(),
const mockGetOptionalAuthToken = vi.fn(async () => undefined as string | undefined)
vi.mock('../authToken.js', () => ({
getOptionalAuthToken: () => mockGetOptionalAuthToken(),
}))
const mockSpinner = {
@@ -5,7 +5,7 @@ import {
ApiV1SkillVersionListResponseSchema,
ApiV1SkillVersionResponseSchema,
} from '../../schema/index.js'
import { readGlobalConfig } from '../../config.js'
import { getOptionalAuthToken } from '../authToken.js'
import { getRegistry } from '../registry.js'
import type { GlobalOpts } from '../types.js'
import { createSpinner, fail, formatError } from '../ui.js'
@@ -32,9 +32,7 @@ export async function cmdInspect(opts: GlobalOpts, slug: string, options: Inspec
if (!trimmed) fail('Slug required')
if (options.version && options.tag) fail('Use either --version or --tag')
const cfg = await readGlobalConfig()
const token = cfg?.token ?? undefined
const token = await getOptionalAuthToken()
const registry = await getRegistry(opts, { cache: true })
const spinner = createSpinner('Fetching skill')
try {
@@ -3,8 +3,8 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { GlobalOpts } from '../types'
vi.mock('../../config.js', () => ({
readGlobalConfig: vi.fn(async () => ({ registry: 'https://clawhub.ai', token: 'tkn' })),
vi.mock('../authToken.js', () => ({
requireAuthToken: vi.fn(async () => 'tkn'),
}))
vi.mock('../registry.js', () => ({
@@ -1,5 +1,4 @@
import { isCancel, select } from '@clack/prompts'
import { readGlobalConfig } from '../../config.js'
import { apiRequest } from '../../http.js'
import {
ApiRoutes,
@@ -8,17 +7,11 @@ import {
ApiV1UserSearchResponseSchema,
parseArk,
} from '../../schema/index.js'
import { requireAuthToken } from '../authToken.js'
import { getRegistry } from '../registry.js'
import type { GlobalOpts } from '../types.js'
import { createSpinner, fail, formatError, isInteractive, promptConfirm } from '../ui.js'
async function requireToken() {
const cfg = await readGlobalConfig()
const token = cfg?.token
if (!token) fail('Not logged in. Run: clawhub login')
return token
}
export async function cmdBanUser(
opts: GlobalOpts,
identifierArg: string,
@@ -30,7 +23,7 @@ export async function cmdBanUser(
const reason = options.reason?.trim() || undefined
const token = await requireToken()
const token = await requireAuthToken()
const registry = await getRegistry(opts, { cache: true })
const allowPrompt = isInteractive() && inputAllowed !== false
const resolved = await resolveUserIdentifier(
@@ -87,7 +80,7 @@ export async function cmdSetRole(
if (!raw) fail('Handle or user id required')
const role = normalizeRole(roleArg)
const token = await requireToken()
const token = await requireAuthToken()
const registry = await getRegistry(opts, { cache: true })
const allowPrompt = isInteractive() && inputAllowed !== false
const resolved = await resolveUserIdentifier(
@@ -6,8 +6,8 @@ import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { GlobalOpts } from '../types'
vi.mock('../../config.js', () => ({
readGlobalConfig: vi.fn(async () => ({ registry: 'https://clawhub.ai', token: 'tkn' })),
vi.mock('../authToken.js', () => ({
requireAuthToken: vi.fn(async () => 'tkn'),
}))
const mockGetRegistry = vi.fn(async (_opts: unknown, _params?: unknown) => 'https://clawhub.ai')
@@ -1,10 +1,10 @@
import { stat } from 'node:fs/promises'
import { basename, resolve } from 'node:path'
import semver from 'semver'
import { readGlobalConfig } from '../../config.js'
import { apiRequestForm } from '../../http.js'
import { ApiRoutes, ApiV1PublishResponseSchema } from '../../schema/index.js'
import { listTextFiles } from '../../skills.js'
import { requireAuthToken } from '../authToken.js'
import { getRegistry } from '../registry.js'
import { sanitizeSlug, titleCase } from '../slug.js'
import type { GlobalOpts } from '../types.js'
@@ -27,9 +27,7 @@ export async function cmdPublish(
const folderStat = await stat(folder).catch(() => null)
if (!folderStat || !folderStat.isDirectory()) fail('Path must be a folder')
const cfg = await readGlobalConfig()
const token = cfg?.token
if (!token) fail('Not logged in. Run: clawhub login')
const token = await requireAuthToken()
const registry = await getRegistry(opts, { cache: true })
const slug = options.slug ?? sanitizeSlug(basename(folder))
@@ -16,9 +16,9 @@ vi.mock('../registry.js', () => ({
getRegistry: () => mockGetRegistry(),
}))
const mockReadGlobalConfig = vi.fn(async () => null as { token?: string } | null)
vi.mock('../../config.js', () => ({
readGlobalConfig: () => mockReadGlobalConfig(),
const mockGetOptionalAuthToken = vi.fn(async () => undefined as string | undefined)
vi.mock('../authToken.js', () => ({
getOptionalAuthToken: () => mockGetOptionalAuthToken(),
}))
const mockSpinner = {
@@ -197,7 +197,7 @@ describe('cmdUpdate', () => {
describe('cmdInstall', () => {
it('passes optional auth token to API + download requests', async () => {
mockReadGlobalConfig.mockResolvedValue({ token: 'tkn' })
mockGetOptionalAuthToken.mockResolvedValue('tkn')
mockApiRequest.mockResolvedValue({
skill: { slug: 'demo', displayName: 'Demo', summary: null, tags: {}, stats: {}, createdAt: 0, updatedAt: 0 },
latestVersion: { version: '1.0.0' },
+3 -5
View File
@@ -21,7 +21,7 @@ import {
import { getRegistry } from '../registry.js'
import type { GlobalOpts, ResolveResult } from '../types.js'
import { createSpinner, fail, formatError, isInteractive, promptConfirm } from '../ui.js'
import { readGlobalConfig } from '../../config.js'
import { getOptionalAuthToken } from '../authToken.js'
export async function cmdSearch(opts: GlobalOpts, query: string, limit?: number) {
if (!query) fail('Query required')
@@ -62,8 +62,7 @@ export async function cmdInstall(
const trimmed = slug.trim()
if (!trimmed) fail('Slug required')
const cfg = await readGlobalConfig()
const token = cfg?.token ?? undefined
const token = await getOptionalAuthToken()
const registry = await getRegistry(opts, { cache: true })
await mkdir(opts.dir, { recursive: true })
@@ -148,8 +147,7 @@ export async function cmdUpdate(
if (options.version && !semver.valid(options.version)) fail('--version must be valid semver')
const allowPrompt = isInteractive() && inputAllowed !== false
const cfg = await readGlobalConfig()
const token = cfg?.token ?? undefined
const token = await getOptionalAuthToken()
const registry = await getRegistry(opts, { cache: true })
const lock = await readLockfile(opts.workdir)
+2 -9
View File
@@ -1,17 +1,10 @@
import { readGlobalConfig } from '../../config.js'
import { apiRequest } from '../../http.js'
import { ApiRoutes, ApiV1StarResponseSchema } from '../../schema/index.js'
import { requireAuthToken } from '../authToken.js'
import { getRegistry } from '../registry.js'
import type { GlobalOpts } from '../types.js'
import { createSpinner, fail, formatError, isInteractive, promptConfirm } from '../ui.js'
async function requireToken() {
const cfg = await readGlobalConfig()
const token = cfg?.token
if (!token) fail('Not logged in. Run: clawhub login')
return token
}
export async function cmdStarSkill(
opts: GlobalOpts,
slugArg: string,
@@ -28,7 +21,7 @@ export async function cmdStarSkill(
if (!ok) return
}
const token = await requireToken()
const token = await requireAuthToken()
const registry = await getRegistry(opts, { cache: true })
const spinner = createSpinner(`Starring ${slug}`)
try {
@@ -26,8 +26,8 @@ vi.mock('@clack/prompts', () => ({
isCancel: () => false,
}))
vi.mock('../../config.js', () => ({
readGlobalConfig: vi.fn(async () => ({ registry: 'https://clawhub.ai', token: 'tkn' })),
vi.mock('../authToken.js', () => ({
requireAuthToken: vi.fn(async () => 'tkn'),
}))
const mockGetRegistry = vi.fn(async () => 'https://clawhub.ai')
+2 -4
View File
@@ -1,7 +1,7 @@
import { intro, outro } from '@clack/prompts'
import { readGlobalConfig } from '../../config.js'
import { hashSkillFiles, listTextFiles, readSkillOrigin } from '../../skills.js'
import { resolveClawdbotSkillRoots } from '../clawdbotConfig.js'
import { requireAuthToken } from '../authToken.js'
import { getFallbackSkillRoots } from '../scanSkills.js'
import type { GlobalOpts } from '../types.js'
import { createSpinner, fail, formatError, isInteractive } from '../ui.js'
@@ -32,9 +32,7 @@ export async function cmdSync(opts: GlobalOpts, options: SyncOptions, inputAllow
const allowPrompt = isInteractive() && inputAllowed !== false
intro('ClawHub sync')
const cfg = await readGlobalConfig()
const token = cfg?.token
if (!token) fail('Not logged in. Run: clawhub login')
const token = await requireAuthToken()
const registry = await getRegistryWithAuth(opts, token)
const selectedRoots = buildScanRoots(opts, options.root)
+2 -9
View File
@@ -1,17 +1,10 @@
import { readGlobalConfig } from '../../config.js'
import { apiRequest } from '../../http.js'
import { ApiRoutes, ApiV1UnstarResponseSchema } from '../../schema/index.js'
import { requireAuthToken } from '../authToken.js'
import { getRegistry } from '../registry.js'
import type { GlobalOpts } from '../types.js'
import { createSpinner, fail, formatError, isInteractive, promptConfirm } from '../ui.js'
async function requireToken() {
const cfg = await readGlobalConfig()
const token = cfg?.token
if (!token) fail('Not logged in. Run: clawhub login')
return token
}
export async function cmdUnstarSkill(
opts: GlobalOpts,
slugArg: string,
@@ -28,7 +21,7 @@ export async function cmdUnstarSkill(
if (!ok) return
}
const token = await requireToken()
const token = await requireAuthToken()
const registry = await getRegistry(opts, { cache: true })
const spinner = createSpinner(`Unstarring ${slug}`)
try {