Skip to content

Commit 7af4828

Browse files
feat(cli): add account members command (#1295)
* feat(cli): add account members command * feat(cli): add account member filters * fix(cli): normalize account member filters * fix(cli): align account member filter help
1 parent aa0df63 commit 7af4828

12 files changed

Lines changed: 591 additions & 5 deletions

File tree

packages/cli/src/ai-context/context.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ export const MANAGE_REFERENCES = [
7272
id: 'manage-plan',
7373
description: 'Check account plan, entitlements, feature limits, and available locations (`account plan`)',
7474
},
75+
{
76+
id: 'manage-account-members',
77+
description: 'List account members and pending invites (`account members`)',
78+
},
7579
] as const
7680

7781
export const SKILL = {
@@ -101,7 +105,7 @@ export const ACTIONS = [
101105
},
102106
{
103107
id: 'manage',
104-
description: 'Understand your account plan, entitlements, and feature limits.',
108+
description: 'Understand your account plan, entitlements, feature limits, members, and pending invites.',
105109
references: MANAGE_REFERENCES,
106110
},
107111
] as const
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Account Members
2+
3+
List active account members and pending or expired account invites.
4+
5+
## Usage
6+
7+
```bash
8+
npx checkly account members
9+
npx checkly account members --output json
10+
npx checkly account members --search alice
11+
npx checkly account members --type invite --status pending
12+
npx checkly account members --role admin
13+
npx checkly account members --limit 25
14+
npx checkly account members --hide-id
15+
```
16+
17+
Flags:
18+
- `--search <term>` — match member names, member emails, and invite emails.
19+
- `--type <type>``member` or `invite` (case-insensitive).
20+
- `--role <role>``owner`, `admin`, `read_write`, `read_run`, or `read_only` (case-insensitive).
21+
- `--status <status>``active`, `pending`, or `expired` (case-insensitive).
22+
- `-l, --limit <n>` — number of rows to return, from 1 to 100.
23+
- `--next-id <cursor>` — cursor for the next page. Requires `--limit`.
24+
- `-o, --output <format>``table` (default), `json`, or `md`.
25+
- `--hide-id` — hide member and invite IDs in table output.
26+
27+
## JSON response shape
28+
29+
```json
30+
{
31+
"members": [
32+
{
33+
"type": "member",
34+
"accountId": "11111111-1111-1111-1111-111111111111",
35+
"userId": "22222222-2222-2222-2222-222222222222",
36+
"name": "Owner User",
37+
"email": "owner@example.com",
38+
"role": "OWNER",
39+
"status": "ACTIVE",
40+
"createdAt": "2026-01-01T00:00:00.000Z",
41+
"updatedAt": "2026-01-02T00:00:00.000Z",
42+
"isSupportMembership": false,
43+
"ssoEnabled": false,
44+
"mfaEnabled": true
45+
},
46+
{
47+
"type": "invite",
48+
"id": "33333333-3333-3333-3333-333333333333",
49+
"accountId": "11111111-1111-1111-1111-111111111111",
50+
"email": "pending@example.com",
51+
"role": "READ_ONLY",
52+
"status": "PENDING",
53+
"inviterEmail": "owner@example.com",
54+
"createdAt": "2026-01-03T00:00:00.000Z",
55+
"updatedAt": "2026-01-03T00:00:00.000Z",
56+
"expiresAt": "2026-02-03T00:00:00.000Z"
57+
}
58+
],
59+
"length": 2,
60+
"nextId": null
61+
}
62+
```
63+
64+
Member roles are returned as `OWNER`, `ADMIN`, `READ_WRITE`, `READ_RUN`, or `READ_ONLY`. Invite roles exclude `OWNER`. Invite statuses are returned as `PENDING` or `EXPIRED`.

packages/cli/src/ai-context/references/manage.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Account Management
22

3-
Understand your account's plan, entitlements, and limits.
3+
Understand your account's plan, entitlements, limits, members, and pending invites.
44

55
## Plan-aware workflow
66

