Skip to content
Closed
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
7 changes: 5 additions & 2 deletions .github/workflows/spec-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,17 @@ jobs:
env:
GH_TOKEN: ${{ secrets.MONOREPO_DISPATCH_TOKEN }}

- name: Validate spec is valid JSON
run: python3 -c "import json; json.load(open('docs/openapi/monitoring-api.json'))"

- run: npm ci
- run: npm run build
- run: npm test

- name: Check for type changes
id: diff
run: |
if git diff --quiet src/lib/api.generated.ts docs/openapi/monitoring-api.json; then
if git diff --quiet src/lib/api.generated.ts src/lib/descriptions.generated.ts docs/openapi/monitoring-api.json; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
Expand All @@ -47,7 +50,7 @@ jobs:
git config user.email "github-actions[bot]@users.noreply.github.com"
branch="chore/update-api-types-$(date +%Y%m%d%H%M%S)"
git checkout -b "$branch"
git add src/lib/api.generated.ts docs/openapi/monitoring-api.json
git add src/lib/api.generated.ts src/lib/descriptions.generated.ts docs/openapi/monitoring-api.json
git commit -m "chore: update generated API types from latest spec"
git push -u origin "$branch"
gh pr create \
Expand Down
2 changes: 1 addition & 1 deletion docs/openapi/monitoring-api.json

Large diffs are not rendered by default.

10 changes: 4 additions & 6 deletions src/commands/alert-channels/test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Command, Args} from '@oclif/core'
import {globalFlags, buildClient} from '../../lib/base-command.js'
import {checkedFetch} from '../../lib/api-client.js'
import {checkedFetch, unwrap} from '../../lib/api-client.js'

