agent-messenger 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/README.md +16 -16
- package/.claude-plugin/marketplace.json +29 -29
- package/.claude-plugin/plugin.json +5 -5
- package/CONTRIBUTING.md +1 -1
- package/README.md +8 -5
- package/bun.lock +70 -110
- package/bunfig.toml +3 -0
- package/dist/package.json +11 -3
- package/dist/src/platforms/discordbot/client.js +2 -2
- package/dist/src/platforms/discordbot/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/cli.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/cli.js +2 -1
- package/dist/src/platforms/kakaotalk/cli.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts +2 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +52 -2
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/index.d.ts +1 -0
- package/dist/src/platforms/kakaotalk/commands/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/index.js +1 -0
- package/dist/src/platforms/kakaotalk/commands/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/profile.d.ts +3 -0
- package/dist/src/platforms/kakaotalk/commands/profile.d.ts.map +1 -0
- package/dist/src/platforms/kakaotalk/commands/profile.js +19 -0
- package/dist/src/platforms/kakaotalk/commands/profile.js.map +1 -0
- package/dist/src/platforms/kakaotalk/index.d.ts +2 -2
- package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/index.js +1 -1
- package/dist/src/platforms/kakaotalk/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.js +2 -1
- package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
- package/dist/src/platforms/kakaotalk/types.d.ts +16 -0
- package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/types.js +8 -0
- package/dist/src/platforms/kakaotalk/types.js.map +1 -1
- package/dist/src/platforms/line/client.d.ts.map +1 -1
- package/dist/src/platforms/line/client.js +9 -36
- package/dist/src/platforms/line/client.js.map +1 -1
- package/dist/src/platforms/line/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/line/commands/auth.js +32 -20
- package/dist/src/platforms/line/commands/auth.js.map +1 -1
- package/dist/src/platforms/teams/commands/reaction.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/reaction.js +2 -0
- package/dist/src/platforms/teams/commands/reaction.js.map +1 -1
- package/dist/src/platforms/wechatbot/cli.d.ts +5 -0
- package/dist/src/platforms/wechatbot/cli.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/cli.js +18 -0
- package/dist/src/platforms/wechatbot/cli.js.map +1 -0
- package/dist/src/platforms/wechatbot/client.d.ts +36 -0
- package/dist/src/platforms/wechatbot/client.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/client.js +208 -0
- package/dist/src/platforms/wechatbot/client.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/auth.d.ts +28 -0
- package/dist/src/platforms/wechatbot/commands/auth.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/auth.js +164 -0
- package/dist/src/platforms/wechatbot/commands/auth.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/index.d.ts +5 -0
- package/dist/src/platforms/wechatbot/commands/index.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/index.js +5 -0
- package/dist/src/platforms/wechatbot/commands/index.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/message.d.ts +18 -0
- package/dist/src/platforms/wechatbot/commands/message.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/message.js +80 -0
- package/dist/src/platforms/wechatbot/commands/message.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/shared.d.ts +9 -0
- package/dist/src/platforms/wechatbot/commands/shared.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/shared.js +13 -0
- package/dist/src/platforms/wechatbot/commands/shared.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/template.d.ts +19 -0
- package/dist/src/platforms/wechatbot/commands/template.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/template.js +76 -0
- package/dist/src/platforms/wechatbot/commands/template.js.map +1 -0
- package/dist/src/platforms/wechatbot/commands/user.d.ts +20 -0
- package/dist/src/platforms/wechatbot/commands/user.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/commands/user.js +53 -0
- package/dist/src/platforms/wechatbot/commands/user.js.map +1 -0
- package/dist/src/platforms/wechatbot/credential-manager.d.ts +17 -0
- package/dist/src/platforms/wechatbot/credential-manager.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/credential-manager.js +121 -0
- package/dist/src/platforms/wechatbot/credential-manager.js.map +1 -0
- package/dist/src/platforms/wechatbot/index.d.ts +5 -0
- package/dist/src/platforms/wechatbot/index.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/index.js +4 -0
- package/dist/src/platforms/wechatbot/index.js.map +1 -0
- package/dist/src/platforms/wechatbot/types.d.ts +94 -0
- package/dist/src/platforms/wechatbot/types.d.ts.map +1 -0
- package/dist/src/platforms/wechatbot/types.js +54 -0
- package/dist/src/platforms/wechatbot/types.js.map +1 -0
- package/dist/src/platforms/whatsapp/client.d.ts +1 -0
- package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/client.js +27 -13
- package/dist/src/platforms/whatsapp/client.js.map +1 -1
- package/dist/src/platforms/whatsapp/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/commands/auth.js +21 -18
- package/dist/src/platforms/whatsapp/commands/auth.js.map +1 -1
- package/dist/src/platforms/whatsapp/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/credential-manager.js +14 -8
- package/dist/src/platforms/whatsapp/credential-manager.js.map +1 -1
- package/docs/content/docs/agent-skills.mdx +4 -4
- package/docs/content/docs/cli/channeltalk.mdx +1 -1
- package/docs/content/docs/cli/channeltalkbot.mdx +1 -1
- package/docs/content/docs/cli/discord.mdx +1 -1
- package/docs/content/docs/cli/discordbot.mdx +1 -1
- package/docs/content/docs/cli/instagram.mdx +1 -1
- package/docs/content/docs/cli/kakaotalk.mdx +1 -1
- package/docs/content/docs/cli/line.mdx +1 -1
- package/docs/content/docs/cli/meta.json +1 -0
- package/docs/content/docs/cli/slack.mdx +1 -1
- package/docs/content/docs/cli/slackbot.mdx +1 -1
- package/docs/content/docs/cli/teams.mdx +1 -1
- package/docs/content/docs/cli/webex.mdx +1 -1
- package/docs/content/docs/cli/wechatbot.mdx +179 -0
- package/docs/content/docs/cli/whatsapp.mdx +1 -1
- package/docs/content/docs/cli/whatsappbot.mdx +1 -1
- package/docs/content/docs/sdk/meta.json +1 -1
- package/docs/content/docs/sdk/wechatbot.mdx +282 -0
- package/docs/content/docs/tui.mdx +1 -1
- package/docs/src/app/page.tsx +5 -5
- package/package.json +11 -3
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +24 -1
- package/skills/agent-line/SKILL.md +7 -11
- package/skills/agent-line/references/authentication.md +13 -4
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +385 -0
- package/skills/agent-whatsapp/SKILL.md +12 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/discord/credential-manager.test.ts +18 -1
- package/src/platforms/discordbot/client.ts +2 -2
- package/src/platforms/instagram/commands/auth.test.ts +216 -0
- package/src/platforms/instagram/commands/chat.test.ts +127 -0
- package/src/platforms/instagram/commands/message.test.ts +178 -0
- package/src/platforms/kakaotalk/cli.ts +2 -1
- package/src/platforms/kakaotalk/client.test.ts +157 -0
- package/src/platforms/kakaotalk/client.ts +57 -3
- package/src/platforms/kakaotalk/commands/auth.test.ts +299 -0
- package/src/platforms/kakaotalk/commands/chat.test.ts +97 -0
- package/src/platforms/kakaotalk/commands/index.ts +1 -0
- package/src/platforms/kakaotalk/commands/message.test.ts +113 -0
- package/src/platforms/kakaotalk/commands/profile.test.ts +84 -0
- package/src/platforms/kakaotalk/commands/profile.ts +21 -0
- package/src/platforms/kakaotalk/index.test.ts +5 -0
- package/src/platforms/kakaotalk/index.ts +2 -0
- package/src/platforms/kakaotalk/protocol/session.ts +2 -0
- package/src/platforms/kakaotalk/types.ts +18 -0
- package/src/platforms/line/client.ts +14 -39
- package/src/platforms/line/commands/auth.test.ts +141 -0
- package/src/platforms/line/commands/auth.ts +28 -19
- package/src/platforms/line/commands/chat.test.ts +110 -0
- package/src/platforms/line/commands/friend.test.ts +98 -0
- package/src/platforms/line/commands/message.test.ts +119 -0
- package/src/platforms/line/commands/profile.test.ts +85 -0
- package/src/platforms/slackbot/commands/channel.test.ts +139 -0
- package/src/platforms/slackbot/commands/message.test.ts +226 -0
- package/src/platforms/slackbot/commands/reaction.test.ts +90 -0
- package/src/platforms/slackbot/commands/user.test.ts +143 -0
- package/src/platforms/teams/commands/reaction.test.ts +45 -61
- package/src/platforms/teams/commands/reaction.ts +2 -0
- package/src/platforms/telegram/commands/chat.test.ts +125 -0
- package/src/platforms/telegram/commands/message.test.ts +92 -0
- package/src/platforms/webex/commands/member.test.ts +65 -58
- package/src/platforms/webex/commands/message.test.ts +78 -121
- package/src/platforms/webex/commands/snapshot.test.ts +59 -46
- package/src/platforms/webex/commands/space.test.ts +49 -48
- package/src/platforms/wechatbot/cli.ts +24 -0
- package/src/platforms/wechatbot/client.test.ts +497 -0
- package/src/platforms/wechatbot/client.ts +268 -0
- package/src/platforms/wechatbot/commands/auth.test.ts +211 -0
- package/src/platforms/wechatbot/commands/auth.ts +203 -0
- package/src/platforms/wechatbot/commands/index.ts +4 -0
- package/src/platforms/wechatbot/commands/message.test.ts +155 -0
- package/src/platforms/wechatbot/commands/message.ts +104 -0
- package/src/platforms/wechatbot/commands/shared.ts +22 -0
- package/src/platforms/wechatbot/commands/template.test.ts +199 -0
- package/src/platforms/wechatbot/commands/template.ts +102 -0
- package/src/platforms/wechatbot/commands/user.test.ts +165 -0
- package/src/platforms/wechatbot/commands/user.ts +75 -0
- package/src/platforms/wechatbot/credential-manager.test.ts +255 -0
- package/src/platforms/wechatbot/credential-manager.ts +148 -0
- package/src/platforms/wechatbot/index.test.ts +49 -0
- package/src/platforms/wechatbot/index.ts +19 -0
- package/src/platforms/wechatbot/types.test.ts +223 -0
- package/src/platforms/wechatbot/types.ts +107 -0
- package/src/platforms/whatsapp/client.ts +24 -13
- package/src/platforms/whatsapp/commands/auth.test.ts +311 -0
- package/src/platforms/whatsapp/commands/auth.ts +21 -17
- package/src/platforms/whatsapp/commands/chat.test.ts +198 -0
- package/src/platforms/whatsapp/commands/message.test.ts +231 -0
- package/src/platforms/whatsapp/credential-manager.test.ts +20 -0
- package/src/platforms/whatsapp/credential-manager.ts +17 -8
- package/src/platforms/whatsappbot/commands/auth.test.ts +217 -0
- package/src/platforms/whatsappbot/commands/message.test.ts +198 -0
- package/src/platforms/whatsappbot/commands/template.test.ts +112 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import type { WeChatBotNewsArticle, WeChatBotTemplate, WeChatBotUserInfo } from './types'
|
|
2
|
+
import { WeChatBotError } from './types'
|
|
3
|
+
|
|
4
|
+
const BASE_URL = 'https://api.weixin.qq.com'
|
|
5
|
+
const MAX_RETRIES = 3
|
|
6
|
+
const BASE_BACKOFF_MS = 100
|
|
7
|
+
|
|
8
|
+
export class WeChatBotClient {
|
|
9
|
+
private appId: string | null = null
|
|
10
|
+
private appSecret: string | null = null
|
|
11
|
+
private accessToken: string | null = null
|
|
12
|
+
private tokenExpiresAt: number = 0
|
|
13
|
+
|
|
14
|
+
async login(credentials?: { appId: string; appSecret: string }): Promise<this> {
|
|
15
|
+
if (credentials) {
|
|
16
|
+
if (!credentials.appId) {
|
|
17
|
+
throw new WeChatBotError('App ID is required', 'missing_app_id')
|
|
18
|
+
}
|
|
19
|
+
if (!credentials.appSecret) {
|
|
20
|
+
throw new WeChatBotError('App Secret is required', 'missing_app_secret')
|
|
21
|
+
}
|
|
22
|
+
this.appId = credentials.appId
|
|
23
|
+
this.appSecret = credentials.appSecret
|
|
24
|
+
return this
|
|
25
|
+
}
|
|
26
|
+
const { WeChatBotCredentialManager } = await import('./credential-manager')
|
|
27
|
+
const credManager = new WeChatBotCredentialManager()
|
|
28
|
+
const creds = await credManager.getCredentials()
|
|
29
|
+
if (!creds) {
|
|
30
|
+
throw new WeChatBotError(
|
|
31
|
+
'No WeChat Bot credentials found. Run "agent-wechatbot auth set" first.',
|
|
32
|
+
'no_credentials',
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
return this.login({ appId: creds.app_id, appSecret: creds.app_secret })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private ensureAuth(): void {
|
|
39
|
+
if (this.appId === null) {
|
|
40
|
+
throw new WeChatBotError('Not authenticated. Call .login() first.', 'not_authenticated')
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async ensureAccessToken(): Promise<string> {
|
|
45
|
+
const now = Date.now()
|
|
46
|
+
if (this.accessToken && now < this.tokenExpiresAt) {
|
|
47
|
+
return this.accessToken
|
|
48
|
+
}
|
|
49
|
+
const result = await this.fetchAccessToken()
|
|
50
|
+
this.accessToken = result.access_token
|
|
51
|
+
this.tokenExpiresAt = now + (result.expires_in - 10) * 1000
|
|
52
|
+
return this.accessToken
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private async fetchAccessToken(): Promise<{ access_token: string; expires_in: number }> {
|
|
56
|
+
const url = `${BASE_URL}/cgi-bin/token?grant_type=client_credential&appid=${this.appId}&secret=${this.appSecret}`
|
|
57
|
+
let response: Response
|
|
58
|
+
try {
|
|
59
|
+
response = await fetch(url)
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
62
|
+
throw new WeChatBotError(`Network error fetching access token: ${msg}`, 'network_error')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const data = (await response.json()) as { access_token?: string; expires_in?: number; errcode?: number; errmsg?: string }
|
|
66
|
+
|
|
67
|
+
if (data.errcode) {
|
|
68
|
+
throw new WeChatBotError(data.errmsg ?? `Error ${data.errcode}`, String(data.errcode))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!data.access_token) {
|
|
72
|
+
throw new WeChatBotError('Failed to obtain access token', 'token_fetch_failed')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { access_token: data.access_token, expires_in: data.expires_in ?? 7200 }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async verifyCredentials(): Promise<boolean> {
|
|
79
|
+
this.ensureAuth()
|
|
80
|
+
try {
|
|
81
|
+
await this.fetchAccessToken()
|
|
82
|
+
return true
|
|
83
|
+
} catch {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async sendTextMessage(openId: string, text: string): Promise<void> {
|
|
89
|
+
await this.request<void>('POST', '/cgi-bin/message/custom/send', {
|
|
90
|
+
touser: openId,
|
|
91
|
+
msgtype: 'text',
|
|
92
|
+
text: { content: text },
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async sendImageMessage(openId: string, mediaId: string): Promise<void> {
|
|
97
|
+
await this.request<void>('POST', '/cgi-bin/message/custom/send', {
|
|
98
|
+
touser: openId,
|
|
99
|
+
msgtype: 'image',
|
|
100
|
+
image: { media_id: mediaId },
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async sendNewsMessage(openId: string, articles: WeChatBotNewsArticle[]): Promise<void> {
|
|
105
|
+
await this.request<void>('POST', '/cgi-bin/message/custom/send', {
|
|
106
|
+
touser: openId,
|
|
107
|
+
msgtype: 'news',
|
|
108
|
+
news: { articles },
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async sendTemplateMessage(
|
|
113
|
+
openId: string,
|
|
114
|
+
templateId: string,
|
|
115
|
+
data: Record<string, { value: string }>,
|
|
116
|
+
url?: string,
|
|
117
|
+
): Promise<{ msgid: number }> {
|
|
118
|
+
return this.request<{ msgid: number }>('POST', '/cgi-bin/message/template/send', {
|
|
119
|
+
touser: openId,
|
|
120
|
+
template_id: templateId,
|
|
121
|
+
url,
|
|
122
|
+
data,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async listTemplates(): Promise<WeChatBotTemplate[]> {
|
|
127
|
+
const result = await this.request<{ template_list: WeChatBotTemplate[] }>(
|
|
128
|
+
'GET',
|
|
129
|
+
'/cgi-bin/template/get_all_private_template',
|
|
130
|
+
)
|
|
131
|
+
return result.template_list
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async deleteTemplate(templateId: string): Promise<void> {
|
|
135
|
+
await this.request<void>('POST', '/cgi-bin/template/del_private_template', {
|
|
136
|
+
template_id: templateId,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async getFollowers(nextOpenId?: string): Promise<{
|
|
141
|
+
total: number
|
|
142
|
+
count: number
|
|
143
|
+
openids: string[]
|
|
144
|
+
next_openid: string
|
|
145
|
+
}> {
|
|
146
|
+
const path = this.buildPath('/cgi-bin/user/get', nextOpenId ? { next_openid: nextOpenId } : undefined)
|
|
147
|
+
const result = await this.request<{
|
|
148
|
+
total: number
|
|
149
|
+
count: number
|
|
150
|
+
data: { openid: string[] }
|
|
151
|
+
next_openid: string
|
|
152
|
+
}>('GET', path)
|
|
153
|
+
return {
|
|
154
|
+
total: result.total,
|
|
155
|
+
count: result.count,
|
|
156
|
+
openids: result.data?.openid ?? [],
|
|
157
|
+
next_openid: result.next_openid,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async getUserInfo(openId: string, lang = 'zh_CN'): Promise<WeChatBotUserInfo> {
|
|
162
|
+
const path = this.buildPath('/cgi-bin/user/info', { openid: openId, lang })
|
|
163
|
+
return this.request<WeChatBotUserInfo>('GET', path)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
167
|
+
this.ensureAuth()
|
|
168
|
+
let lastError: Error | undefined
|
|
169
|
+
let tokenInvalidated = false
|
|
170
|
+
|
|
171
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
172
|
+
const token = await this.ensureAccessToken()
|
|
173
|
+
const url = `${BASE_URL}${path}${path.includes('?') ? '&' : '?'}access_token=${token}`
|
|
174
|
+
|
|
175
|
+
const options: RequestInit = {
|
|
176
|
+
method,
|
|
177
|
+
headers: {
|
|
178
|
+
'Content-Type': 'application/json',
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (body !== undefined) {
|
|
183
|
+
options.body = JSON.stringify(body)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let response: Response
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
response = await fetch(url, options)
|
|
190
|
+
} catch (error) {
|
|
191
|
+
lastError = error instanceof Error ? error : new Error(String(error))
|
|
192
|
+
if (attempt < MAX_RETRIES && method === 'GET') {
|
|
193
|
+
await this.sleep(BASE_BACKOFF_MS * 2 ** attempt)
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
throw new WeChatBotError(`Network error: ${lastError.message}`, 'network_error')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (response.status >= 500 && response.status <= 599) {
|
|
200
|
+
if (attempt < MAX_RETRIES && method === 'GET') {
|
|
201
|
+
await this.sleep(BASE_BACKOFF_MS * 2 ** attempt)
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
throw new WeChatBotError(`HTTP ${response.status}`, `http_${response.status}`)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const data = (await response.json()) as {
|
|
208
|
+
errcode?: number
|
|
209
|
+
errmsg?: string
|
|
210
|
+
[key: string]: unknown
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (data.errcode === 40001 || data.errcode === 42001) {
|
|
214
|
+
if (!tokenInvalidated) {
|
|
215
|
+
tokenInvalidated = true
|
|
216
|
+
this.accessToken = null
|
|
217
|
+
this.tokenExpiresAt = 0
|
|
218
|
+
continue
|
|
219
|
+
}
|
|
220
|
+
throw new WeChatBotError(data.errmsg ?? `Error ${data.errcode}`, String(data.errcode))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (data.errcode === -1) {
|
|
224
|
+
if (attempt < MAX_RETRIES) {
|
|
225
|
+
await this.sleep(BASE_BACKOFF_MS * 2 ** attempt)
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
throw new WeChatBotError('WeChat system busy', '-1')
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (data.errcode === 45009) {
|
|
232
|
+
throw new WeChatBotError('WeChat API frequency limit exceeded', '45009')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (data.errcode && data.errcode !== 0) {
|
|
236
|
+
throw new WeChatBotError(data.errmsg ?? `Error ${data.errcode}`, String(data.errcode))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return data as T
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
throw lastError ?? new WeChatBotError('Request failed after retries', 'max_retries')
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private buildPath(path: string, params?: Record<string, string | number | undefined>): string {
|
|
246
|
+
if (!params) {
|
|
247
|
+
return path
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const searchParams = new URLSearchParams()
|
|
251
|
+
for (const [key, value] of Object.entries(params)) {
|
|
252
|
+
if (value !== undefined) {
|
|
253
|
+
searchParams.set(key, String(value))
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const query = searchParams.toString()
|
|
258
|
+
if (!query) {
|
|
259
|
+
return path
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return `${path}?${query}`
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private sleep(ms: number): Promise<void> {
|
|
266
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { afterAll, describe, expect, mock, test } from 'bun:test'
|
|
2
|
+
import { rmSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { WeChatBotCredentialManager } from '@/platforms/wechatbot/credential-manager'
|
|
6
|
+
|
|
7
|
+
mock.module('../client', () => ({
|
|
8
|
+
WeChatBotClient: class MockWeChatBotClient {
|
|
9
|
+
async login() {
|
|
10
|
+
return this
|
|
11
|
+
}
|
|
12
|
+
verifyCredentials = mock(() => Promise.resolve(true))
|
|
13
|
+
},
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
const { setAction, statusAction, clearAction, listAction, useAction, removeAction } = await import(
|
|
17
|
+
'@/platforms/wechatbot/commands/auth'
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const testDirs: string[] = []
|
|
21
|
+
|
|
22
|
+
function makeCredManager(): WeChatBotCredentialManager {
|
|
23
|
+
const dir = join(
|
|
24
|
+
import.meta.dir,
|
|
25
|
+
`.test-auth-config-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
26
|
+
)
|
|
27
|
+
testDirs.push(dir)
|
|
28
|
+
return new WeChatBotCredentialManager(dir)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
afterAll(() => {
|
|
32
|
+
for (const dir of testDirs) {
|
|
33
|
+
rmSync(dir, { recursive: true, force: true })
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('setAction', () => {
|
|
38
|
+
test('returns success with app_id when credentials are valid', async () => {
|
|
39
|
+
const credManager = makeCredManager()
|
|
40
|
+
const result = await setAction('wx123', 'secret123', { _credManager: credManager })
|
|
41
|
+
|
|
42
|
+
expect(result.success).toBe(true)
|
|
43
|
+
expect(result.app_id).toBe('wx123')
|
|
44
|
+
expect(result.account_name).toBe('wx123')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('saves credentials to credManager', async () => {
|
|
48
|
+
const credManager = makeCredManager()
|
|
49
|
+
await setAction('wx456', 'secret456', { _credManager: credManager })
|
|
50
|
+
|
|
51
|
+
const creds = await credManager.getCredentials()
|
|
52
|
+
expect(creds?.app_id).toBe('wx456')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('returns error when verifyCredentials returns false', async () => {
|
|
56
|
+
mock.module('../client', () => ({
|
|
57
|
+
WeChatBotClient: class MockWeChatBotClient {
|
|
58
|
+
async login() {
|
|
59
|
+
return this
|
|
60
|
+
}
|
|
61
|
+
verifyCredentials = mock(() => Promise.resolve(false))
|
|
62
|
+
},
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
const { setAction: setActionFresh } = await import('@/platforms/wechatbot/commands/auth')
|
|
66
|
+
const credManager = makeCredManager()
|
|
67
|
+
const result = await setActionFresh('wx-bad', 'bad-secret', { _credManager: credManager })
|
|
68
|
+
|
|
69
|
+
expect(result.error).toBeDefined()
|
|
70
|
+
expect(result.success).toBeUndefined()
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('statusAction', () => {
|
|
75
|
+
test('returns valid: false when no credentials configured', async () => {
|
|
76
|
+
const credManager = makeCredManager()
|
|
77
|
+
const result = await statusAction({ _credManager: credManager })
|
|
78
|
+
|
|
79
|
+
expect(result.valid).toBe(false)
|
|
80
|
+
expect(result.error).toBeDefined()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('returns valid: true when credentials exist and are valid', async () => {
|
|
84
|
+
mock.module('../client', () => ({
|
|
85
|
+
WeChatBotClient: class MockWeChatBotClient {
|
|
86
|
+
async login() {
|
|
87
|
+
return this
|
|
88
|
+
}
|
|
89
|
+
verifyCredentials = mock(() => Promise.resolve(true))
|
|
90
|
+
},
|
|
91
|
+
}))
|
|
92
|
+
|
|
93
|
+
const { statusAction: statusActionFresh } = await import('@/platforms/wechatbot/commands/auth')
|
|
94
|
+
const credManager = makeCredManager()
|
|
95
|
+
await credManager.setCredentials({ app_id: 'wx123', app_secret: 'secret123', account_name: 'My Account' })
|
|
96
|
+
|
|
97
|
+
const result = await statusActionFresh({ _credManager: credManager })
|
|
98
|
+
|
|
99
|
+
expect(result.valid).toBe(true)
|
|
100
|
+
expect(result.app_id).toBe('wx123')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('returns valid: false when credentials exist but are invalid', async () => {
|
|
104
|
+
mock.module('../client', () => ({
|
|
105
|
+
WeChatBotClient: class MockWeChatBotClient {
|
|
106
|
+
async login() {
|
|
107
|
+
return this
|
|
108
|
+
}
|
|
109
|
+
verifyCredentials = mock(() => Promise.resolve(false))
|
|
110
|
+
},
|
|
111
|
+
}))
|
|
112
|
+
|
|
113
|
+
const { statusAction: statusActionFresh } = await import('@/platforms/wechatbot/commands/auth')
|
|
114
|
+
const credManager = makeCredManager()
|
|
115
|
+
await credManager.setCredentials({ app_id: 'wx123', app_secret: 'bad-secret', account_name: 'My Account' })
|
|
116
|
+
|
|
117
|
+
const result = await statusActionFresh({ _credManager: credManager })
|
|
118
|
+
|
|
119
|
+
expect(result.valid).toBe(false)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('returns error message when account not found', async () => {
|
|
123
|
+
const credManager = makeCredManager()
|
|
124
|
+
const result = await statusAction({ account: 'nonexistent', _credManager: credManager })
|
|
125
|
+
|
|
126
|
+
expect(result.valid).toBe(false)
|
|
127
|
+
expect(result.error).toContain('nonexistent')
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('clearAction', () => {
|
|
132
|
+
test('clears all credentials and returns success', async () => {
|
|
133
|
+
const credManager = makeCredManager()
|
|
134
|
+
await credManager.setCredentials({ app_id: 'wx123', app_secret: 'secret123', account_name: 'My Account' })
|
|
135
|
+
|
|
136
|
+
const result = await clearAction({ _credManager: credManager })
|
|
137
|
+
|
|
138
|
+
expect(result.success).toBe(true)
|
|
139
|
+
|
|
140
|
+
const creds = await credManager.getCredentials()
|
|
141
|
+
expect(creds).toBeNull()
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('listAction', () => {
|
|
146
|
+
test('returns empty accounts array when none configured', async () => {
|
|
147
|
+
const credManager = makeCredManager()
|
|
148
|
+
const result = await listAction({ _credManager: credManager })
|
|
149
|
+
|
|
150
|
+
expect(result.accounts).toEqual([])
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('returns all accounts with is_current flag', async () => {
|
|
154
|
+
const credManager = makeCredManager()
|
|
155
|
+
await credManager.setCredentials({ app_id: 'wx-a', app_secret: 'secret-a', account_name: 'Account A' })
|
|
156
|
+
await credManager.setCredentials({ app_id: 'wx-b', app_secret: 'secret-b', account_name: 'Account B' })
|
|
157
|
+
|
|
158
|
+
const result = await listAction({ _credManager: credManager })
|
|
159
|
+
|
|
160
|
+
expect(result.accounts).toHaveLength(2)
|
|
161
|
+
const acctA = result.accounts?.find((a) => a.app_id === 'wx-a')
|
|
162
|
+
const acctB = result.accounts?.find((a) => a.app_id === 'wx-b')
|
|
163
|
+
expect(acctA?.is_current).toBe(false)
|
|
164
|
+
expect(acctB?.is_current).toBe(true)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('useAction', () => {
|
|
169
|
+
test('switches to specified account and returns success', async () => {
|
|
170
|
+
const credManager = makeCredManager()
|
|
171
|
+
await credManager.setCredentials({ app_id: 'wx-a', app_secret: 'secret-a', account_name: 'Account A' })
|
|
172
|
+
await credManager.setCredentials({ app_id: 'wx-b', app_secret: 'secret-b', account_name: 'Account B' })
|
|
173
|
+
|
|
174
|
+
const result = await useAction('wx-a', { _credManager: credManager })
|
|
175
|
+
|
|
176
|
+
expect(result.success).toBe(true)
|
|
177
|
+
expect(result.app_id).toBe('wx-a')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('returns error when account not found', async () => {
|
|
181
|
+
const credManager = makeCredManager()
|
|
182
|
+
const result = await useAction('nonexistent', { _credManager: credManager })
|
|
183
|
+
|
|
184
|
+
expect(result.error).toBeDefined()
|
|
185
|
+
expect(result.error).toContain('nonexistent')
|
|
186
|
+
expect(result.success).toBeUndefined()
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
describe('removeAction', () => {
|
|
191
|
+
test('removes specified account and returns success', async () => {
|
|
192
|
+
const credManager = makeCredManager()
|
|
193
|
+
await credManager.setCredentials({ app_id: 'wx123', app_secret: 'secret123', account_name: 'My Account' })
|
|
194
|
+
|
|
195
|
+
const result = await removeAction('wx123', { _credManager: credManager })
|
|
196
|
+
|
|
197
|
+
expect(result.success).toBe(true)
|
|
198
|
+
|
|
199
|
+
const creds = await credManager.getCredentials()
|
|
200
|
+
expect(creds).toBeNull()
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('returns error when account not found', async () => {
|
|
204
|
+
const credManager = makeCredManager()
|
|
205
|
+
const result = await removeAction('nonexistent', { _credManager: credManager })
|
|
206
|
+
|
|
207
|
+
expect(result.error).toBeDefined()
|
|
208
|
+
expect(result.error).toContain('nonexistent')
|
|
209
|
+
expect(result.success).toBeUndefined()
|
|
210
|
+
})
|
|
211
|
+
})
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
|
|
3
|
+
import { formatOutput } from '@/shared/utils/output'
|
|
4
|
+
|
|
5
|
+
import { WeChatBotClient } from '../client'
|
|
6
|
+
import { WeChatBotCredentialManager } from '../credential-manager'
|
|
7
|
+
|
|
8
|
+
interface ActionOptions {
|
|
9
|
+
account?: string
|
|
10
|
+
pretty?: boolean
|
|
11
|
+
_credManager?: WeChatBotCredentialManager
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ActionResult {
|
|
15
|
+
success?: boolean
|
|
16
|
+
error?: string
|
|
17
|
+
valid?: boolean
|
|
18
|
+
app_id?: string
|
|
19
|
+
account_name?: string
|
|
20
|
+
accounts?: Array<{ app_id: string; account_name: string; is_current: boolean }>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function setAction(appId: string, appSecret: string, options: ActionOptions): Promise<ActionResult> {
|
|
24
|
+
try {
|
|
25
|
+
const client = await new WeChatBotClient().login({ appId, appSecret })
|
|
26
|
+
const valid = await client.verifyCredentials()
|
|
27
|
+
|
|
28
|
+
if (!valid) {
|
|
29
|
+
return { error: 'Invalid credentials. Could not obtain access token.' }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const credManager = options._credManager ?? new WeChatBotCredentialManager()
|
|
33
|
+
await credManager.setCredentials({
|
|
34
|
+
app_id: appId,
|
|
35
|
+
app_secret: appSecret,
|
|
36
|
+
account_name: appId,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
return { success: true, app_id: appId, account_name: appId }
|
|
40
|
+
} catch (error: unknown) {
|
|
41
|
+
return { error: (error as Error).message }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function statusAction(options: ActionOptions): Promise<ActionResult> {
|
|
46
|
+
try {
|
|
47
|
+
const credManager = options._credManager ?? new WeChatBotCredentialManager()
|
|
48
|
+
const creds = await credManager.getCredentials(options.account)
|
|
49
|
+
|
|
50
|
+
if (!creds) {
|
|
51
|
+
return {
|
|
52
|
+
valid: false,
|
|
53
|
+
error: options.account
|
|
54
|
+
? `Account "${options.account}" not found. Run "auth list" to see available accounts.`
|
|
55
|
+
: 'No credentials configured. Run "auth set <app-id> <app-secret>" first.',
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let valid = false
|
|
60
|
+
let appId: string | undefined
|
|
61
|
+
let accountName: string | undefined
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const client = await new WeChatBotClient().login({ appId: creds.app_id, appSecret: creds.app_secret })
|
|
65
|
+
valid = await client.verifyCredentials()
|
|
66
|
+
appId = creds.app_id
|
|
67
|
+
accountName = creds.account_name
|
|
68
|
+
} catch {
|
|
69
|
+
valid = false
|
|
70
|
+
appId = creds.app_id
|
|
71
|
+
accountName = creds.account_name
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { valid, app_id: appId, account_name: accountName }
|
|
75
|
+
} catch (error: unknown) {
|
|
76
|
+
return { error: (error as Error).message }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function clearAction(options: ActionOptions): Promise<ActionResult> {
|
|
81
|
+
try {
|
|
82
|
+
const credManager = options._credManager ?? new WeChatBotCredentialManager()
|
|
83
|
+
await credManager.clearCredentials()
|
|
84
|
+
return { success: true }
|
|
85
|
+
} catch (error: unknown) {
|
|
86
|
+
return { error: (error as Error).message }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function listAction(options: ActionOptions): Promise<ActionResult> {
|
|
91
|
+
try {
|
|
92
|
+
const credManager = options._credManager ?? new WeChatBotCredentialManager()
|
|
93
|
+
const all = await credManager.listAll()
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
accounts: all.map((a) => ({
|
|
97
|
+
app_id: a.app_id,
|
|
98
|
+
account_name: a.account_name,
|
|
99
|
+
is_current: a.is_current,
|
|
100
|
+
})),
|
|
101
|
+
}
|
|
102
|
+
} catch (error: unknown) {
|
|
103
|
+
return { error: (error as Error).message }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function useAction(accountId: string, options: ActionOptions): Promise<ActionResult> {
|
|
108
|
+
try {
|
|
109
|
+
const credManager = options._credManager ?? new WeChatBotCredentialManager()
|
|
110
|
+
const found = await credManager.setCurrent(accountId)
|
|
111
|
+
|
|
112
|
+
if (!found) {
|
|
113
|
+
return { error: `Account "${accountId}" not found. Run "auth list" to see available accounts.` }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const creds = await credManager.getCredentials()
|
|
117
|
+
return {
|
|
118
|
+
success: true,
|
|
119
|
+
app_id: creds?.app_id,
|
|
120
|
+
account_name: creds?.account_name,
|
|
121
|
+
}
|
|
122
|
+
} catch (error: unknown) {
|
|
123
|
+
return { error: (error as Error).message }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function removeAction(accountId: string, options: ActionOptions): Promise<ActionResult> {
|
|
128
|
+
try {
|
|
129
|
+
const credManager = options._credManager ?? new WeChatBotCredentialManager()
|
|
130
|
+
const removed = await credManager.removeAccount(accountId)
|
|
131
|
+
|
|
132
|
+
if (!removed) {
|
|
133
|
+
return { error: `Account "${accountId}" not found. Run "auth list" to see available accounts.` }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { success: true }
|
|
137
|
+
} catch (error: unknown) {
|
|
138
|
+
return { error: (error as Error).message }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function cliOutput(result: ActionResult, pretty?: boolean, exitOnError = true): void {
|
|
143
|
+
console.log(formatOutput(result, pretty))
|
|
144
|
+
if (result.error && exitOnError) process.exit(1)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const authCommand = new Command('auth')
|
|
148
|
+
.description('Authentication commands')
|
|
149
|
+
.addCommand(
|
|
150
|
+
new Command('set')
|
|
151
|
+
.description('Set account credentials')
|
|
152
|
+
.argument('<app-id>', 'WeChat Official Account App ID')
|
|
153
|
+
.argument('<app-secret>', 'WeChat Official Account App Secret')
|
|
154
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
155
|
+
.action(async (appId: string, appSecret: string, opts: { pretty?: boolean }) => {
|
|
156
|
+
cliOutput(await setAction(appId, appSecret, opts), opts.pretty)
|
|
157
|
+
}),
|
|
158
|
+
)
|
|
159
|
+
.addCommand(
|
|
160
|
+
new Command('status')
|
|
161
|
+
.description('Show authentication status')
|
|
162
|
+
.option('--account <id>', 'Check specific account (default: current)')
|
|
163
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
164
|
+
.action(async (opts: { account?: string; pretty?: boolean }) => {
|
|
165
|
+
const result = await statusAction(opts)
|
|
166
|
+
console.log(formatOutput(result, opts.pretty))
|
|
167
|
+
if (!result.valid) process.exit(1)
|
|
168
|
+
}),
|
|
169
|
+
)
|
|
170
|
+
.addCommand(
|
|
171
|
+
new Command('clear')
|
|
172
|
+
.description('Clear all stored credentials')
|
|
173
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
174
|
+
.action(async (opts: { pretty?: boolean }) => {
|
|
175
|
+
cliOutput(await clearAction(opts), opts.pretty)
|
|
176
|
+
}),
|
|
177
|
+
)
|
|
178
|
+
.addCommand(
|
|
179
|
+
new Command('list')
|
|
180
|
+
.description('List all stored accounts')
|
|
181
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
182
|
+
.action(async (opts: { pretty?: boolean }) => {
|
|
183
|
+
cliOutput(await listAction(opts), opts.pretty)
|
|
184
|
+
}),
|
|
185
|
+
)
|
|
186
|
+
.addCommand(
|
|
187
|
+
new Command('use')
|
|
188
|
+
.description('Switch active account')
|
|
189
|
+
.argument('<account-id>', 'Account ID (App ID)')
|
|
190
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
191
|
+
.action(async (accountId: string, opts: { pretty?: boolean }) => {
|
|
192
|
+
cliOutput(await useAction(accountId, opts), opts.pretty)
|
|
193
|
+
}),
|
|
194
|
+
)
|
|
195
|
+
.addCommand(
|
|
196
|
+
new Command('remove')
|
|
197
|
+
.description('Remove a stored account')
|
|
198
|
+
.argument('<account-id>', 'Account ID (App ID)')
|
|
199
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
200
|
+
.action(async (accountId: string, opts: { pretty?: boolean }) => {
|
|
201
|
+
cliOutput(await removeAction(accountId, opts), opts.pretty)
|
|
202
|
+
}),
|
|
203
|
+
)
|