packages/cli/src/ai-context/skill.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: checkly
3-
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.
3+
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.
44
allowed-tools: Bash(npx:checkly:*) Bash(npm:install:*)
55
metadata:
66
author: checkly
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, expect, it } from 'vitest'
2+
import {
3+
normalizeAccountMemberRole,
4+
normalizeAccountMemberStatus,
5+
normalizeAccountMemberType,
6+
} from '../account/members'
7+
8+
describe('account members flag normalization', () => {
9+
it('normalizes type values case-insensitively', () => {
10+
expect(normalizeAccountMemberType('member')).toBe('member')
11+
expect(normalizeAccountMemberType('INVITE')).toBe('invite')
12+
expect(normalizeAccountMemberType(' Invite ')).toBe('invite')
13+
})
14+
15+
it('normalizes role values case-insensitively', () => {
16+
expect(normalizeAccountMemberRole('admin')).toBe('ADMIN')
17+
expect(normalizeAccountMemberRole('Read_Run')).toBe('READ_RUN')
18+
expect(normalizeAccountMemberRole(' read_only ')).toBe('READ_ONLY')
19+
})
20+
21+
it('normalizes status values case-insensitively', () => {
22+
expect(normalizeAccountMemberStatus('active')).toBe('ACTIVE')
23+
expect(normalizeAccountMemberStatus('Pending')).toBe('PENDING')
24+
expect(normalizeAccountMemberStatus(' expired ')).toBe('EXPIRED')
25+
})
26+
27+
it('returns undefined for invalid filter values', () => {
28+
expect(normalizeAccountMemberType('user')).toBeUndefined()
29+
expect(normalizeAccountMemberRole('superadmin')).toBeUndefined()
30+
expect(normalizeAccountMemberStatus('disabled')).toBeUndefined()
31+
})
32+
})

packages/cli/src/commands/__tests__/command-metadata.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ import PwTest from '../pw-test'
3535
import SyncPlaywright from '../sync-playwright'
3636
import SkillsInstall from '../skills/install'
3737
import AccountPlan from '../account/plan'
38+
import AccountMembers from '../account/members'
3839

