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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,9 @@ Every resource supports `list`, `get`, `create`, `update`, and `delete` subcomma
| `resource-groups` | list, get, create, update, delete | `/api/v1/resource-groups` |
| `webhooks` | list, get, create, update, delete, test | `/api/v1/webhooks` |
| `api-keys` | list, get, create, delete, revoke | `/api/v1/api-keys` |
| `dependencies` | list, get, track, delete | `/api/v1/service-subscriptions` |
| `data services` | status, uptime | `/api/v1/services` |
| `dependencies` | list, get, track, update, delete | `/api/v1/service-subscriptions` |
| `services` | list, get, status, summary, components, categories, uptime, incidents, maintenances | `/api/v1/services`, `/api/v1/categories` |
| `data services` | status, uptime (aliases of `services status` / `services uptime`) | `/api/v1/services` |

### Global Flags

Expand Down
9 changes: 9 additions & 0 deletions docs/openapi/monitoring-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -13520,6 +13520,15 @@
"type": "boolean"
}
},
{
"name": "search",
"in": "query",
"description": "Case-insensitive substring match on service name or slug",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "cursor",
"in": "query",
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@
"secrets": {
"description": "Manage workspace secrets used in monitor configurations and headers"
},
"services": {
"description": "Browse the status-data service catalog and query service health"
},
"skills": {
"description": "Generate and install Cursor / Claude agent skill packages for DevHelm"
},
Expand Down
21 changes: 4 additions & 17 deletions src/commands/data/services/status.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,4 @@
import {Command, Args} from '@oclif/core'
import {globalFlags, buildClient, display} from '../../../lib/base-command.js'
import {checkedFetch, unwrapData} from '../../../lib/api-client.js'

export default class DataServicesStatus extends Command {
static description = 'Get the current status of a service'
static examples = ['<%= config.bin %> data services status aws-ec2']
static args = {slug: Args.string({description: 'Service slug', required: true})}
static flags = {...globalFlags}

async run() {
const {args, flags} = await this.parse(DataServicesStatus)
const client = buildClient(flags)
const resp = await checkedFetch(client.GET('/api/v1/services/{slugOrId}', {params: {path: {slugOrId: args.slug}}}))
display(this, unwrapData(resp), flags.output)
}
}
// Back-compat shim: `data services status` is the legacy spelling of
// `services status`. Re-exporting the class registers the same command
// under the old id so existing scripts keep working.
export {default} from '../../services/status.js'
30 changes: 4 additions & 26 deletions src/commands/data/services/uptime.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,4 @@
import {Command, Args, Flags} from '@oclif/core'
import {globalFlags, buildClient, display} from '../../../lib/base-command.js'
import {apiGet, unwrapData} from '../../../lib/api-client.js'

export default class DataServicesUptime extends Command {
static description = 'Get uptime data for a service'
static examples = [
'<%= config.bin %> data services uptime aws-ec2',
'<%= config.bin %> data services uptime aws-ec2 --period 30d',
]
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)'}),
}

async run() {
const {args, flags} = await this.parse(DataServicesUptime)
const client = buildClient(flags)
const query: Record<string, string> = {period: flags.period}
if (flags.granularity) query.granularity = flags.granularity
const resp = await apiGet(client, `/api/v1/services/${args.slug}/uptime`, {query})
display(this, unwrapData(resp), flags.output)
}
}
// Back-compat shim: `data services uptime` is the legacy spelling of
// `services uptime`. Re-exporting the class registers the same command
// under the old id so existing scripts keep working.
export {default} from '../../services/uptime.js'
33 changes: 29 additions & 4 deletions src/commands/dependencies/track.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,43 @@
import {Command, Args} from '@oclif/core'
import {Command, Args, Flags} from '@oclif/core'
import type {components} from '../../lib/api.generated.js'
import {globalFlags, buildClient} from '../../lib/base-command.js'
import {checkedFetch} from '../../lib/api-client.js'
import {ALERT_SENSITIVITIES} from '../../lib/spec-facts.generated.js'
import {uuidFlag} from '../../lib/validators.js'

