Skip to content
Draft
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
6 changes: 5 additions & 1 deletion packages/cli/src/ai-context/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export const MANAGE_REFERENCES = [
id: 'manage-plan',
description: 'Check account plan, entitlements, feature limits, and available locations (`account plan`)',
},
{
id: 'manage-account-members',
description: 'List account members and pending invites (`account members`)',
},
] as const

export const SKILL = {
Expand Down Expand Up @@ -101,7 +105,7 @@ export const ACTIONS = [
},
{
id: 'manage',
description: 'Understand your account plan, entitlements, and feature limits.',
description: 'Understand your account plan, entitlements, feature limits, members, and pending invites.',
references: MANAGE_REFERENCES,
},
] as const
Expand Down
48 changes: 48 additions & 0 deletions packages/cli/src/ai-context/references/manage-account-members.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Account Members

List active account members and pending or expired account invites.

## Usage

```bash
npx checkly account members
npx checkly account members --output json
npx checkly account members --hide-id
```

## JSON response shape

```json
{
"members": [
{
"type": "member",
"accountId": "11111111-1111-1111-1111-111111111111",
"userId": "22222222-2222-2222-2222-222222222222",
"name": "Owner User",
"email": "owner@example.com",
"role": "OWNER",
"status": "ACTIVE",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-02T00:00:00.000Z",
"isSupportMembership": false,
"ssoEnabled": false,
"mfaEnabled": true
},
{
"type": "invite",
"id": "33333333-3333-3333-3333-333333333333",
"accountId": "11111111-1111-1111-1111-111111111111",
"email": "pending@example.com",
"role": "READ_ONLY",
"status": "PENDING",
"inviterEmail": "owner@example.com",
"createdAt": "2026-01-03T00:00:00.000Z",
"updatedAt": "2026-01-03T00:00:00.000Z",
"expiresAt": "2026-02-03T00:00:00.000Z"
}
]
}
```

Member roles are `OWNER`, `ADMIN`, `READ_WRITE`, `READ_RUN`, or `READ_ONLY`. Invite roles exclude `OWNER`. Invite statuses are `PENDING` or `EXPIRED`.
2 changes: 1 addition & 1 deletion packages/cli/src/ai-context/references/manage.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Account Management

Understand your account's plan, entitlements, and limits.
Understand your account's plan, entitlements, limits, members, and pending invites.

## Plan-aware workflow

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/ai-context/skill.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: checkly
description: Set up, create, test and manage monitoring checks using the Checkly CLI. Use when working with Agentic Checks, API Checks, Browser Checks, URL Monitors, ICMP Monitors, Playwright Check Suites, Heartbeat Monitors, Alert Channels, Dashboards, or Status Pages. Access Checkly account plan, entitlements and feature limits.
description: Set up, create, test and manage monitoring checks using the Checkly CLI. Use when working with Agentic Checks, API Checks, Browser Checks, URL Monitors, ICMP Monitors, Playwright Check Suites, Heartbeat Monitors, Alert Channels, Dashboards, or Status Pages. Access Checkly account plan, entitlements, feature limits, members, and pending invites.
allowed-tools: Bash(npx:checkly:*) Bash(npm:install:*)
metadata:
author: checkly
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/__tests__/command-metadata.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ import PwTest from '../pw-test'
import SyncPlaywright from '../sync-playwright'
import SkillsInstall from '../skills/install'
import AccountPlan from '../account/plan'
import AccountMembers from '../account/members'

const commands: Array<[string, typeof BaseCommand]> = [
['account members', AccountMembers],
['account plan', AccountPlan],
['checks list', ChecksList],
['checks get', ChecksGet],
Expand Down
46 changes: 46 additions & 0 deletions packages/cli/src/commands/account/members.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Flags } from '@oclif/core'
import { AuthCommand } from '../authCommand'
import { outputFlag } from '../../helpers/flags'
import * as api from '../../rest/api'
import type { OutputFormat } from '../../formatters/render'
import { formatAccountMembers } from '../../formatters/account-members'