3940
const commands: Array<[string, typeof BaseCommand]> = [
41+
['account members', AccountMembers],
4042
['account plan', AccountPlan],
4143
['checks list', ChecksList],
4244
['checks get', ChecksGet],
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { Flags } from '@oclif/core'
2+
import { AuthCommand } from '../authCommand'
3+
import { outputFlag } from '../../helpers/flags'
4+
import * as api from '../../rest/api'
5+
import type { OutputFormat } from '../../formatters/render'
6+
import type {
7+
AccountMemberRole,
8+
AccountMemberStatus,
9+
AccountMemberType,
10+
AccountMembersListParams,
11+
} from '../../rest/account-members'
12+
import {
13+
formatAccountMembers,
14+
formatCursorNavigationHints,
15+
formatCursorPaginationInfo,
16+
} from '../../formatters/account-members'
17+
18+
const accountMemberTypes = ['member', 'invite'] as const
19+
const accountMemberRoles = ['OWNER', 'ADMIN', 'READ_WRITE', 'READ_RUN', 'READ_ONLY'] as const
20+
const accountMemberStatuses = ['ACTIVE', 'PENDING', 'EXPIRED'] as const
21+
const accountMemberRoleOptions = accountMemberRoles.map(role => role.toLowerCase())
22+
const accountMemberStatusOptions = accountMemberStatuses.map(status => status.toLowerCase())
23+
24+
function isAccountMemberType (value: string): value is AccountMemberType {
25+
return accountMemberTypes.includes(value as AccountMemberType)
26+
}
27+
28+
function isAccountMemberRole (value: string): value is AccountMemberRole {
29+
return accountMemberRoles.includes(value as AccountMemberRole)
30+
}
31+
32+
function isAccountMemberStatus (value: string): value is AccountMemberStatus {
33+
return accountMemberStatuses.includes(value as AccountMemberStatus)
34+
}
35+
36+
export function normalizeAccountMemberType (value: string | undefined): AccountMemberType | undefined {
37+
if (value === undefined) return undefined
38+
const normalized = value.trim().toLowerCase()
39+
return isAccountMemberType(normalized) ? normalized : undefined
40+
}
41+
42+
export function normalizeAccountMemberRole (value: string | undefined): AccountMemberRole | undefined {
43+
if (value === undefined) return undefined
44+
const normalized = value.trim().toUpperCase()
45+
return isAccountMemberRole(normalized) ? normalized : undefined
46+
}
47+
48+
export function normalizeAccountMemberStatus (value: string | undefined): AccountMemberStatus | undefined {
49+
if (value === undefined) return undefined
50+
const normalized = value.trim().toUpperCase()
51+
return isAccountMemberStatus(normalized) ? normalized : undefined
52+
}
53+
54+
export default class AccountMembers extends AuthCommand {
55+
static hidden = false
56+
static readOnly = true
57+
static idempotent = true
58+
static description = 'List account members and pending invites.'
59+
60+
static flags = {
61+
'search': Flags.string({
62+
char: 's',
63+
description: 'Search members and invites by name or email.',
64+
}),
65+
'type': Flags.string({
66+
description: 'Filter by item type: member or invite.',
67+
}),
68+
'role': Flags.string({
69+
description: `Filter by member or invite role: ${accountMemberRoleOptions.join(', ')}.`,
70+
}),
71+
'status': Flags.string({
72+
description: `Filter by member or invite status: ${accountMemberStatusOptions.join(', ')}.`,
73+
}),
74+
'limit': Flags.integer({
75+
char: 'l',
76+
description: 'Number of account members to return (1-100). Enables cursor pagination.',
77+
}),
78+
'next-id': Flags.string({
79+
description: 'Cursor for next page. Requires --limit.',
80+
}),
81+
'hide-id': Flags.boolean({
82+
description: 'Hide member and invite IDs in table output.',
83+
default: false,
84+
}),
85+
'output': outputFlag({ default: 'table' }),
86+
}
87+
88+
async run (): Promise<void> {
89+
const { flags } = await this.parse(AccountMembers)
90+
this.style.outputFormat = flags.output
91+
const limit = flags.limit
92+
93+
if (limit !== undefined && (limit < 1 || limit > 100)) {
94+
this.error('--limit must be an integer between 1 and 100.')
95+
}
96+
97+
if (flags['next-id'] && limit === undefined) {
98+
this.error('Cannot use --next-id without --limit.')
99+
}
100+
101+
const type = normalizeAccountMemberType(flags.type)
102+
if (flags.type && !type) {
103+
this.error(`Invalid --type "${flags.type}". Valid values: ${accountMemberTypes.join(', ')}.`)
104+
}
105+
106+
const role = normalizeAccountMemberRole(flags.role)
107+
if (flags.role && !role) {
108+
this.error(`Invalid --role "${flags.role}". Valid values: ${accountMemberRoleOptions.join(', ')}.`)
109+
}
110+
111+
const status = normalizeAccountMemberStatus(flags.status)
112+
if (flags.status && !status) {
113+
this.error(`Invalid --status "${flags.status}". Valid values: ${accountMemberStatusOptions.join(', ')}.`)
114+
}
115+
116+
const params: AccountMembersListParams = {
117+
search: flags.search,
118+
type,
119+
role,
120+
status,
121+
limit,
122+
nextId: flags['next-id'],
123+
}
124+
125+
try {
126+
const { data } = await api.accountMembers.getAll(params)
127+
128+
if (flags.output === 'json') {
129+
this.log(JSON.stringify(data, null, 2))
130+
return
131+
}
132+
133+
if (data.members.length === 0) {
134+
this.log('No account members found.')
135+
return
136+
}
137+
138+
const fmt: OutputFormat = flags.output === 'md' ? 'md' : 'terminal'
139+
if (fmt === 'md') {
140+
this.log(formatAccountMembers(data.members, fmt, { showId: !flags['hide-id'] }))
141+
return
142+
}
143+
144+
const output = [
145+
formatAccountMembers(data.members, fmt, { showId: !flags['hide-id'] }),
146+
]
147+
148+
if (limit !== undefined) {
149+
output.push('')
150+
output.push(formatCursorPaginationInfo(data.length, data.nextId))
151+
152+
const navHints = formatCursorNavigationHints(data.nextId)
153+
if (navHints) {
154+
output.push('')
155+
output.push(navHints.replace('<limit>', String(limit)))
156+
}
157+
}
158+
159+
this.log(output.join('\n'))
160+
} catch (err: any) {
161+
this.style.longError('Failed to list account members.', err)
162+
process.exitCode = 1
163+
}
164+
}
165+
}

0 commit comments

Comments
 (0)