@vex-chat/cli 0.2.0 → 0.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/package.json +2 -2
- package/src/vex-chat.js +109 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vex-chat/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Terminal client for vex-chat.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"better-sqlite3": "11.10.0",
|
|
33
33
|
"msgpackr": "^1.11.9",
|
|
34
|
-
"@vex-chat/libvex": "^7.
|
|
34
|
+
"@vex-chat/libvex": "^7.4.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^25.6.0",
|
package/src/vex-chat.js
CHANGED
|
@@ -145,6 +145,9 @@ async function main() {
|
|
|
145
145
|
case "invite":
|
|
146
146
|
await inviteCommand(ctx, positionals);
|
|
147
147
|
break;
|
|
148
|
+
case "entitlements":
|
|
149
|
+
await entitlementsCommand(ctx, positionals);
|
|
150
|
+
break;
|
|
148
151
|
case "group":
|
|
149
152
|
await groupCommand(ctx, positionals);
|
|
150
153
|
break;
|
|
@@ -176,6 +179,7 @@ function isCommand(command) {
|
|
|
176
179
|
"channels",
|
|
177
180
|
"chat",
|
|
178
181
|
"dm",
|
|
182
|
+
"entitlements",
|
|
179
183
|
"group",
|
|
180
184
|
"help",
|
|
181
185
|
"invite",
|
|
@@ -285,6 +289,7 @@ async function createContext(flags) {
|
|
|
285
289
|
return {
|
|
286
290
|
dataDir,
|
|
287
291
|
configPath,
|
|
292
|
+
flags,
|
|
288
293
|
clientOptions: {
|
|
289
294
|
dbFolder: path.join(dataDir, "db"),
|
|
290
295
|
deviceName: "vex-chat-cli",
|
|
@@ -377,6 +382,11 @@ function httpFromApiUrl(raw) {
|
|
|
377
382
|
}
|
|
378
383
|
}
|
|
379
384
|
|
|
385
|
+
function apiBaseUrl(ctx) {
|
|
386
|
+
const scheme = ctx.clientOptions.unsafeHttp ? "http" : "https";
|
|
387
|
+
return `${scheme}://${ctx.clientOptions.host}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
380
390
|
function normalizeAccountHost(host) {
|
|
381
391
|
return String(host ?? DEFAULT_HOST)
|
|
382
392
|
.trim()
|
|
@@ -597,16 +607,68 @@ function getClientBearerToken(client) {
|
|
|
597
607
|
return match?.[1] ?? null;
|
|
598
608
|
}
|
|
599
609
|
|
|
610
|
+
function responseBodyMessage(data) {
|
|
611
|
+
if (!data || typeof data !== "object" || ArrayBuffer.isView(data)) {
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
if (data instanceof ArrayBuffer) {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
const error = data.error;
|
|
618
|
+
if (typeof error === "string") return error;
|
|
619
|
+
if (error && typeof error === "object") {
|
|
620
|
+
const nestedMessage = error.message;
|
|
621
|
+
if (typeof nestedMessage === "string") return nestedMessage;
|
|
622
|
+
}
|
|
623
|
+
const message = data.message;
|
|
624
|
+
return typeof message === "string" ? message : null;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function bytesFromResponseBody(data) {
|
|
628
|
+
if (ArrayBuffer.isView(data)) {
|
|
629
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
630
|
+
}
|
|
631
|
+
if (data instanceof ArrayBuffer) {
|
|
632
|
+
return new Uint8Array(data);
|
|
633
|
+
}
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function decodeBinaryResponseBody(data) {
|
|
638
|
+
const bytes = bytesFromResponseBody(data);
|
|
639
|
+
if (!bytes) return null;
|
|
640
|
+
try {
|
|
641
|
+
return unpack(bytes);
|
|
642
|
+
} catch {
|
|
643
|
+
// Express JSON error bodies also arrive as ArrayBuffer with fetch.
|
|
644
|
+
}
|
|
645
|
+
const text = new TextDecoder("utf-8", { fatal: false })
|
|
646
|
+
.decode(bytes)
|
|
647
|
+
.trim();
|
|
648
|
+
if (!text) return null;
|
|
649
|
+
if (text.startsWith("{") || text.startsWith("[")) {
|
|
650
|
+
try {
|
|
651
|
+
return JSON.parse(text);
|
|
652
|
+
} catch {
|
|
653
|
+
return text;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return text;
|
|
657
|
+
}
|
|
658
|
+
|
|
600
659
|
function errorResponseMessage(err) {
|
|
601
660
|
const data = err?.response?.data;
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
661
|
+
const binaryBody = decodeBinaryResponseBody(data);
|
|
662
|
+
const binaryMessage = responseBodyMessage(binaryBody);
|
|
663
|
+
if (binaryMessage) return binaryMessage;
|
|
664
|
+
if (typeof binaryBody === "string") return binaryBody;
|
|
665
|
+
const objectMessage = responseBodyMessage(data);
|
|
666
|
+
if (objectMessage) return objectMessage;
|
|
606
667
|
if (typeof data === "string") {
|
|
607
668
|
try {
|
|
608
669
|
const parsed = JSON.parse(data);
|
|
609
|
-
|
|
670
|
+
const jsonMessage = responseBodyMessage(parsed);
|
|
671
|
+
if (jsonMessage) return jsonMessage;
|
|
610
672
|
} catch {
|
|
611
673
|
return data;
|
|
612
674
|
}
|
|
@@ -1443,6 +1505,47 @@ async function inviteCommand(ctx, args) {
|
|
|
1443
1505
|
});
|
|
1444
1506
|
}
|
|
1445
1507
|
|
|
1508
|
+
async function entitlementsCommand(ctx, args) {
|
|
1509
|
+
const sub = args.shift() ?? "status";
|
|
1510
|
+
if (sub === "set" || sub === "grant") {
|
|
1511
|
+
const userID = requireArg(args, 0, "user id");
|
|
1512
|
+
const tier = requireArg(args, 1, "tier");
|
|
1513
|
+
if (!["free", "plus", "pro"].includes(tier)) {
|
|
1514
|
+
throw new Error("Tier must be one of: free, plus, pro.");
|
|
1515
|
+
}
|
|
1516
|
+
const devKey = ctx.clientOptions.devApiKey;
|
|
1517
|
+
if (!devKey) {
|
|
1518
|
+
throw new Error(
|
|
1519
|
+
"A dev key is required. Pass --dev-key or set DEV_API_KEY.",
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
const expiresAt = ctx.flags["expires-at"]
|
|
1523
|
+
? String(ctx.flags["expires-at"])
|
|
1524
|
+
: null;
|
|
1525
|
+
const res = await fetch(`${apiBaseUrl(ctx)}/__dev/billing/grants`, {
|
|
1526
|
+
body: JSON.stringify({ expiresAt, tier, userID }),
|
|
1527
|
+
headers: {
|
|
1528
|
+
"Content-Type": "application/json",
|
|
1529
|
+
"x-dev-api-key": devKey,
|
|
1530
|
+
},
|
|
1531
|
+
method: "POST",
|
|
1532
|
+
});
|
|
1533
|
+
if (!res.ok) {
|
|
1534
|
+
throw new Error(
|
|
1535
|
+
`Entitlement grant failed with HTTP ${String(res.status)}: ${await res.text()}`,
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
const body = await res.json();
|
|
1539
|
+
console.log(
|
|
1540
|
+
`${color(ROOT_ACCENT, "granted")} ${color("bold", tier)} ${color("dim", `to ${userID}`)} ${color("dim", `source=${body.source ?? "unknown"}`)}`,
|
|
1541
|
+
);
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
throw new Error(
|
|
1545
|
+
"Usage: vex entitlements set <user-id> <free|plus|pro> [--expires-at <iso>] --dev-key <key>",
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1446
1549
|
async function groupCommand(ctx, args) {
|
|
1447
1550
|
const sub = args[0];
|
|
1448
1551
|
if (sub === "send") {
|
|
@@ -4590,6 +4693,7 @@ Commands:
|
|
|
4590
4693
|
vex auth requests list pending device login requests
|
|
4591
4694
|
vex auth accounts
|
|
4592
4695
|
vex auth use <username>
|
|
4696
|
+
vex entitlements set <user-id> <free|plus|pro> --dev-key <key>
|
|
4593
4697
|
vex whoami
|
|
4594
4698
|
|
|
4595
4699
|
Flags:
|