export default class AccountMembers extends AuthCommand {
static hidden = false
static readOnly = true
static idempotent = true
static description = 'List account members and pending invites.'

static flags = {
'hide-id': Flags.boolean({
description: 'Hide member and invite IDs in table output.',
default: false,
}),
'output': outputFlag({ default: 'table' }),
}

async run (): Promise<void> {
const { flags } = await this.parse(AccountMembers)
this.style.outputFormat = flags.output

try {
const { data } = await api.accountMembers.getAll()

if (flags.output === 'json') {
this.log(JSON.stringify(data, null, 2))
return
}

if (data.members.length === 0) {
this.log('No account members found.')
return
}

const fmt: OutputFormat = flags.output === 'md' ? 'md' : 'terminal'
this.log(formatAccountMembers(data.members, fmt, { showId: !flags['hide-id'] }))
} catch (err: any) {
this.style.longError('Failed to list account members.', err)
process.exitCode = 1
}
}
}
87 changes: 87 additions & 0 deletions packages/cli/src/formatters/__tests__/account-members.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, it, expect } from 'vitest'
import { stripAnsi } from '../render'
import { formatAccountMembers } from '../account-members'
import type { AccountMember } from '../../rest/account-members'

const activeMember: AccountMember = {
type: 'member',
accountId: '11111111-1111-1111-1111-111111111111',
userId: '22222222-2222-2222-2222-222222222222',
name: 'Owner User',
email: 'owner@example.com',
role: 'OWNER',
status: 'ACTIVE',
createdAt: '2026-01-01T00:00:00.000Z',
updatedAt: '2026-01-02T00:00:00.000Z',
isSupportMembership: true,
ssoEnabled: true,
mfaEnabled: true,
}

const pendingInvite: AccountMember = {
type: 'invite',
id: '33333333-3333-3333-3333-333333333333',
accountId: '11111111-1111-1111-1111-111111111111',
email: 'pending@example.com',
role: 'READ_ONLY',
status: 'PENDING',
inviterEmail: 'owner@example.com',
createdAt: '2026-01-03T00:00:00.000Z',
updatedAt: '2026-01-03T00:00:00.000Z',
expiresAt: '2026-02-03T00:00:00.000Z',
}

const expiredInvite: AccountMember = {
...pendingInvite,
id: '44444444-4444-4444-4444-444444444444',
email: 'expired@example.com',
status: 'EXPIRED',
}

describe('formatAccountMembers', () => {
it('renders active member security and support fields', () => {
const result = stripAnsi(formatAccountMembers([activeMember], 'terminal'))

expect(result).toContain('TYPE')
expect(result).toContain('owner@example.com')
expect(result).toContain('Owner User')
expect(result).toContain('OWNER')
expect(result).toContain('ACTIVE')
expect(result).toContain('yes')
expect(result).toContain('22222222-2222-2222-2222-222222222222')
})

it('renders pending invite with expiry and invite id', () => {
const result = stripAnsi(formatAccountMembers([pendingInvite], 'terminal'))

expect(result).toContain('invite')
expect(result).toContain('pending@example.com')
expect(result).toContain('READ_ONLY')
expect(result).toContain('PENDING')
expect(result).toContain('2026-02-03 00:00:00')
expect(result).toContain('33333333-3333-3333-3333-333333333333')
})

it('renders expired invite status', () => {
const result = stripAnsi(formatAccountMembers([expiredInvite], 'terminal'))

expect(result).toContain('expired@example.com')
expect(result).toContain('EXPIRED')
})

it('renders markdown output', () => {
const result = formatAccountMembers([activeMember, pendingInvite], 'md')

expect(result).toContain('| Type | Email | Name | Role | Status | MFA | SSO | Support | Expires | ID |')
expect(result).toContain('| member | owner@example.com | Owner User | OWNER | ACTIVE | yes | yes | yes | - | 22222222-2222-2222-2222-222222222222 |')
expect(result).toContain('| invite | pending@example.com | - | READ_ONLY | PENDING | - | - | - | 2026-02-03 00:00:00 UTC | 33333333-3333-3333-3333-333333333333 |')
})

it('hides IDs when requested', () => {
const result = stripAnsi(formatAccountMembers([activeMember, pendingInvite], 'terminal', { showId: false }))

expect(result).not.toContain('ID')
expect(result).not.toContain('22222222-2222-2222-2222-222222222222')
expect(result).not.toContain('33333333-3333-3333-3333-333333333333')
})
})
139 changes: 139 additions & 0 deletions packages/cli/src/formatters/account-members.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import chalk from 'chalk'
import type { AccountMember } from '../rest/account-members'
import {
type ColumnDef,
type OutputFormat,
formatDate,
renderTable,
truncateToWidth,
} from './render'

