xapi-to 0.1.12 → 0.1.14

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.
@@ -1,328 +0,0 @@
1
- /**
2
- * Top-level action commands: list, search, categories, services, get, call
3
- * Unified interface for all actions (capabilities + APIs).
4
- * Use --source capability|api to filter by source type.
5
- */
6
-
7
- import { getConfig, requireApiKey } from '../config.ts';
8
- import * as client from '../client.ts';
9
- import { output, err, getFormat } from '../format.ts';
10
- import { generateCode, buildDefaultInput, resolveTarget } from '../codegen.ts';
11
-
12
- const VALID_SOURCES = ['capability', 'api'];
13
-
14
- // ── Subcommand help texts ────────────────────────────────────────────────────
15
-
16
- const LIST_HELP = `xapi list - List all actions
17
-
18
- USAGE
19
- xapi list [flags]
20
-
21
- FLAGS
22
- --source capability|api Filter by source type
23
- --category <name> Filter by category
24
- --service-id <id> Filter by service
25
- --page N Page number (default: 1)
26
- --page-size N Results per page
27
- --format json|pretty|table Output format
28
-
29
- EXAMPLES
30
- xapi list
31
- xapi list --source api --format table
32
- xapi list --category social --page 2
33
- `;
34
-
35
- const SEARCH_HELP = `xapi search - Search actions by keyword
36
-
37
- USAGE
38
- xapi search <query> [flags]
39
-
40
- FLAGS
41
- --source capability|api Filter by source type
42
- --category <name> Filter by category
43
- --page N Page number (default: 1)
44
- --page-size N Results per page
45
- --format json|pretty|table Output format
46
-
47
- EXAMPLES
48
- xapi search twitter
49
- xapi search "tweet detail" --source api
50
- xapi search weather --category utility --format table
51
- `;
52
-
53
- const GET_HELP = `xapi get - Get action schema
54
-
55
- USAGE
56
- xapi get <id> [flags]
57
-
58
- FLAGS
59
- --method GET|POST|... Filter by HTTP method
60
- --code <target> Generate code snippet instead of showing schema
61
- --format json|pretty|table Output format
62
-
63
- CODE TARGETS
64
- curl cURL command
65
- py, python Python (requests)
66
- python.requests Python with requests
67
- py.requests alias for python.requests
68
- python.httpx Python with httpx
69
- py.httpx alias for python.httpx
70
- js, javascript JavaScript (fetch)
71
- javascript.fetch JavaScript with fetch
72
- js.fetch alias for javascript.fetch
73
- javascript.axios JavaScript with axios
74
- js.axios alias for javascript.axios
75
- ts, typescript TypeScript (fetch)
76
- typescript.fetch TypeScript with fetch
77
- ts.fetch alias for typescript.fetch
78
- go Go (net/http)
79
-
80
- EXAMPLES
81
- xapi get twitter.tweet_detail
82
- xapi get twitter.tweet_detail --method POST
83
- xapi get twitter.tweet_detail --code curl
84
- xapi get twitter.tweet_detail --code python.httpx --format pretty
85
- `;
86
-
87
- const CALL_HELP = `xapi call - Execute an action
88
-
89
- USAGE
90
- xapi call <id> --input '{"key":"val"}' [flags]
91
-
92
- FLAGS
93
- --input <json> Input payload as JSON (required for execution)
94
- --method GET|POST|... Override HTTP method
95
- --code <target> Generate code snippet instead of executing
96
- --format json|pretty|table Output format
97
-
98
- CODE TARGETS
99
- curl cURL command
100
- py, python Python (requests)
101
- python.requests Python with requests
102
- py.requests alias for python.requests
103
- python.httpx Python with httpx
104
- py.httpx alias for python.httpx
105
- js, javascript JavaScript (fetch)
106
- javascript.fetch JavaScript with fetch
107
- js.fetch alias for javascript.fetch
108
- javascript.axios JavaScript with axios
109
- js.axios alias for javascript.axios
110
- ts, typescript TypeScript (fetch)
111
- typescript.fetch TypeScript with fetch
112
- ts.fetch alias for typescript.fetch
113
- go Go (net/http)
114
-
115
- EXAMPLES
116
- xapi call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
117
- xapi call twitter.tweet_detail --input '{"tweet_id":"123"}' --code py
118
- xapi call twitter.tweet_detail --input '{"tweet_id":"123"}' --code curl --format pretty
119
- `;
120
-
121
- /** Print subcommand help and exit if --help flag is set */
122
- function showHelpIfRequested(flags: Record<string, string>, helpText: string): void {
123
- if (flags.help) {
124
- console.log(helpText);
125
- process.exit(0);
126
- }
127
- }
128
-
129
- /** Validate --code flag: check for bare flag and unknown target (fail fast before I/O) */
130
- function validateCodeFlag(flags: Record<string, string>): void {
131
- if (flags.code === 'true') {
132
- err('--code requires a target language, e.g. --code curl, --code py, --code js');
133
- }
134
- resolveTarget(flags.code);
135
- }
136
-
137
- /** Output code snippet respecting --format */
138
- function outputCode(result: { lang: string; lib: string; code: string }, flags: Record<string, string>) {
139
- const fmt = flags.format || getFormat();
140
- if (fmt === 'json') {
141
- output({ language: result.lang, library: result.lib, code: result.code }, 'json');
142
- } else {
143
- console.log(result.code);
144
- }
145
- }
146
-
147
- /** Validate and return source filter from --source flag */
148
- function getSource(flags: Record<string, string>): string | undefined {
149
- if (!flags.source) return undefined;
150
- if (!VALID_SOURCES.includes(flags.source)) {
151
- err(`invalid --source value: "${flags.source}". Must be "capability" or "api".`);
152
- }
153
- return flags.source;
154
- }
155
-
156
- export async function actionList(args: string[], flags: Record<string, string>) {
157
- showHelpIfRequested(flags, LIST_HELP);
158
- const cfg = getConfig();
159
- try {
160
- const res = await client.actionList(cfg, {
161
- source: getSource(flags),
162
- page: flags.page ? parseInt(flags.page) : undefined,
163
- page_size: flags['page-size'] ? parseInt(flags['page-size']) : undefined,
164
- category: flags.category,
165
- service_id: flags['service-id'],
166
- });
167
- const actions = (res.actions || []) as any[];
168
- if (flags.format === 'table') {
169
- output(actions.map((a: any) => ({
170
- id: a.id,
171
- method: a.method ?? '',
172
- displayName: a.displayName ?? '',
173
- source: a.source ?? '',
174
- category: a.meta?.category ?? '',
175
- status: a.status ?? '',
176
- cost: a.meta?.cost ?? '',
177
- })), 'table');
178
- } else {
179
- output(res, flags.format as any);
180
- }
181
- } catch (e: any) {
182
- err('list failed', e.message);
183
- }
184
- }
185
-
186
- export async function actionSearch(args: string[], flags: Record<string, string>) {
187
- showHelpIfRequested(flags, SEARCH_HELP);
188
- const query = args[0];
189
- if (!query) err('usage: xapi search <query>');
190
- const cfg = getConfig();
191
- try {
192
- const res = await client.actionSearch(query, cfg, {
193
- source: getSource(flags),
194
- category: flags.category,
195
- page: flags.page ? parseInt(flags.page) : undefined,
196
- page_size: flags['page-size'] ? parseInt(flags['page-size']) : undefined,
197
- });
198
- const results = (res.results || []) as any[];
199
- if (flags.format === 'table') {
200
- output(results.map((a: any) => ({
201
- id: a.id,
202
- method: a.method ?? '',
203
- displayName: a.displayName ?? '',
204
- source: a.source ?? '',
205
- category: a.meta?.category ?? '',
206
- status: a.status ?? '',
207
- cost: a.meta?.cost ?? '',
208
- })), 'table');
209
- } else {
210
- output(res, flags.format as any);
211
- }
212
- } catch (e: any) {
213
- err('search failed', e.message);
214
- }
215
- }
216
-
217
- export async function actionCategories(args: string[], flags: Record<string, string>) {
218
- const cfg = getConfig();
219
- try {
220
- const res = await client.actionCategories(cfg, { source: getSource(flags) });
221
- if (flags.format === 'table') {
222
- output(res.categories.map(c => ({ category: c })), 'table');
223
- } else {
224
- output(res, flags.format as any);
225
- }
226
- } catch (e: any) {
227
- err('categories failed', e.message);
228
- }
229
- }
230
-
231
- export async function actionServices(args: string[], flags: Record<string, string>) {
232
- const cfg = getConfig();
233
- try {
234
- const res = await client.actionServices(cfg, {
235
- page: flags.page ? parseInt(flags.page) : undefined,
236
- page_size: flags['page-size'] ? parseInt(flags['page-size']) : undefined,
237
- category: flags.category,
238
- });
239
- const services = (res.services || []) as any[];
240
- if (flags.format === 'table') {
241
- output(services.map((s: any) => ({
242
- id: s.id,
243
- name: s.name ?? '',
244
- category: s.category ?? '',
245
- source: s.source ?? '',
246
- endpoints: s.endpointCount ?? '',
247
- status: s.status ?? '',
248
- })), 'table');
249
- } else {
250
- output(res, flags.format as any);
251
- }
252
- } catch (e: any) {
253
- err('services failed', e.message);
254
- }
255
- }
256
-
257
- export async function actionGet(args: string[], flags: Record<string, string>) {
258
- showHelpIfRequested(flags, GET_HELP);
259
- const id = args[0];
260
- if (!id) err('usage: xapi get <id> [--method GET|POST|DELETE|...]');
261
- if (flags.code) validateCodeFlag(flags);
262
- const cfg = getConfig();
263
- try {
264
- const res = await client.actionGet(id, cfg);
265
- const actions = Array.isArray(res) ? res : [res];
266
- const methodFilter = flags.method?.toUpperCase();
267
- const filtered = methodFilter
268
- ? actions.filter((a: any) => a.method?.toUpperCase() === methodFilter)
269
- : actions;
270
- if (filtered.length === 0) {
271
- err(`no endpoint found for method "${methodFilter}" in action "${id}"`);
272
- }
273
-
274
- if (flags.code) {
275
- if (filtered.length > 1) {
276
- process.stderr.write(
277
- `Warning: action "${id}" has ${filtered.length} endpoints; using method "${(filtered[0] as any).method}". Use --method to select a specific one.\n`,
278
- );
279
- }
280
- const action = filtered[0] as any;
281
- const { method: _schemaMethod, ...cleanCodeInput } = buildDefaultInput(action.input ?? {});
282
- const result = generateCode(flags.code, { actionId: id, input: cleanCodeInput, actionHost: cfg.actionHost, method: action.method });
283
- outputCode(result, flags);
284
- return;
285
- }
286
-
287
- output(filtered.length === 1 ? filtered[0] : filtered, flags.format as any);
288
- } catch (e: any) {
289
- err('get failed', e.message);
290
- }
291
- }
292
-
293
- export async function actionCall(args: string[], flags: Record<string, string>) {
294
- showHelpIfRequested(flags, CALL_HELP);
295
- const id = args[0];
296
- if (!id) err('usage: xapi call <id> --input \'{"key":"val"}\'');
297
- if (flags.code) validateCodeFlag(flags);
298
- const cfg = getConfig();
299
- let input: Record<string, unknown> = {};
300
- if (flags.input) {
301
- try {
302
- input = JSON.parse(flags.input);
303
- } catch {
304
- err('--input must be valid JSON');
305
- }
306
- if (typeof input !== 'object' || input === null || Array.isArray(input)) {
307
- err('--input must be a JSON object');
308
- }
309
- }
310
- // method 作为独立参数传递,兼容 input 内的 method
311
- const { method: inputMethod, ...cleanInput } = input;
312
- const method = flags.method?.toUpperCase()
313
- || (typeof inputMethod === 'string' ? inputMethod.toUpperCase() : undefined);
314
-
315
- if (flags.code) {
316
- const result = generateCode(flags.code, { actionId: id, input: cleanInput, actionHost: cfg.actionHost, method });
317
- outputCode(result, flags);
318
- return;
319
- }
320
-
321
- requireApiKey(cfg);
322
- try {
323
- const res = await client.actionCall(id, cleanInput, cfg, method);
324
- output(res, flags.format as any);
325
- } catch (e: any) {
326
- err('call failed', e.message);
327
- }
328
- }
@@ -1,35 +0,0 @@
1
- /**
2
- * balance command
3
- * Fetches balance from GET /auth/me
4
- */
5
-
6
- import { getConfig, requireApiKey, XAPI_API_HOST, scheme } from '../config.ts';
7
- import { loginWithApiKey, request } from '../client.ts';
8
- import { output, err } from '../format.ts';
9
-
10
- export async function balance(args: string[], flags: Record<string, string>) {
11
- const cfg = getConfig();
12
- requireApiKey(cfg);
13
-
14
- let token: string;
15
- try {
16
- const res = await loginWithApiKey(cfg.apiKey!, XAPI_API_HOST);
17
- token = res.accessToken;
18
- } catch (e: any) {
19
- err('login failed', e.message);
20
- }
21
-
22
- try {
23
- const me = await request<{ balance: string; accountType: string; tier: string }>(
24
- `${scheme(XAPI_API_HOST)}://${XAPI_API_HOST}/api/auth/me`,
25
- { method: 'GET', headers: { Authorization: `Bearer ${token!}` } },
26
- );
27
- output({
28
- balance: me.balance,
29
- accountType: me.accountType,
30
- tier: me.tier,
31
- }, flags.format as any);
32
- } catch (e: any) {
33
- err('balance fetch failed', e.message);
34
- }
35
- }
@@ -1,39 +0,0 @@
1
- /**
2
- * config commands: show, set, health
3
- */
4
-
5
- import { getConfig, saveConfig, showConfig } from '../config.ts';
6
- import { healthCheck } from '../client.ts';
7
- import { output, err } from '../format.ts';
8
-
9
- export async function configShow(args: string[], flags: Record<string, string>) {
10
- showConfig();
11
- }
12
-
13
- export async function configSet(args: string[], flags: Record<string, string>) {
14
- // xapi config set apiKey=xapi_xxx
15
- if (args.length === 0) err('usage: xapi config set apiKey=<key>');
16
- const updates: { apiKey?: string } = {};
17
- for (const arg of args) {
18
- const eq = arg.indexOf('=');
19
- if (eq < 1) err(`invalid key=value: ${arg}`);
20
- const key = arg.slice(0, eq);
21
- if (key === 'host') err('host is built-in and cannot be configured');
22
- if (key !== 'apiKey') err(`unknown config key: ${key} (only apiKey is configurable)`);
23
- updates.apiKey = arg.slice(eq + 1);
24
- }
25
- saveConfig(updates);
26
- console.log(JSON.stringify({ ok: true, updated: Object.keys(updates) }));
27
- }
28
-
29
- export async function configHealth(args: string[], flags: Record<string, string>) {
30
- const cfg = getConfig();
31
- const start = Date.now();
32
- try {
33
- await healthCheck(cfg);
34
- output({ status: 'ok', host: cfg.actionHost, latency_ms: Date.now() - start }, flags.format as any);
35
- } catch (e: any) {
36
- output({ status: 'error', host: cfg.actionHost, error: e.message }, flags.format as any);
37
- process.exit(1);
38
- }
39
- }
@@ -1,297 +0,0 @@
1
- /**
2
- * oauth commands: bind, status, unbind
3
- *
4
- * Flow for `xapi oauth bind [--provider twitter]`:
5
- * 1. Login with current API key → get JWT
6
- * 2. List API keys → find the one matching the current key by prefix
7
- * 3. Enable OAuth on the key if not already (POST /keys/:id/enable-oauth)
8
- * 4. List OAuth providers → find the requested provider
9
- * 5. POST /oauth/authorize → get authorizationUrl
10
- * 6. Open browser (macOS/Linux/Windows) and poll for binding completion
11
- *
12
- * `xapi oauth status`: list current OAuth bindings for the API key
13
- * `xapi oauth unbind <binding-id>`: delete an OAuth binding
14
- */
15
-
16
- import { spawnSync } from 'child_process';
17
- import { XAPI_API_HOST, getConfig, requireApiKey } from '../config.ts';
18
- import {
19
- loginWithApiKey,
20
- listKeys,
21
- enableOAuthForKey,
22
- listOAuthProviders,
23
- initiateOAuth,
24
- listOAuthBindings,
25
- deleteOAuthBinding,
26
- } from '../client.ts';
27
- import { output, err } from '../format.ts';
28
-
29
- /** Try to open a URL in the default browser. Silent on failure. */
30
- function openBrowser(url: string): void {
31
- const cmd = process.platform === 'win32' ? 'start'
32
- : process.platform === 'darwin' ? 'open'
33
- : 'xdg-open';
34
- try {
35
- spawnSync(cmd, [url], { stdio: 'ignore' });
36
- } catch {
37
- // ignore — user can open manually
38
- }
39
- }
40
-
41
- /**
42
- * Poll bindings until one for (apiKeyId, providerId) appears.
43
- * Shows a live countdown in TTY mode.
44
- * Returns the matched binding or null on timeout.
45
- */
46
- async function pollForBinding(
47
- apiKeyId: string,
48
- providerId: string,
49
- jwtToken: string,
50
- timeoutMs = 5 * 60 * 1000,
51
- intervalMs = 3000,
52
- ): Promise<{ providerAccountName: string | null } | null> {
53
- const deadline = Date.now() + timeoutMs;
54
- const isTTY = process.stdout.isTTY;
55
-
56
- while (Date.now() < deadline) {
57
- await new Promise((r) => setTimeout(r, intervalMs));
58
-
59
- try {
60
- const bindings = await listOAuthBindings(jwtToken, XAPI_API_HOST);
61
- const match = Array.isArray(bindings)
62
- ? bindings.find((b) => b.apiKeyId === apiKeyId && b.providerId === providerId)
63
- : null;
64
- if (match) return match;
65
- } catch {
66
- // transient error — keep polling
67
- }
68
-
69
- if (isTTY) {
70
- const remaining = Math.ceil((deadline - Date.now()) / 1000);
71
- process.stdout.write(`\r Waiting for authorization... (${remaining}s remaining) `);
72
- }
73
- }
74
-
75
- if (process.stdout.isTTY) process.stdout.write('\n');
76
- return null;
77
- }
78
-
79
- // ── Helpers ────────────────────────────────────────────────────────────────────
80
-
81
- async function loginAndGetJwt(apiKey: string): Promise<string> {
82
- const result = await loginWithApiKey(apiKey, XAPI_API_HOST) as any;
83
- if (!result?.accessToken) {
84
- throw new Error('Login failed: no access token returned');
85
- }
86
- return result.accessToken;
87
- }
88
-
89
- /**
90
- * Find the API key record that corresponds to the current plaintext API key.
91
- * Matches by key prefix (first 7 chars of the plaintext key = keyPrefix).
92
- */
93
- async function findCurrentKeyRecord(
94
- plaintextKey: string,
95
- jwtToken: string,
96
- ): Promise<{ id: string; name: string; keyPreview: string; oauthEnabled: boolean }> {
97
- const keys = await listKeys(jwtToken, XAPI_API_HOST);
98
- if (!Array.isArray(keys) || keys.length === 0) {
99
- throw new Error('No API keys found for this account');
100
- }
101
- if (keys.length === 1) return keys[0];
102
- // Match by prefix: keyPreview starts with the key's prefix
103
- const prefix = plaintextKey.substring(0, 7);
104
- const match = keys.find((k) => k.keyPreview.startsWith(prefix));
105
- if (!match) {
106
- // Fallback: use the first key
107
- return keys[0];
108
- }
109
- return match;
110
- }
111
-
112
- // ── Help text ──────────────────────────────────────────────────────────────────
113
-
114
- export const OAUTH_HELP = `xapi oauth - Manage OAuth bindings
115
-
116
- USAGE
117
- xapi oauth <command> [flags]
118
-
119
- COMMANDS
120
- bind [--provider <name>] Bind an OAuth account to your API key
121
- status List current OAuth bindings
122
- unbind <binding-id> Remove an OAuth binding
123
- providers List available OAuth providers
124
-
125
- FLAGS
126
- --provider <name> OAuth provider (default: twitter)
127
- --format json|pretty|table Output format
128
-
129
- EXAMPLES
130
- xapi oauth bind
131
- xapi oauth bind --provider twitter
132
- xapi oauth status
133
- xapi oauth status --format pretty
134
- xapi oauth unbind abc123
135
- xapi oauth providers
136
- `;
137
-
138
- // ── Commands ───────────────────────────────────────────────────────────────────
139
-
140
- /**
141
- * xapi oauth bind [--provider twitter]
142
- *
143
- * Initiates OAuth binding for the current API key.
144
- * Prints the authorization URL for the user to open in a browser.
145
- */
146
- export async function oauthBind(args: string[], flags: Record<string, string>) {
147
- const cfg = getConfig();
148
- requireApiKey(cfg);
149
- const apiKey = cfg.apiKey!;
150
- const providerName = (flags.provider || 'twitter').toLowerCase();
151
-
152
- try {
153
- // 1. Login to get JWT
154
- const jwtToken = await loginAndGetJwt(apiKey);
155
-
156
- // 2. Find the API key record
157
- const keyRecord = await findCurrentKeyRecord(apiKey, jwtToken);
158
-
159
- // 3. Enable OAuth on the key if needed
160
- if (!keyRecord.oauthEnabled) {
161
- await enableOAuthForKey(keyRecord.id, apiKey, jwtToken, XAPI_API_HOST);
162
- }
163
-
164
- // 4. Find the requested OAuth provider
165
- const providers = await listOAuthProviders(XAPI_API_HOST);
166
- if (!Array.isArray(providers) || providers.length === 0) {
167
- throw new Error('No OAuth providers available');
168
- }
169
- const provider = providers.find(
170
- (p) =>
171
- p.type.toLowerCase() === providerName ||
172
- p.name.toLowerCase().includes(providerName),
173
- );
174
- if (!provider) {
175
- const available = providers.map((p) => p.type).join(', ');
176
- throw new Error(
177
- `Provider "${providerName}" not found. Available: ${available}`,
178
- );
179
- }
180
-
181
- // 5. Initiate OAuth authorization
182
- const result = await initiateOAuth(keyRecord.id, provider.id, jwtToken, XAPI_API_HOST);
183
- const { authorizationUrl } = result;
184
-
185
- const isTTY = process.stdout.isTTY;
186
-
187
- if (isTTY) {
188
- // Interactive mode: open browser + poll
189
- console.error(`\n Provider : ${provider.name}`);
190
- console.error(` API Key : ${keyRecord.keyPreview}`);
191
- console.error(`\n Authorization URL:\n ${authorizationUrl}\n`);
192
- console.error(' Opening browser...');
193
- openBrowser(authorizationUrl);
194
- console.error(' Waiting for you to complete authorization in the browser...\n');
195
-
196
- const binding = await pollForBinding(keyRecord.id, provider.id, jwtToken);
197
-
198
- if (process.stdout.isTTY) process.stdout.write('\n');
199
-
200
- if (binding) {
201
- const account = (binding as any).providerAccountName || 'unknown';
202
- console.error(`\n Authorization complete! Bound to @${account}\n`);
203
- output({ status: 'success', provider: provider.name, account }, flags.format as any);
204
- } else {
205
- err('oauth bind timed out', 'Authorization was not completed within 5 minutes. Run "xapi oauth bind" again.');
206
- }
207
- } else {
208
- // Non-interactive / agent mode: just output the URL
209
- output({
210
- status: 'pending',
211
- provider: provider.name,
212
- apiKey: keyRecord.keyPreview,
213
- authorizationUrl,
214
- }, flags.format as any);
215
- }
216
- } catch (e: any) {
217
- err('oauth bind failed', e.message);
218
- }
219
- }
220
-
221
- /**
222
- * xapi oauth status
223
- *
224
- * Lists all OAuth bindings for the current account.
225
- */
226
- export async function oauthStatus(args: string[], flags: Record<string, string>) {
227
- const cfg = getConfig();
228
- requireApiKey(cfg);
229
- const apiKey = cfg.apiKey!;
230
-
231
- try {
232
- const jwtToken = await loginAndGetJwt(apiKey);
233
- const bindings = await listOAuthBindings(jwtToken, XAPI_API_HOST);
234
-
235
- if (!Array.isArray(bindings) || bindings.length === 0) {
236
- output({
237
- status: 'no_bindings',
238
- message: 'No OAuth bindings found. Run "xapi oauth bind" to connect an account.',
239
- }, flags.format as any);
240
- return;
241
- }
242
-
243
- output({
244
- status: 'ok',
245
- count: bindings.length,
246
- bindings: bindings.map((b) => ({
247
- id: b.id,
248
- provider: b.provider.name,
249
- providerType: b.provider.type,
250
- account: b.providerAccountName || b.providerAccountId,
251
- apiKeyId: b.apiKeyId,
252
- scopes: b.scopes,
253
- boundAt: b.createdAt,
254
- })),
255
- }, flags.format as any);
256
- } catch (e: any) {
257
- err('oauth status failed', e.message);
258
- }
259
- }
260
-
261
- /**
262
- * xapi oauth unbind <binding-id>
263
- *
264
- * Deletes an OAuth binding. Get the ID from `xapi oauth status`.
265
- */
266
- export async function oauthUnbind(args: string[], flags: Record<string, string>) {
267
- const cfg = getConfig();
268
- requireApiKey(cfg);
269
- const apiKey = cfg.apiKey!;
270
-
271
- const bindingId = args[0];
272
- if (!bindingId) {
273
- err('usage: xapi oauth unbind <binding-id>', 'Get the binding ID from "xapi oauth status"');
274
- }
275
-
276
- try {
277
- const jwtToken = await loginAndGetJwt(apiKey);
278
- const result = await deleteOAuthBinding(bindingId, jwtToken, XAPI_API_HOST);
279
- output({ success: result.success, message: 'OAuth binding removed' }, flags.format as any);
280
- } catch (e: any) {
281
- err('oauth unbind failed', e.message);
282
- }
283
- }
284
-
285
- /**
286
- * xapi oauth providers
287
- *
288
- * Lists available OAuth providers.
289
- */
290
- export async function oauthProviders(args: string[], flags: Record<string, string>) {
291
- try {
292
- const providers = await listOAuthProviders(XAPI_API_HOST);
293
- output(providers, flags.format as any);
294
- } catch (e: any) {
295
- err('oauth providers failed', e.message);
296
- }
297
- }