xapi-to 0.1.13 → 0.1.15
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/README.md +42 -40
- package/dist/index.js +1335 -0
- package/package.json +14 -9
- package/src/client.ts +0 -265
- package/src/codegen.ts +0 -307
- package/src/commands/action.ts +0 -328
- package/src/commands/balance.ts +0 -35
- package/src/commands/config.ts +0 -58
- package/src/commands/oauth.ts +0 -297
- package/src/commands/register.ts +0 -77
- package/src/commands/topup.ts +0 -33
- package/src/config.ts +0 -69
- package/src/format.ts +0 -61
- package/src/index.ts +0 -216
package/src/commands/oauth.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/register.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* register command: create a new user account
|
|
3
|
-
*
|
|
4
|
-
* POST /auth/register — no auth required
|
|
5
|
-
* Returns apiKey (shown once), claimCode, referralCode, claimUrl, tweetTemplate
|
|
6
|
-
* Automatically saves apiKey to ~/.xapi/config.json
|
|
7
|
-
*
|
|
8
|
-
* Optional referral code (please replace xapito to the actual referral code):
|
|
9
|
-
* xapi register --referral-code xapito
|
|
10
|
-
* xapi register --referralCode xapito # alias
|
|
11
|
-
* xapi register xapito # positional shorthand
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { XAPI_API_HOST, saveConfig, scheme } from '../config.ts';
|
|
15
|
-
import { output, err } from '../format.ts';
|
|
16
|
-
|
|
17
|
-
interface RegisterResponse {
|
|
18
|
-
apiKey: string;
|
|
19
|
-
claimCode: string;
|
|
20
|
-
referralCode?: string;
|
|
21
|
-
claimSessionId: string;
|
|
22
|
-
claimUrl: string;
|
|
23
|
-
tweetTemplate: string;
|
|
24
|
-
user: { id: string; accountType: string };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function registerAccount(referralCode?: string): Promise<RegisterResponse> {
|
|
28
|
-
const controller = new AbortController();
|
|
29
|
-
const timer = setTimeout(() => controller.abort(), 15_000);
|
|
30
|
-
try {
|
|
31
|
-
const res = await fetch(`${scheme(XAPI_API_HOST)}://${XAPI_API_HOST}/api/auth/register`, {
|
|
32
|
-
method: 'POST',
|
|
33
|
-
headers: { 'Content-Type': 'application/json' },
|
|
34
|
-
body: JSON.stringify(referralCode ? { referralCode } : {}),
|
|
35
|
-
signal: controller.signal,
|
|
36
|
-
});
|
|
37
|
-
if (!res.ok) {
|
|
38
|
-
const text = await res.text();
|
|
39
|
-
throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
|
|
40
|
-
}
|
|
41
|
-
return res.json() as Promise<RegisterResponse>;
|
|
42
|
-
} finally {
|
|
43
|
-
clearTimeout(timer);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function register(args: string[], flags: Record<string, string>) {
|
|
48
|
-
try {
|
|
49
|
-
// 邀请码来源优先级:--referral-code > --referralCode > 第一个位置参数
|
|
50
|
-
const rawReferral =
|
|
51
|
-
flags['referral-code'] ?? flags['referralCode'] ?? args[0];
|
|
52
|
-
const referralCode =
|
|
53
|
-
typeof rawReferral === 'string' && rawReferral !== 'true' && rawReferral.length > 0
|
|
54
|
-
? rawReferral
|
|
55
|
-
: undefined;
|
|
56
|
-
|
|
57
|
-
const res = await registerAccount(referralCode);
|
|
58
|
-
|
|
59
|
-
saveConfig({ apiKey: res.apiKey });
|
|
60
|
-
|
|
61
|
-
output({
|
|
62
|
-
apiKey: res.apiKey,
|
|
63
|
-
user: res.user,
|
|
64
|
-
referralCode: res.referralCode,
|
|
65
|
-
claim: {
|
|
66
|
-
code: res.claimCode,
|
|
67
|
-
sessionId: res.claimSessionId,
|
|
68
|
-
url: res.claimUrl,
|
|
69
|
-
},
|
|
70
|
-
tweetTemplate: res.tweetTemplate,
|
|
71
|
-
...(referralCode ? { referredBy: referralCode } : {}),
|
|
72
|
-
note: 'apiKey saved to ~/.xapi/config.json',
|
|
73
|
-
}, flags.format as any);
|
|
74
|
-
} catch (e: any) {
|
|
75
|
-
err('register failed', e.message);
|
|
76
|
-
}
|
|
77
|
-
}
|
package/src/commands/topup.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* topup command
|
|
3
|
-
*
|
|
4
|
-
* Generates a payment URL pointing to the xapi frontend topup page.
|
|
5
|
-
* All params are optional.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* xapi topup [--amount <usd>] [--method stripe|x402]
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { getConfig } from '../config.ts';
|
|
12
|
-
import { output } from '../format.ts';
|
|
13
|
-
|
|
14
|
-
const TOPUP_BASE_URL = 'https://www.xapi.to/topup/payment';
|
|
15
|
-
|
|
16
|
-
export async function topup(args: string[], flags: Record<string, string>) {
|
|
17
|
-
const cfg = getConfig();
|
|
18
|
-
|
|
19
|
-
const url = new URL(TOPUP_BASE_URL);
|
|
20
|
-
|
|
21
|
-
if (cfg.apiKey) url.searchParams.set('apikey', cfg.apiKey);
|
|
22
|
-
if (flags.method) url.searchParams.set('method', flags.method);
|
|
23
|
-
|
|
24
|
-
const amountStr = flags.amount || args[0];
|
|
25
|
-
if (amountStr) {
|
|
26
|
-
const amountUsd = parseFloat(amountStr);
|
|
27
|
-
if (!isNaN(amountUsd) && amountUsd > 0) {
|
|
28
|
-
url.searchParams.set('amount', String(amountUsd));
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
output({ url: url.toString() }, flags.format as any);
|
|
33
|
-
}
|
package/src/config.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Config management
|
|
3
|
-
* Only apiKey is user-configurable. Host is built-in.
|
|
4
|
-
* Reads from env var XAPI_KEY or ~/.xapi/config.json
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
|
|
8
|
-
import { err } from './format.ts';
|
|
9
|
-
import { homedir } from 'os';
|
|
10
|
-
import { join } from 'path';
|
|
11
|
-
|
|
12
|
-
export const XAPI_ACTION_HOST = process.env.XAPI_ACTION_HOST || 'action.xapi.to'; // action service (capabilities + APIs)
|
|
13
|
-
export const XAPI_API_HOST = process.env.XAPI_API_HOST || 'api.xapi.to'; // auth + agent API
|
|
14
|
-
|
|
15
|
-
/** Returns https:// for remote hosts, http:// for localhost */
|
|
16
|
-
export function scheme(host: string): string {
|
|
17
|
-
return host.startsWith('localhost') || host.startsWith('127.') ? 'http' : 'https';
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface XapiConfig {
|
|
21
|
-
actionHost: string;
|
|
22
|
-
apiKey?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const CONFIG_DIR = join(homedir(), '.xapi');
|
|
26
|
-
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
27
|
-
|
|
28
|
-
function loadFileConfig(): { apiKey?: string } {
|
|
29
|
-
if (!existsSync(CONFIG_FILE)) return {};
|
|
30
|
-
try {
|
|
31
|
-
return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
|
|
32
|
-
} catch {
|
|
33
|
-
return {};
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function getConfig(): XapiConfig {
|
|
38
|
-
const file = loadFileConfig();
|
|
39
|
-
return {
|
|
40
|
-
actionHost: XAPI_ACTION_HOST,
|
|
41
|
-
apiKey: process.env.XAPI_KEY || process.env.XAPI_API_KEY || file.apiKey,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function requireApiKey(cfg: XapiConfig): void {
|
|
46
|
-
if (!cfg.apiKey) {
|
|
47
|
-
err('API key not configured', 'Run "npx xapi-to register" to create an account, or "npx xapi-to config set apiKey=<key>" to set an existing key.');
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function saveConfig(updates: { apiKey?: string }): void {
|
|
52
|
-
const current = loadFileConfig();
|
|
53
|
-
const merged = { ...current, ...updates };
|
|
54
|
-
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
55
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), { mode: 0o600 });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function showConfig(): void {
|
|
59
|
-
const cfg = getConfig();
|
|
60
|
-
const file = loadFileConfig();
|
|
61
|
-
console.log(JSON.stringify({
|
|
62
|
-
actionHost: cfg.actionHost,
|
|
63
|
-
apiKey: cfg.apiKey ? `${cfg.apiKey.slice(0, 8)}...` : undefined,
|
|
64
|
-
source: {
|
|
65
|
-
apiKey: (process.env.XAPI_KEY || process.env.XAPI_API_KEY) ? 'env' : file.apiKey ? 'file' : 'none',
|
|
66
|
-
},
|
|
67
|
-
configFile: CONFIG_FILE,
|
|
68
|
-
}, null, 2));
|
|
69
|
-
}
|
package/src/format.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Output formatting
|
|
3
|
-
* Supports: json (default, machine-readable), pretty (human-readable), table
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export type OutputFormat = 'json' | 'pretty' | 'table';
|
|
7
|
-
|
|
8
|
-
export function getFormat(): OutputFormat {
|
|
9
|
-
const f = process.env.XAPI_OUTPUT || 'json';
|
|
10
|
-
if (f === 'pretty' || f === 'table') return f;
|
|
11
|
-
return 'json';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function output(data: unknown, format?: OutputFormat): void {
|
|
15
|
-
const fmt = format || getFormat();
|
|
16
|
-
if (fmt === 'json') {
|
|
17
|
-
console.log(JSON.stringify(data));
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
if (fmt === 'pretty') {
|
|
21
|
-
console.log(JSON.stringify(data, null, 2));
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
// table: try to render arrays of objects as a table
|
|
25
|
-
if (fmt === 'table' && Array.isArray(data)) {
|
|
26
|
-
printTable(data as Record<string, unknown>[]);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
console.log(JSON.stringify(data, null, 2));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function printTable(rows: Record<string, unknown>[]): void {
|
|
33
|
-
if (rows.length === 0) {
|
|
34
|
-
console.log('(empty)');
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const keys = Object.keys(rows[0]);
|
|
38
|
-
const widths = keys.map(k =>
|
|
39
|
-
Math.min(40, Math.max(k.length, ...rows.map(r => String(r[k] ?? '').length)))
|
|
40
|
-
);
|
|
41
|
-
const sep = widths.map(w => '-'.repeat(w)).join(' ');
|
|
42
|
-
const header = keys.map((k, i) => k.padEnd(widths[i])).join(' ');
|
|
43
|
-
console.log(header);
|
|
44
|
-
console.log(sep);
|
|
45
|
-
for (const row of rows) {
|
|
46
|
-
const line = keys.map((k, i) => String(row[k] ?? '').slice(0, widths[i]).padEnd(widths[i])).join(' ');
|
|
47
|
-
console.log(line);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function err(msg: string, detail?: unknown): never {
|
|
52
|
-
if (process.stderr.isTTY) {
|
|
53
|
-
console.error(`Error: ${msg}`);
|
|
54
|
-
if (detail !== undefined) console.error(` ${detail}`);
|
|
55
|
-
} else {
|
|
56
|
-
const out: Record<string, unknown> = { error: msg };
|
|
57
|
-
if (detail !== undefined) out.detail = detail;
|
|
58
|
-
console.error(JSON.stringify(out));
|
|
59
|
-
}
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|