export interface AccountMembersTableOptions {
showId?: boolean
}

function boolSymbol (value: boolean | undefined, format: OutputFormat): string {
if (value === undefined) return format === 'terminal' ? chalk.dim('-') : '-'
if (format === 'md') return value ? 'yes' : '-'
return value ? chalk.green('yes') : chalk.dim('-')
}

function memberName (member: AccountMember, format: OutputFormat): string {
if (member.type === 'invite') return format === 'terminal' ? chalk.dim('-') : '-'
return member.name ?? (format === 'terminal' ? chalk.dim('-') : '-')
}

function memberId (member: AccountMember): string {
return member.type === 'member' ? member.userId : member.id
}

function expiresAt (member: AccountMember, format: OutputFormat): string {
if (member.type === 'member') return format === 'terminal' ? chalk.dim('-') : '-'
return formatDate(member.expiresAt, format)
}

function buildAccountMemberColumns (
members: AccountMember[],
format: OutputFormat,
options: AccountMembersTableOptions = {},
): ColumnDef<AccountMember>[] {
if (format === 'md') {
const columns: ColumnDef<AccountMember>[] = [
{ header: 'Type', value: m => m.type },
{ header: 'Email', value: m => m.email },
{ header: 'Name', value: (m, fmt) => memberName(m, fmt) },
{ header: 'Role', value: m => m.role },
{ header: 'Status', value: m => m.status },
{ header: 'MFA', value: (m, fmt) => boolSymbol(m.type === 'member' ? m.mfaEnabled : undefined, fmt) },
{ header: 'SSO', value: (m, fmt) => boolSymbol(m.type === 'member' ? m.ssoEnabled : undefined, fmt) },
{ header: 'Support', value: (m, fmt) => boolSymbol(m.type === 'member' ? m.isSupportMembership : undefined, fmt) },
{ header: 'Expires', value: (m, fmt) => expiresAt(m, fmt) },
]

if (options.showId !== false) {
columns.push({ header: 'ID', value: memberId })
}

return columns
}

const showId = options.showId !== false
const termWidth = process.stdout.columns || 120
const idReserve = showId ? 38 : 0
const fixedWidth = 8 + 13 + 10 + 5 + 5 + 9 + 25 + idReserve
const flexibleWidth = Math.max(24, termWidth - fixedWidth)
const hasNames = members.some(member => member.type === 'member' && member.name)
const emailWidth = Math.max(20, Math.min(34, Math.floor(flexibleWidth * (hasNames ? 0.6 : 1))))
const nameWidth = hasNames ? Math.max(14, Math.min(24, flexibleWidth - emailWidth)) : 0

const columns: ColumnDef<AccountMember>[] = [
{
header: 'Type',
width: 8,
value: m => m.type,
},
{
header: 'Email',
width: emailWidth,
value: m => truncateToWidth(m.email, emailWidth - 2),
},
]

if (hasNames) {
columns.push({
header: 'Name',
width: nameWidth,
value: (m, fmt) => truncateToWidth(memberName(m, fmt), nameWidth - 2),
})
}

columns.push(
{
header: 'Role',
width: 13,
value: m => m.role,
},
{
header: 'Status',
width: 10,
value: m => m.status,
},
{
header: 'MFA',
width: 5,
value: (m, fmt) => boolSymbol(m.type === 'member' ? m.mfaEnabled : undefined, fmt),
},
{
header: 'SSO',
width: 5,
value: (m, fmt) => boolSymbol(m.type === 'member' ? m.ssoEnabled : undefined, fmt),
},
{
header: 'Support',
width: 9,
value: (m, fmt) => boolSymbol(m.type === 'member' ? m.isSupportMembership : undefined, fmt),
},
{
header: 'Expires',
width: 25,
value: (m, fmt) => truncateToWidth(expiresAt(m, fmt), 23),
},
)

if (showId) {
columns.push({
header: 'ID',
value: m => chalk.dim(memberId(m)),
})
}

return columns
}

export function formatAccountMembers (
members: AccountMember[],
format: OutputFormat,
options: AccountMembersTableOptions = {},
): string {
return renderTable(buildAccountMemberColumns(members, format, options), members, format)
}
Loading
Loading