type ServiceSubscribeRequest = components['schemas']['ServiceSubscribeRequest']

export default class DependenciesTrack extends Command {
static description = 'Start tracking a service as a dependency'
static examples = ['<%= config.bin %> dependencies track aws-ec2']
static examples = [
'<%= config.bin %> dependencies track aws-ec2',
'<%= config.bin %> dependencies track stripe --alert-sensitivity INCIDENTS_ONLY',
]
static args = {slug: Args.string({description: 'Service slug from the catalog', required: true})}
static flags = {...globalFlags}
static flags = {
...globalFlags,
component: uuidFlag({
description: 'Component ID to subscribe to (omit for a whole-service subscription)',
}),
'alert-sensitivity': Flags.string({
description: 'Alert sensitivity (default: AWARENESS — track silently, never alert)',
options: [...ALERT_SENSITIVITIES],
}),
}

async run() {
const {args, flags} = await this.parse(DependenciesTrack)
const client = buildClient(flags)
const body: ServiceSubscribeRequest = {}
if (flags.component) body.componentId = flags.component
if (flags['alert-sensitivity']) body.alertSensitivity = flags['alert-sensitivity']
const resp = await checkedFetch(
client.POST('/api/v1/service-subscriptions/{slug}', {params: {path: {slug: args.slug}}}),
client.POST('/api/v1/service-subscriptions/{slug}', {
params: {path: {slug: args.slug}},
// Omit the body entirely when no flag was set, preserving the
// pre-flag behavior of an empty POST.
...(Object.keys(body).length > 0 ? {body} : {}),
}),
)
this.log(`Now tracking '${resp?.data?.name ?? args.slug}' as a dependency.`)
}
Expand Down
35 changes: 35 additions & 0 deletions src/commands/dependencies/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {Command, Flags} from '@oclif/core'
import {globalFlags, buildClient, display} from '../../lib/base-command.js'
import {checkedFetch, unwrapData} from '../../lib/api-client.js'
import {ALERT_SENSITIVITIES} from '../../lib/spec-facts.generated.js'
import {uuidArg} from '../../lib/validators.js'

export default class DependenciesUpdate extends Command {
static description = 'Update the alert sensitivity of a tracked dependency'
static examples = [
'<%= config.bin %> dependencies update <subscriptionId> --alert-sensitivity INCIDENTS_ONLY',
]
static args = {
subscriptionId: uuidArg({description: 'dependency subscriptionId', required: true}),
}
static flags = {
...globalFlags,
'alert-sensitivity': Flags.string({
description: 'New alert sensitivity for the subscription',
options: [...ALERT_SENSITIVITIES],
required: true,
}),
}

async run() {
const {args, flags} = await this.parse(DependenciesUpdate)
const client = buildClient(flags)
const resp = await checkedFetch(
client.PATCH('/api/v1/service-subscriptions/{id}/alert-sensitivity', {
params: {path: {id: args.subscriptionId}},
body: {alertSensitivity: flags['alert-sensitivity']},
}),
)
display(this, unwrapData(resp), flags.output)
}
}
31 changes: 31 additions & 0 deletions src/commands/services/categories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {Command} from '@oclif/core'
import type {z} from 'zod'
import {apiGetPage} from '../../lib/api-client.js'
import type {components} from '../../lib/api.generated.js'
import {schemas as apiSchemas} from '../../lib/api-zod.generated.js'
import {globalFlags, buildClient, display} from '../../lib/base-command.js'
import type {ColumnDef} from '../../lib/output.js'

type CategoryDto = components['schemas']['CategoryDto']

const COLUMNS: ColumnDef<CategoryDto>[] = [
{header: 'CATEGORY', get: (r) => r.category ?? ''},
{header: 'SERVICES', get: (r) => (r.serviceCount == null ? '' : String(r.serviceCount))},
]

export default class ServicesCategories extends Command {
static description = 'List service catalog categories with service counts'
static examples = ['<%= config.bin %> services categories']
static flags = {...globalFlags}

async run() {
const {flags} = await this.parse(ServicesCategories)
const client = buildClient(flags)
const result = await apiGetPage(
client,
'/api/v1/categories',
apiSchemas.CategoryDto as z.ZodType<CategoryDto>,
)
display(this, result.data, flags.output, COLUMNS as ColumnDef[])
}
}
34 changes: 34 additions & 0 deletions src/commands/services/components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {Command, Args} from '@oclif/core'
import type {z} from 'zod'
import {apiGetPage} from '../../lib/api-client.js'
import type {components} from '../../lib/api.generated.js'
import {schemas as apiSchemas} from '../../lib/api-zod.generated.js'
import {globalFlags, buildClient, display} from '../../lib/base-command.js'
import type {ColumnDef} from '../../lib/output.js'

type ServiceComponentDto = components['schemas']['ServiceComponentDto']

const COLUMNS: ColumnDef<ServiceComponentDto>[] = [
{header: 'ID', get: (r) => r.id ?? ''},
{header: 'NAME', get: (r) => r.name ?? ''},
{header: 'STATUS', get: (r) => r.status ?? ''},
{header: 'GROUP', get: (r) => r.groupId ?? ''},
]

export default class ServicesComponents extends Command {
static description = 'List the components of a service'
static examples = ['<%= config.bin %> services components aws-ec2']
static args = {slug: Args.string({description: 'Service slug or ID', required: true})}
static flags = {...globalFlags}

async run() {
const {args, flags} = await this.parse(ServicesComponents)
const client = buildClient(flags)
const result = await apiGetPage(
client,
`/api/v1/services/${args.slug}/components`,
apiSchemas.ServiceComponentDto as z.ZodType<ServiceComponentDto>,
)
display(this, result.data, flags.output, COLUMNS as ColumnDef[])
}
}
19 changes: 19 additions & 0 deletions src/commands/services/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {Command, Args} from '@oclif/core'
import {globalFlags, buildClient, display} from '../../lib/base-command.js'
import {checkedFetch, unwrapData} from '../../lib/api-client.js'

export default class ServicesGet extends Command {
static description = 'Get full catalog details for a service'
static examples = ['<%= config.bin %> services get stripe']
static args = {slug: Args.string({description: 'Service slug or ID', required: true})}
static flags = {...globalFlags}

async run() {
const {args, flags} = await this.parse(ServicesGet)
const client = buildClient(flags)
const resp = await checkedFetch(
client.GET('/api/v1/services/{slugOrId}', {params: {path: {slugOrId: args.slug}}}),
)
display(this, unwrapData(resp), flags.output)
}
}
67 changes: 67 additions & 0 deletions src/commands/services/incidents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {Command, Args, Flags} from '@oclif/core'
import type {z} from 'zod'
import {apiGetPage, checkedFetch, unwrapData} from '../../lib/api-client.js'
import type {components} from '../../lib/api.generated.js'
import {schemas as apiSchemas} from '../../lib/api-zod.generated.js'
import {globalFlags, buildClient, display} from '../../lib/base-command.js'
import type {ColumnDef} from '../../lib/output.js'

type ServiceIncidentDto = components['schemas']['ServiceIncidentDto']

const COLUMNS: ColumnDef<ServiceIncidentDto>[] = [
{header: 'ID', get: (r) => r.id ?? ''},
{header: 'TITLE', get: (r) => r.title ?? ''},
{header: 'IMPACT', get: (r) => r.impact ?? ''},
{header: 'STATUS', get: (r) => r.status ?? ''},
{header: 'STARTED', get: (r) => r.startedAt ?? ''},
{header: 'RESOLVED', get: (r) => r.resolvedAt ?? ''},
]

export default class ServicesIncidents extends Command {
static description =
'List incidents for a service (or across all services), or show one incident in full detail'
static examples = [
'<%= config.bin %> services incidents stripe',
'<%= config.bin %> services incidents stripe --status active',
'<%= config.bin %> services incidents --from 2026-06-01T00:00:00Z',
'<%= config.bin %> services incidents stripe 22222222-2222-2222-2222-222222222222',
]
static args = {
slug: Args.string({description: 'Service slug or ID (omit to list incidents across all services)'}),
incidentId: Args.string({description: 'Incident ID — show full detail for this incident'}),
}
static flags = {
...globalFlags,
status: Flags.string({description: 'Filter by incident status', options: ['active', 'resolved']}),
from: Flags.string({description: 'ISO-8601 lower bound on incident start time'}),
}

async run() {
const {args, flags} = await this.parse(ServicesIncidents)
const client = buildClient(flags)

if (args.slug && args.incidentId) {
const resp = await checkedFetch(
client.GET('/api/v1/services/{slugOrId}/incidents/{incidentId}', {
params: {path: {slugOrId: args.slug, incidentId: args.incidentId}},
}),
)
display(this, unwrapData(resp), flags.output)
return
}

const path = args.slug
? `/api/v1/services/${args.slug}/incidents`
: '/api/v1/services/incidents'
const query: Record<string, unknown> = {}
if (flags.status) query.status = flags.status
if (flags.from) query.from = flags.from
const result = await apiGetPage(
client,
path,
apiSchemas.ServiceIncidentDto as z.ZodType<ServiceIncidentDto>,
{query},
)
display(this, result.data, flags.output, COLUMNS as ColumnDef[])
}
}
54 changes: 54 additions & 0 deletions src/commands/services/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {Command, Flags} from '@oclif/core'
import type {z} from 'zod'
import {apiGetCursorPage} from '../../lib/api-client.js'
import type {components} from '../../lib/api.generated.js'
import {schemas as apiSchemas} from '../../lib/api-zod.generated.js'
import {globalFlags, buildClient, display} from '../../lib/base-command.js'
import type {ColumnDef} from '../../lib/output.js'

type ServiceCatalogDto = components['schemas']['ServiceCatalogDto']

const COLUMNS: ColumnDef<ServiceCatalogDto>[] = [
{header: 'NAME', get: (r) => r.name ?? ''},
{header: 'SLUG', get: (r) => r.slug ?? ''},
{header: 'CATEGORY', get: (r) => r.category ?? ''},
{header: 'STATUS', get: (r) => r.overallStatus ?? ''},
{header: 'UPTIME 30D', get: (r) => (r.uptime30d == null ? '' : String(r.uptime30d))},
]

export default class ServicesList extends Command {
static description = 'Browse the status-data service catalog (Stripe, GitHub, AWS, ...)'
static examples = [
'<%= config.bin %> services list',
'<%= config.bin %> services list --category cloud --status operational',
'<%= config.bin %> services list --search stripe',
]
static flags = {
...globalFlags,
category: Flags.string({description: 'Filter by category slug'}),
search: Flags.string({description: 'Case-insensitive substring match on service name or slug'}),
status: Flags.string({description: 'Filter by current overall status'}),
limit: Flags.integer({description: 'Page size (1–100)', default: 20}),
cursor: Flags.string({description: 'Opaque cursor from a previous response'}),
}

async run() {
const {flags} = await this.parse(ServicesList)
const client = buildClient(flags)
const query: Record<string, unknown> = {limit: flags.limit}
if (flags.category) query.category = flags.category
if (flags.search) query.search = flags.search
if (flags.status) query.status = flags.status
if (flags.cursor) query.cursor = flags.cursor
const page = await apiGetCursorPage(
client,
'/api/v1/services',
apiSchemas.ServiceCatalogDto as z.ZodType<ServiceCatalogDto>,
{query},
)
display(this, page.data, flags.output, COLUMNS as ColumnDef[])
if (page.hasMore && page.nextCursor && flags.output === 'table') {
this.log(`More results available — re-run with --cursor ${page.nextCursor}`)
}
}
}
Loading
Loading