Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as readline from 'node:readline'

export default class AuthLogin extends Command {
static description = 'Authenticate with the DevHelm API'
static examples = ['<%= config.bin %> auth login', '<%= config.bin %> auth login --token sk_live_...']
static examples = ['<%= config.bin %> auth login', '<%= config.bin %> auth login --token dh_live_...']
static flags = {
...globalFlags,
token: Flags.string({description: 'API token (skips interactive prompt)'}),
Expand All @@ -23,14 +23,35 @@ export default class AuthLogin extends Command {
const apiUrl = flags['api-url'] || resolveApiUrl()
this.log('Validating token...')
const client = createApiClient({baseUrl: apiUrl, token})

// Try /api/v1/auth/me first (API key — returns rich identity info).
// Falls back to /api/v1/dashboard/overview for non-API-key tokens (dev tokens, JWTs).
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const me = await checkedFetch(client.GET('/platform/me' as any, {} as any))
const resp = await checkedFetch(client.GET('/api/v1/auth/me' as any, {} as any))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const me = (resp as any)?.data ?? resp

saveContext({name: flags.name, apiUrl, token}, true)
this.log('')
this.log(` Authenticated successfully.`)
this.log(` Organization: ${me.organization?.name ?? 'unknown'} (ID: ${me.organization?.id ?? '?'})`)
this.log(` Key: ${me.key?.name ?? 'unknown'}`)
this.log(` Plan: ${me.plan?.tier ?? 'unknown'}`)
this.log('')
this.log(` Context '${flags.name}' saved to ~/.devhelm/contexts.json`)
return
} catch {
// /auth/me failed — might be a non-API-key token; try basic validation
}

try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const email = (me as any)?.data?.email ?? (me as any)?.email
await checkedFetch(client.GET('/api/v1/dashboard/overview' as any, {} as any))
saveContext({name: flags.name, apiUrl, token}, true)
this.log(`\nAuthenticated as ${email}`)
this.log(`Context '${flags.name}' saved to ~/.devhelm/contexts.json`)
this.log('')
this.log(` Authenticated successfully.`)
this.log(` Context '${flags.name}' saved to ~/.devhelm/contexts.json`)
} catch {
this.error('Invalid token. Authentication failed.', {exit: 2})
}
Expand Down
60 changes: 60 additions & 0 deletions src/commands/auth/me.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {Command} from '@oclif/core'
import {globalFlags, buildClient} from '../../lib/base-command.js'
import {checkedFetch} from '../../lib/api-client.js'
import {formatOutput, OutputFormat} from '../../lib/output.js'

export default class AuthMe extends Command {
static description = 'Show current API key identity, organization, plan, and rate limits'
static examples = ['<%= config.bin %> auth me', '<%= config.bin %> auth me --output json']
static flags = {...globalFlags}

async run() {
const {flags} = await this.parse(AuthMe)
const client = buildClient(flags)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resp = await checkedFetch(client.GET('/api/v1/auth/me' as any, {} as any))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const me = (resp as any)?.data ?? resp

const format = flags.output as OutputFormat
if (format === 'json' || format === 'yaml') {
this.log(formatOutput(me, format))
return
}

const k = me.key ?? {}
const o = me.organization ?? {}
const p = me.plan ?? {}
const r = me.rateLimits ?? {}

this.log('')
this.log(' API Key')
this.log(` Name: ${k.name ?? '–'} ID: ${k.id ?? '–'}`)
this.log(` Created: ${k.createdAt ?? '–'} Expires: ${k.expiresAt ?? 'never'}`)
this.log(` Last used: ${k.lastUsedAt ?? 'never'}`)
this.log('')
this.log(' Organization')
this.log(` Name: ${o.name ?? '–'} ID: ${o.id ?? '–'}`)
this.log('')
this.log(' Plan')
this.log(` Tier: ${p.tier ?? '–'} Status: ${p.subscriptionStatus ?? '–'} Trial: ${p.trialActive ? `active (expires ${p.trialExpiresAt})` : 'no'}`)
this.log('')
this.log(' Rate Limits')
this.log(` Limit: ${r.requestsPerMinute ?? '–'} req/min Remaining: ${r.remaining ?? '–'} Window: ${r.windowMs ? `${r.windowMs / 1000}s` : '–'}`)

const usage = p.usage as Record<string, number> | undefined
const entitlements = p.entitlements as Record<string, {value: number}> | undefined
if (usage && entitlements) {
this.log('')
this.log(' Usage')
for (const [key, used] of Object.entries(usage)) {
const limit = entitlements[key]?.value
const limitStr = limit != null && limit < Number.MAX_SAFE_INTEGER ? String(limit) : '∞'
const label = key.replace(/\./g, ' ').replace(/_/g, ' ')
this.log(` ${label}: ${used} / ${limitStr}`)
}
}

this.log('')
}
}
Loading