export default class AlertChannelsTest extends Command {
static description = 'Send a test notification to an alert channel'
Expand All @@ -11,10 +11,8 @@ export default class AlertChannelsTest extends Command {
async run() {
const {args, flags} = await this.parse(AlertChannelsTest)
const client = buildClient(flags)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resp = await checkedFetch(client.POST(`/api/v1/alert-channels/${args.id}/test` as any, {} as any))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = (resp as any)?.data ?? resp
this.log(result.success ? 'Test notification sent successfully.' : 'Test notification failed.')
const resp = await checkedFetch(client.POST('/api/v1/alert-channels/{id}/test', {params: {path: {id: args.id}}}))
const result = unwrap<{success?: boolean}>(resp)
this.log(result?.success ? 'Test notification sent successfully.' : 'Test notification failed.')
}
}
3 changes: 1 addition & 2 deletions src/commands/api-keys/revoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ export default class ApiKeysRevoke extends Command {
async run() {
const {args, flags} = await this.parse(ApiKeysRevoke)
const client = buildClient(flags)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await checkedFetch(client.POST(`/api/v1/api-keys/${args.id}/revoke` as any, {} as any))
await checkedFetch(client.POST('/api/v1/api-keys/{id}/revoke', {params: {path: {id: Number(args.id)}}}))
this.log(`API key '${args.id}' revoked.`)
}
}
20 changes: 18 additions & 2 deletions src/commands/auth/context/create.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
import {Command, Args, Flags} from '@oclif/core'
import {globalFlags} from '../../../lib/base-command.js'
import {createApiClient, checkedFetch} from '../../../lib/api-client.js'
import {saveContext} from '../../../lib/auth.js'

export default class AuthContextCreate extends Command {
static description = 'Create a new auth context'
static examples = ['<%= config.bin %> auth context create staging --api-url https://staging-api.devhelm.io --token sk_...']
static description = 'Create a new auth context (validates token before saving)'
static examples = ['<%= config.bin %> auth context create staging --api-url https://staging-api.devhelm.io --token dh_live_...']
static args = {name: Args.string({description: 'Context name', required: true})}
static flags = {
...globalFlags,
token: Flags.string({description: 'API token', required: true}),
'set-current': Flags.boolean({description: 'Set as current context', default: true}),
'skip-validation': Flags.boolean({description: 'Save without validating the token', default: false}),
}

async run() {
const {args, flags} = await this.parse(AuthContextCreate)
const apiUrl = flags['api-url'] || 'https://api.devhelm.io'

if (!flags['skip-validation']) {
const client = createApiClient({baseUrl: apiUrl, token: flags.token})
try {
await checkedFetch(client.GET('/api/v1/auth/me'))
} catch {
try {
await checkedFetch(client.GET('/api/v1/dashboard/overview'))
} catch {
this.error('Token validation failed. Use --skip-validation to save anyway.', {exit: 2})
}
}
}

saveContext({name: args.name, apiUrl, token: flags.token}, flags['set-current'])
this.log(`Context '${args.name}' created.${flags['set-current'] ? ' (active)' : ''}`)
}
Expand Down
17 changes: 7 additions & 10 deletions src/commands/auth/login.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {Command, Flags} from '@oclif/core'
import {globalFlags} from '../../lib/base-command.js'
import {createApiClient, checkedFetch} from '../../lib/api-client.js'
import {createApiClient, checkedFetch, unwrap, type Schemas} from '../../lib/api-client.js'
import {saveContext, resolveApiUrl} from '../../lib/auth.js'
import * as readline from 'node:readline'

type AuthMeResponse = Schemas['AuthMeResponse']

export default class AuthLogin extends Command {
static description = 'Authenticate with the DevHelm API'
static examples = ['<%= config.bin %> auth login', '<%= config.bin %> auth login --token dh_live_...']
Expand All @@ -24,13 +26,9 @@ export default class AuthLogin extends Command {
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 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 resp = await checkedFetch(client.GET('/api/v1/auth/me'))
const me = unwrap<AuthMeResponse>(resp)

saveContext({name: flags.name, apiUrl, token}, true)
this.log('')
Expand All @@ -42,12 +40,11 @@ export default class AuthLogin extends Command {
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
// /auth/me requires API key auth; fall back for dev tokens / JWTs
}

try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await checkedFetch(client.GET('/api/v1/dashboard/overview' as any, {} as any))
await checkedFetch(client.GET('/api/v1/dashboard/overview'))
saveContext({name: flags.name, apiUrl, token}, true)
this.log('')
this.log(` Authenticated successfully.`)
Expand Down
12 changes: 6 additions & 6 deletions src/commands/auth/me.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
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'
import {checkedFetch, unwrap, type Schemas} from '../../lib/api-client.js'
import {formatOutput, type OutputFormat} from '../../lib/output.js'

type AuthMeResponse = Schemas['AuthMeResponse']

export default class AuthMe extends Command {
static description = 'Show current API key identity, organization, plan, and rate limits'
Expand All @@ -11,10 +13,8 @@ export default class AuthMe extends Command {
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 resp = await checkedFetch(client.GET('/api/v1/auth/me'))
const me = unwrap<AuthMeResponse>(resp)

const format = flags.output as OutputFormat
if (format === 'json' || format === 'yaml') {
Expand Down
9 changes: 3 additions & 6 deletions src/commands/data/services/status.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Command, Args} from '@oclif/core'
import {globalFlags, buildClient, display} from '../../../lib/base-command.js'
import {checkedFetch} from '../../../lib/api-client.js'
import {checkedFetch, unwrap} from '../../../lib/api-client.js'

export default class DataServicesStatus extends Command {
static description = 'Get the current status of a service'
Expand All @@ -11,10 +11,7 @@ export default class DataServicesStatus extends Command {
async run() {
const {args, flags} = await this.parse(DataServicesStatus)
const client = buildClient(flags)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resp = await checkedFetch(client.GET(`/api/v1/services/${args.slug}` as any, {} as any))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const service = (resp as any)?.data ?? resp
display(this, service, flags.output)
const resp = await checkedFetch(client.GET('/api/v1/services/{slugOrId}', {params: {path: {slugOrId: args.slug}}}))
display(this, unwrap(resp), flags.output)
}
}
23 changes: 13 additions & 10 deletions src/commands/data/services/uptime.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Command, Args, Flags} from '@oclif/core'
import {globalFlags, buildClient, display} from '../../../lib/base-command.js'
import {checkedFetch} from '../../../lib/api-client.js'
import {checkedFetch, unwrap} from '../../../lib/api-client.js'

export default class DataServicesUptime extends Command {
static description = 'Get uptime data for a service'
Expand All @@ -11,19 +11,22 @@ export default class DataServicesUptime extends Command {
static args = {slug: Args.string({description: 'Service slug', required: true})}
static flags = {
...globalFlags,
period: Flags.string({description: 'Time period (7d, 30d, 90d)', default: '30d'}),
granularity: Flags.string({description: 'Data granularity (hourly, daily)'}),
period: Flags.string({description: 'Time period', default: '30d', options: ['24h', '7d', '30d', '90d', '1y', '2y', 'all']}),
granularity: Flags.string({description: 'Data granularity', options: ['hourly', 'daily', 'monthly']}),
}

async run() {
const {args, flags} = await this.parse(DataServicesUptime)
const client = buildClient(flags)
let path = `/api/v1/services/${args.slug}/uptime?period=${flags.period}`
if (flags.granularity) path += `&granularity=${flags.granularity}`
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resp = await checkedFetch(client.GET(path as any, {} as any))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const uptime = (resp as any)?.data ?? resp
display(this, uptime, flags.output)
const resp = await checkedFetch(client.GET('/api/v1/services/{slugOrId}/uptime', {
params: {
path: {slugOrId: args.slug},
query: {
period: flags.period as '24h' | '7d' | '30d' | '90d' | '1y' | '2y' | 'all',
...(flags.granularity ? {granularity: flags.granularity as 'hourly' | 'daily' | 'monthly'} : {}),
},
},
}))
display(this, unwrap(resp), flags.output)
}
}
10 changes: 4 additions & 6 deletions src/commands/dependencies/track.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Command, Args} from '@oclif/core'
import {globalFlags, buildClient} from '../../lib/base-command.js'
import {checkedFetch} from '../../lib/api-client.js'
import {checkedFetch, unwrap} from '../../lib/api-client.js'

export default class DependenciesTrack extends Command {
static description = 'Start tracking a service as a dependency'
Expand All @@ -11,10 +11,8 @@ export default class DependenciesTrack extends Command {
async run() {
const {args, flags} = await this.parse(DependenciesTrack)
const client = buildClient(flags)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resp = await checkedFetch(client.POST(`/api/v1/service-subscriptions/${args.slug}` as any, {} as any))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sub = (resp as any)?.data ?? resp
this.log(`Now tracking '${sub.serviceName}' as a dependency.`)
const resp = await checkedFetch(client.POST('/api/v1/service-subscriptions/{slug}', {params: {path: {slug: args.slug}}}))
const sub = unwrap<{serviceName?: string}>(resp)
this.log(`Now tracking '${sub.serviceName ?? args.slug}' as a dependency.`)
}
}
13 changes: 6 additions & 7 deletions src/commands/incidents/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Command, Args, Flags} from '@oclif/core'
import {globalFlags, buildClient} from '../../lib/base-command.js'
import {checkedFetch} from '../../lib/api-client.js'
import {checkedFetch, unwrap, type Schemas} from '../../lib/api-client.js'

export default class IncidentsResolve extends Command {
static description = 'Resolve an incident'
Expand All @@ -15,12 +15,11 @@ export default class IncidentsResolve extends Command {
const {args, flags} = await this.parse(IncidentsResolve)
const client = buildClient(flags)
const body = flags.message ? {message: flags.message} : undefined
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const opts = body ? {body: body as any} : {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resp = await checkedFetch(client.POST(`/api/v1/incidents/${args.id}/resolve` as any, opts as any))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const incident = (resp as any)?.data ?? resp
const resp = await checkedFetch(client.POST('/api/v1/incidents/{id}/resolve', {
params: {path: {id: args.id}},
...(body ? {body} : {}),
}))
const incident = unwrap<Schemas['IncidentDto']>(resp)
this.log(`Incident '${incident.title}' resolved.`)
}
}
8 changes: 4 additions & 4 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ monitors:
- name: Website Health Check
type: HTTP
url: https://example.com
interval: 60
frequency: 60
regions:
- us-east-1
- eu-west-1
Expand All @@ -26,17 +26,17 @@ monitors:
# type: HTTP
# url: https://api.example.com/health
# method: GET
# interval: 30
# frequency: 30
# timeout: 10000

# - name: DNS Check
# type: DNS
# url: example.com
# interval: 300
# frequency: 300

# - name: Heartbeat
# type: HEARTBEAT
# interval: 120
# frequency: 120
# grace: 300
`

Expand Down
8 changes: 3 additions & 5 deletions src/commands/monitors/pause.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Command, Args} from '@oclif/core'
import {globalFlags, buildClient} from '../../lib/base-command.js'
import {checkedFetch} from '../../lib/api-client.js'
import {checkedFetch, unwrap, type Schemas} from '../../lib/api-client.js'

export default class MonitorsPause extends Command {
static description = 'Pause a monitor'
Expand All @@ -11,10 +11,8 @@ export default class MonitorsPause extends Command {
async run() {
const {args, flags} = await this.parse(MonitorsPause)
const client = buildClient(flags)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resp = await checkedFetch(client.POST(`/api/v1/monitors/${args.id}/pause` as any, {} as any))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const monitor = (resp as any)?.data ?? resp
const resp = await checkedFetch(client.POST('/api/v1/monitors/{id}/pause', {params: {path: {id: args.id}}}))
const monitor = unwrap<Schemas['MonitorDto']>(resp)
this.log(`Monitor '${monitor.name}' paused.`)
}
}
23 changes: 12 additions & 11 deletions src/commands/monitors/results.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {Command, Args, Flags} from '@oclif/core'
import {globalFlags, buildClient, display} from '../../lib/base-command.js'
import {checkedFetch} from '../../lib/api-client.js'
import {checkedFetch, unwrap, type Schemas} from '../../lib/api-client.js'

type CheckResult = Schemas['CheckResultDto']

export default class MonitorsResults extends Command {
static description = 'Show recent check results for a monitor'
Expand All @@ -14,17 +16,16 @@ export default class MonitorsResults extends Command {
async run() {
const {args, flags} = await this.parse(MonitorsResults)
const client = buildClient(flags)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resp = await checkedFetch(client.GET(`/api/v1/monitors/${args.id}/results?limit=${flags.limit}` as any, {} as any))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const items = (resp as any)?.data ?? resp
const resp = await checkedFetch(client.GET('/api/v1/monitors/{id}/results', {
params: {path: {id: args.id}, query: {limit: flags.limit}},
}))
const items = unwrap<CheckResult[]>(resp)
display(this, items, flags.output, [
{header: 'ID', get: (r: Record<string, unknown>) => String(r.id ?? '')},
{header: 'STATUS', get: (r: Record<string, unknown>) => String(r.status ?? '')},
{header: 'RESPONSE TIME', get: (r: Record<string, unknown>) => String(r.responseTime ?? '')},
{header: 'CODE', get: (r: Record<string, unknown>) => String(r.statusCode ?? '')},
{header: 'REGION', get: (r: Record<string, unknown>) => String(r.region ?? '')},
{header: 'CHECKED AT', get: (r: Record<string, unknown>) => String(r.checkedAt ?? '')},
{header: 'ID', get: (r: CheckResult) => String(r.id ?? '')},
{header: 'PASSED', get: (r: CheckResult) => String(r.passed ?? '')},
{header: 'RESPONSE TIME', get: (r: CheckResult) => r.responseTimeMs != null ? `${r.responseTimeMs}ms` : ''},
{header: 'REGION', get: (r: CheckResult) => r.region ?? ''},
{header: 'CHECKED AT', get: (r: CheckResult) => r.timestamp ?? ''},
])
}
}
8 changes: 3 additions & 5 deletions src/commands/monitors/resume.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Command, Args} from '@oclif/core'
import {globalFlags, buildClient} from '../../lib/base-command.js'
import {checkedFetch} from '../../lib/api-client.js'
import {checkedFetch, unwrap, type Schemas} from '../../lib/api-client.js'

export default class MonitorsResume extends Command {
static description = 'Resume a paused monitor'
Expand All @@ -11,10 +11,8 @@ export default class MonitorsResume extends Command {
async run() {
const {args, flags} = await this.parse(MonitorsResume)
const client = buildClient(flags)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resp = await checkedFetch(client.POST(`/api/v1/monitors/${args.id}/resume` as any, {} as any))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const monitor = (resp as any)?.data ?? resp
const resp = await checkedFetch(client.POST('/api/v1/monitors/{id}/resume', {params: {path: {id: args.id}}}))
const monitor = unwrap<Schemas['MonitorDto']>(resp)
this.log(`Monitor '${monitor.name}' resumed.`)
}
}
Loading
Loading