@usethrottle/cli 1.0.4 → 2.0.1
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 +179 -33
- package/dist/cli.js +651 -311
- package/dist/cli.js.map +1 -1
- package/package.json +12 -11
package/dist/cli.js
CHANGED
|
@@ -3,8 +3,31 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
|
-
// src/
|
|
7
|
-
import
|
|
6
|
+
// src/version.ts
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
var pkg = JSON.parse(
|
|
10
|
+
readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8")
|
|
11
|
+
);
|
|
12
|
+
var CLI_VERSION = pkg.version;
|
|
13
|
+
|
|
14
|
+
// src/banner.ts
|
|
15
|
+
var BRAND_BLUE = "\x1B[38;2;29;86;232m";
|
|
16
|
+
var RESET = "\x1B[0m";
|
|
17
|
+
var DIM = "\x1B[2m";
|
|
18
|
+
var BOLD = "\x1B[1m";
|
|
19
|
+
function printBanner(opts) {
|
|
20
|
+
if (process.env.THROTTLE_CLI_NO_BANNER === "1") return;
|
|
21
|
+
if (!process.stdout.isTTY) return;
|
|
22
|
+
const v = opts?.version ? `v${opts.version}` : "";
|
|
23
|
+
process.stderr.write(
|
|
24
|
+
`${BRAND_BLUE}${BOLD}\u25B8 throttle${RESET} ${DIM}CLI${v ? " " + v : ""} \xB7 docs https://docs.usethrottle.dev${RESET}
|
|
25
|
+
|
|
26
|
+
`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/lib/errors.ts
|
|
8
31
|
import pc from "picocolors";
|
|
9
32
|
|
|
10
33
|
// ../errors/dist/index.js
|
|
@@ -23,22 +46,29 @@ var ThrottleError = class extends Error {
|
|
|
23
46
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
24
47
|
}
|
|
25
48
|
};
|
|
49
|
+
function isThrottleError(value) {
|
|
50
|
+
return value instanceof ThrottleError;
|
|
51
|
+
}
|
|
26
52
|
|
|
27
|
-
// src/lib/
|
|
53
|
+
// src/lib/client.ts
|
|
28
54
|
var ThrottleApiError = class extends ThrottleError {
|
|
29
55
|
constructor(code, message, statusCode) {
|
|
30
56
|
super({ code, message, statusCode });
|
|
31
57
|
this.name = "ThrottleApiError";
|
|
32
58
|
}
|
|
33
59
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
60
|
+
function url(auth, path) {
|
|
61
|
+
return `${auth.apiBaseUrl.replace(/\/$/, "")}${path}`;
|
|
62
|
+
}
|
|
63
|
+
function headers(auth, init2) {
|
|
64
|
+
return { "content-type": "application/json", "x-api-key": auth.apiKey, ...init2.headers ?? {} };
|
|
65
|
+
}
|
|
66
|
+
async function callThrottle(auth, path, init2 = {}) {
|
|
67
|
+
const method = (init2.method ?? "GET").toUpperCase();
|
|
68
|
+
if (init2.body == null && (method === "POST" || method === "PATCH" || method === "PUT")) {
|
|
69
|
+
init2 = { ...init2, body: "{}" };
|
|
70
|
+
}
|
|
71
|
+
const res = await fetch(url(auth, path), { ...init2, headers: headers(auth, init2) });
|
|
42
72
|
const text = await res.text();
|
|
43
73
|
let body = {};
|
|
44
74
|
try {
|
|
@@ -50,248 +80,41 @@ async function callThrottle(opts, path, init2 = {}) {
|
|
|
50
80
|
const message = body.error?.message ?? `Request failed (${res.status}): ${text.slice(0, 120)}`;
|
|
51
81
|
throw new ThrottleApiError(code, message, res.status);
|
|
52
82
|
}
|
|
53
|
-
return body.data
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// src/commands/bc-connect.ts
|
|
57
|
-
async function bcConnect(options) {
|
|
58
|
-
const apiKey = options.apiKey ?? process.env.THROTTLE_API_KEY ?? await promptApiKey();
|
|
59
|
-
const storeHash = options.storeHash ?? await promptStoreHash();
|
|
60
|
-
const accessToken = options.accessToken ?? await promptAccessToken();
|
|
61
|
-
console.log();
|
|
62
|
-
console.log(pc.dim("Connecting..."));
|
|
63
|
-
try {
|
|
64
|
-
const result = await callThrottle(
|
|
65
|
-
{ apiBaseUrl: options.apiBaseUrl, apiKey },
|
|
66
|
-
"/api/v1/bigcommerce/connect",
|
|
67
|
-
{
|
|
68
|
-
method: "POST",
|
|
69
|
-
body: JSON.stringify({ storeHash, accessToken })
|
|
70
|
-
}
|
|
71
|
-
);
|
|
72
|
-
console.log(pc.green("\u2713") + ` BigCommerce store ${pc.bold(storeHash)} connected`);
|
|
73
|
-
console.log(pc.dim(` Connection id: ${result.id}`));
|
|
74
|
-
console.log(pc.dim(` Webhooks created: ${result.webhookCount}`));
|
|
75
|
-
console.log(pc.dim(` Status: ${result.status}`));
|
|
76
|
-
console.log();
|
|
77
|
-
console.log(pc.dim("Next steps:"));
|
|
78
|
-
console.log(pc.dim(" \u2022 Test by placing a sandbox order \u2014 throttle should receive a webhook"));
|
|
79
|
-
console.log(pc.dim(" \u2022 Run `throttle bc-disconnect` if you ever need to remove this connection"));
|
|
80
|
-
} catch (err) {
|
|
81
|
-
if (err instanceof ThrottleApiError) {
|
|
82
|
-
console.error(pc.red("\u2717") + ` Failed (${err.code}): ${err.message}`);
|
|
83
|
-
if (err.code === "invalid_bc_credentials") {
|
|
84
|
-
console.error(
|
|
85
|
-
pc.dim(" Re-check your store hash and API token in your BigCommerce dashboard.")
|
|
86
|
-
);
|
|
87
|
-
console.error(
|
|
88
|
-
pc.dim(
|
|
89
|
-
" Store hash: settings \u2192 API accounts \u2192 store URL contains it (e.g. abc123 in stores/abc123/)"
|
|
90
|
-
)
|
|
91
|
-
);
|
|
92
|
-
console.error(
|
|
93
|
-
pc.dim(
|
|
94
|
-
" API token: settings \u2192 advanced settings \u2192 API accounts \u2192 create V2/V3 API token"
|
|
95
|
-
)
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
process.exitCode = 1;
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
throw err;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
async function promptApiKey() {
|
|
105
|
-
const res = await prompts(
|
|
106
|
-
{
|
|
107
|
-
type: "password",
|
|
108
|
-
name: "apiKey",
|
|
109
|
-
message: "Throttle API key (sk_...)",
|
|
110
|
-
validate: (v) => v.startsWith("sk_") && v.length > 20 ? true : "Must look like sk_\u2026 (paste from https://checkout.usethrottle.dev/welcome)"
|
|
111
|
-
},
|
|
112
|
-
{ onCancel: () => process.exit(1) }
|
|
113
|
-
);
|
|
114
|
-
return res.apiKey;
|
|
115
|
-
}
|
|
116
|
-
async function promptStoreHash() {
|
|
117
|
-
const res = await prompts(
|
|
118
|
-
{
|
|
119
|
-
type: "text",
|
|
120
|
-
name: "storeHash",
|
|
121
|
-
message: "BigCommerce store hash (e.g. abc123 from stores/abc123/...)",
|
|
122
|
-
validate: (v) => v.length >= 4 ? true : "Must be at least 4 characters"
|
|
123
|
-
},
|
|
124
|
-
{ onCancel: () => process.exit(1) }
|
|
125
|
-
);
|
|
126
|
-
return res.storeHash.trim();
|
|
127
|
-
}
|
|
128
|
-
async function promptAccessToken() {
|
|
129
|
-
console.log(
|
|
130
|
-
pc.dim(
|
|
131
|
-
" Generate a token at: BigCommerce dashboard \u2192 Settings \u2192 API accounts \u2192 Create API account"
|
|
132
|
-
)
|
|
133
|
-
);
|
|
134
|
-
console.log(
|
|
135
|
-
pc.dim(
|
|
136
|
-
' Required scopes: Orders, Customers, Carts, Webhooks (all "Modify"); Information & Products (Read-only)'
|
|
137
|
-
)
|
|
138
|
-
);
|
|
139
|
-
const res = await prompts(
|
|
140
|
-
{
|
|
141
|
-
type: "password",
|
|
142
|
-
name: "accessToken",
|
|
143
|
-
message: "BigCommerce API token",
|
|
144
|
-
validate: (v) => v.length >= 10 ? true : "Token looks too short \u2014 paste the full value"
|
|
145
|
-
},
|
|
146
|
-
{ onCancel: () => process.exit(1) }
|
|
147
|
-
);
|
|
148
|
-
return res.accessToken;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// src/commands/bc-disconnect.ts
|
|
152
|
-
import prompts2 from "prompts";
|
|
153
|
-
import pc2 from "picocolors";
|
|
154
|
-
async function bcDisconnect(options) {
|
|
155
|
-
const apiKey = options.apiKey ?? process.env.THROTTLE_API_KEY ?? await promptApiKey2();
|
|
156
|
-
const connectionId = options.connectionId ?? await promptConnectionId();
|
|
157
|
-
if (!options.yes) {
|
|
158
|
-
const confirm = await prompts2(
|
|
159
|
-
{
|
|
160
|
-
type: "confirm",
|
|
161
|
-
name: "ok",
|
|
162
|
-
message: `Disconnect ${connectionId}? (will be hard-deleted in 90 days)`,
|
|
163
|
-
initial: false
|
|
164
|
-
},
|
|
165
|
-
{ onCancel: () => process.exit(1) }
|
|
166
|
-
);
|
|
167
|
-
if (!confirm.ok) {
|
|
168
|
-
console.log(pc2.dim("Aborted."));
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
try {
|
|
173
|
-
await callThrottle(
|
|
174
|
-
{ apiBaseUrl: options.apiBaseUrl, apiKey },
|
|
175
|
-
`/api/v1/bigcommerce/connections/${encodeURIComponent(connectionId)}`,
|
|
176
|
-
{ method: "DELETE" }
|
|
177
|
-
);
|
|
178
|
-
console.log(pc2.green("\u2713") + ` Disconnected ${connectionId}`);
|
|
179
|
-
} catch (err) {
|
|
180
|
-
if (err instanceof ThrottleApiError) {
|
|
181
|
-
console.error(pc2.red("\u2717") + ` Failed (${err.code}): ${err.message}`);
|
|
182
|
-
process.exitCode = 1;
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
throw err;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
async function promptApiKey2() {
|
|
189
|
-
const res = await prompts2(
|
|
190
|
-
{ type: "password", name: "apiKey", message: "Throttle API key (sk_...)" },
|
|
191
|
-
{ onCancel: () => process.exit(1) }
|
|
192
|
-
);
|
|
193
|
-
return res.apiKey;
|
|
83
|
+
return body && typeof body === "object" && "data" in body ? body.data : body;
|
|
194
84
|
}
|
|
195
|
-
async function
|
|
196
|
-
const res = await
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return res.id.trim();
|
|
85
|
+
async function callThrottleText(auth, path, init2 = {}) {
|
|
86
|
+
const res = await fetch(url(auth, path), { ...init2, headers: headers(auth, init2) });
|
|
87
|
+
const text = await res.text();
|
|
88
|
+
if (!res.ok) throw new ThrottleApiError(`http_${res.status}`, text.slice(0, 120), res.status);
|
|
89
|
+
return text;
|
|
201
90
|
}
|
|
202
91
|
|
|
203
|
-
// src/
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return res.apiKey;
|
|
219
|
-
}
|
|
220
|
-
function printConfig(cfg) {
|
|
221
|
-
console.log(pc3.dim(" allowedOrigins:"));
|
|
222
|
-
if (cfg.allowedOrigins.length === 0) {
|
|
223
|
-
console.log(pc3.dim(" (none \u2014 embeds are disabled until you add an origin)"));
|
|
224
|
-
} else {
|
|
225
|
-
for (const o of cfg.allowedOrigins) console.log(pc3.dim(` ${o}`));
|
|
226
|
-
}
|
|
227
|
-
console.log(pc3.dim(` primaryColor: ${cfg.primaryColor ?? "(unset)"}`));
|
|
228
|
-
console.log(pc3.dim(` logoUrl: ${cfg.logoUrl ?? "(unset)"}`));
|
|
229
|
-
console.log(pc3.dim(` merchantName: ${cfg.merchantName ?? "(unset)"}`));
|
|
230
|
-
}
|
|
231
|
-
async function embedConfigShow(options) {
|
|
232
|
-
const apiKey = await resolveApiKey(options.apiKey);
|
|
233
|
-
try {
|
|
234
|
-
const data = await callThrottle(
|
|
235
|
-
{ apiBaseUrl: options.apiBaseUrl, apiKey },
|
|
236
|
-
"/api/v1/embed-config",
|
|
237
|
-
{ method: "GET" }
|
|
238
|
-
);
|
|
239
|
-
console.log(pc3.green("\u2713") + " Current embed config");
|
|
240
|
-
printConfig(data);
|
|
241
|
-
} catch (err) {
|
|
242
|
-
if (err instanceof ThrottleApiError) {
|
|
243
|
-
console.error(pc3.red("\u2717") + ` Failed (${err.code}): ${err.message}`);
|
|
244
|
-
process.exitCode = 1;
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
throw err;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
async function embedConfigSet(options) {
|
|
251
|
-
const apiKey = await resolveApiKey(options.apiKey);
|
|
252
|
-
const body = {};
|
|
253
|
-
if (options.origins) {
|
|
254
|
-
body.allowedOrigins = options.origins.split(",").map((s) => s.trim()).filter(Boolean);
|
|
255
|
-
}
|
|
256
|
-
if (options.primary) body.primaryColor = options.primary;
|
|
257
|
-
if (options.logo) body.logoUrl = options.logo;
|
|
258
|
-
if (options.merchantName) body.merchantName = options.merchantName;
|
|
259
|
-
if (Object.keys(body).length === 0) {
|
|
260
|
-
console.error(
|
|
261
|
-
pc3.red("\u2717") + " No fields provided. Pass --origins, --primary, --logo, or --merchant-name."
|
|
262
|
-
);
|
|
263
|
-
process.exitCode = 1;
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
try {
|
|
267
|
-
const data = await callThrottle(
|
|
268
|
-
{ apiBaseUrl: options.apiBaseUrl, apiKey },
|
|
269
|
-
"/api/v1/embed-config",
|
|
270
|
-
{ method: "PUT", body: JSON.stringify(body) }
|
|
271
|
-
);
|
|
272
|
-
console.log(pc3.green("\u2713") + " Embed config updated");
|
|
273
|
-
printConfig(data);
|
|
274
|
-
} catch (err) {
|
|
275
|
-
if (err instanceof ThrottleApiError) {
|
|
276
|
-
console.error(pc3.red("\u2717") + ` Failed (${err.code}): ${err.message}`);
|
|
277
|
-
if (err.code === "invalid_origin") {
|
|
278
|
-
console.error(
|
|
279
|
-
pc3.dim(
|
|
280
|
-
" Origins must be https://host[:port]. No paths, no trailing slash, no wildcards."
|
|
281
|
-
)
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
process.exitCode = 1;
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
throw err;
|
|
92
|
+
// src/lib/errors.ts
|
|
93
|
+
var HINTS = {
|
|
94
|
+
insufficient_scopes: "Your API key lacks a required scope. Mint a key with the needed scope in the dashboard.",
|
|
95
|
+
environment_mismatch: "This resource is in a different environment than your key. Use a key for the right environment.",
|
|
96
|
+
refund_amount_exceeds_balance: "The refund is larger than the captured balance remaining on this payment.",
|
|
97
|
+
invalid_api_key: "The API key is invalid or expired. Re-run `throttle config set` with a fresh key.",
|
|
98
|
+
not_found: "Not found (or your key cannot see it in this environment)."
|
|
99
|
+
};
|
|
100
|
+
function handleError(err) {
|
|
101
|
+
if (isThrottleError(err)) {
|
|
102
|
+
process.stderr.write(pc.red("\u2717") + ` (${err.code}): ${err.message}
|
|
103
|
+
`);
|
|
104
|
+
const hint = HINTS[err.code];
|
|
105
|
+
if (hint) process.stderr.write(pc.dim(" " + hint) + "\n");
|
|
106
|
+
return 1;
|
|
288
107
|
}
|
|
108
|
+
const exitCode = typeof err?.exitCode === "number" ? err.exitCode : 1;
|
|
109
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
110
|
+
process.stderr.write(pc.red("\u2717") + " " + message + "\n");
|
|
111
|
+
return exitCode;
|
|
289
112
|
}
|
|
290
113
|
|
|
291
114
|
// src/sentry.ts
|
|
292
|
-
import { readFileSync } from "fs";
|
|
115
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
293
116
|
import { dirname, resolve } from "path";
|
|
294
|
-
import { fileURLToPath } from "url";
|
|
117
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
295
118
|
import * as Sentry from "@sentry/node";
|
|
296
119
|
function normalizeKey(key) {
|
|
297
120
|
return key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
@@ -329,8 +152,8 @@ function isOptInValue(value) {
|
|
|
329
152
|
}
|
|
330
153
|
function readCliPackageRelease() {
|
|
331
154
|
try {
|
|
332
|
-
const packageJsonPath = resolve(dirname(
|
|
333
|
-
const packageJson = JSON.parse(
|
|
155
|
+
const packageJsonPath = resolve(dirname(fileURLToPath2(import.meta.url)), "../package.json");
|
|
156
|
+
const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
|
|
334
157
|
return packageJson.version ? `@usethrottle/cli@${packageJson.version}` : void 0;
|
|
335
158
|
} catch {
|
|
336
159
|
return void 0;
|
|
@@ -381,81 +204,598 @@ async function flushCliSentry(timeout = 2e3) {
|
|
|
381
204
|
await Sentry.flush(timeout);
|
|
382
205
|
}
|
|
383
206
|
|
|
384
|
-
// src/
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
var
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
207
|
+
// src/config/profiles.ts
|
|
208
|
+
import { mkdirSync, readFileSync as readFileSync3, writeFileSync, existsSync, chmodSync } from "fs";
|
|
209
|
+
import { homedir } from "os";
|
|
210
|
+
import { join } from "path";
|
|
211
|
+
var DEFAULT_API_URL = "https://api.usethrottle.dev";
|
|
212
|
+
var ConfigError = class extends Error {
|
|
213
|
+
exitCode = 2;
|
|
214
|
+
};
|
|
215
|
+
function home() {
|
|
216
|
+
return process.env.THROTTLE_CONFIG_HOME || join(homedir(), ".throttle");
|
|
217
|
+
}
|
|
218
|
+
function configPath() {
|
|
219
|
+
return join(home(), "config.json");
|
|
220
|
+
}
|
|
221
|
+
function loadConfig() {
|
|
222
|
+
const p = configPath();
|
|
223
|
+
if (!existsSync(p)) return { profiles: {} };
|
|
224
|
+
try {
|
|
225
|
+
return JSON.parse(readFileSync3(p, "utf8"));
|
|
226
|
+
} catch {
|
|
227
|
+
throw new ConfigError(`Config at ${p} is not valid JSON`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function saveConfig(cfg) {
|
|
231
|
+
mkdirSync(home(), { recursive: true });
|
|
232
|
+
const p = configPath();
|
|
233
|
+
writeFileSync(p, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
234
|
+
chmodSync(p, 384);
|
|
235
|
+
}
|
|
236
|
+
function setProfile(name, prof) {
|
|
237
|
+
const cfg = loadConfig();
|
|
238
|
+
cfg.profiles[name] = prof;
|
|
239
|
+
if (!cfg.defaultProfile) cfg.defaultProfile = name;
|
|
240
|
+
saveConfig(cfg);
|
|
241
|
+
}
|
|
242
|
+
function useProfile(name) {
|
|
243
|
+
const cfg = loadConfig();
|
|
244
|
+
if (!cfg.profiles[name]) throw new ConfigError(`No profile named "${name}"`);
|
|
245
|
+
cfg.defaultProfile = name;
|
|
246
|
+
saveConfig(cfg);
|
|
247
|
+
}
|
|
248
|
+
function removeProfile(name) {
|
|
249
|
+
const cfg = loadConfig();
|
|
250
|
+
if (!cfg.profiles[name]) throw new ConfigError(`No profile named "${name}"`);
|
|
251
|
+
delete cfg.profiles[name];
|
|
252
|
+
if (cfg.defaultProfile === name) cfg.defaultProfile = Object.keys(cfg.profiles)[0];
|
|
253
|
+
saveConfig(cfg);
|
|
254
|
+
}
|
|
255
|
+
function maskKey(key) {
|
|
256
|
+
if (key.length <= 12) return "****";
|
|
257
|
+
const m = key.match(/^(sk|pk)_(live|test)_/);
|
|
258
|
+
const prefix = m ? m[0] : key.slice(0, 4);
|
|
259
|
+
return `${prefix}\u2026${key.slice(-4)}`;
|
|
260
|
+
}
|
|
261
|
+
function resolveAuth(globals) {
|
|
262
|
+
const cfg = loadConfig();
|
|
263
|
+
const profName = globals.profile ?? cfg.defaultProfile;
|
|
264
|
+
const prof = profName ? cfg.profiles[profName] : void 0;
|
|
265
|
+
if (globals.profile && !prof) throw new ConfigError(`No profile named "${globals.profile}"`);
|
|
266
|
+
const apiKey = globals.apiKey ?? process.env.THROTTLE_API_KEY ?? prof?.apiKey;
|
|
267
|
+
if (!apiKey) throw new ConfigError("No API key. Set one with `throttle config set <name> --api-key sk_...`, pass --api-key, or set $THROTTLE_API_KEY.");
|
|
268
|
+
const apiBaseUrl = globals.api ?? process.env.THROTTLE_API_URL ?? prof?.apiBaseUrl ?? DEFAULT_API_URL;
|
|
269
|
+
return { apiKey, apiBaseUrl };
|
|
270
|
+
}
|
|
400
271
|
|
|
272
|
+
// src/lib/output.ts
|
|
273
|
+
import pc2 from "picocolors";
|
|
274
|
+
function cell(v) {
|
|
275
|
+
return v === null || v === void 0 ? "" : String(v);
|
|
276
|
+
}
|
|
277
|
+
function printJson(data) {
|
|
278
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
279
|
+
}
|
|
280
|
+
function renderList(rows, columns, opts) {
|
|
281
|
+
if (opts.json) {
|
|
282
|
+
printJson(rows);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (rows.length === 0) {
|
|
286
|
+
process.stdout.write(pc2.dim("No results.\n"));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const headers2 = columns.map((c) => c.header);
|
|
290
|
+
const body = rows.map((r) => columns.map((c) => cell(c.get(r))));
|
|
291
|
+
const widths = headers2.map((h, i) => Math.max(h.length, ...body.map((row) => row[i].length)));
|
|
292
|
+
const line = (cells) => cells.map((v, i) => v.padEnd(widths[i])).join(" ").trimEnd();
|
|
293
|
+
process.stdout.write(pc2.bold(line(headers2)) + "\n");
|
|
294
|
+
for (const row of body) process.stdout.write(line(row) + "\n");
|
|
295
|
+
}
|
|
296
|
+
function renderObject(obj, opts) {
|
|
297
|
+
if (opts.json) {
|
|
298
|
+
printJson(obj);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (obj === null || typeof obj !== "object") {
|
|
302
|
+
process.stdout.write(cell(obj) + "\n");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const entries = Object.entries(obj);
|
|
306
|
+
const w = Math.max(0, ...entries.map(([k]) => k.length));
|
|
307
|
+
for (const [k, v] of entries) {
|
|
308
|
+
const val = v !== null && typeof v === "object" ? JSON.stringify(v) : cell(v);
|
|
309
|
+
process.stdout.write(`${pc2.dim(k.padEnd(w))} ${val}
|
|
401
310
|
`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function success(msg) {
|
|
314
|
+
process.stdout.write(pc2.green("\u2713") + " " + msg + "\n");
|
|
402
315
|
}
|
|
403
316
|
|
|
404
|
-
// src/
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
);
|
|
410
|
-
|
|
317
|
+
// src/commands/config.ts
|
|
318
|
+
function register(program2) {
|
|
319
|
+
const config = program2.command("config").description("Manage local CLI profiles (~/.throttle/config.json)");
|
|
320
|
+
config.command("set <profile>").description("Create or update a profile").option("--api-key <sk_>", "Throttle API key").option("--api <url>", "API base URL", "https://api.usethrottle.dev").option("--label <text>", "Human label").action((name, opts, cmd) => {
|
|
321
|
+
const g = cmd.optsWithGlobals();
|
|
322
|
+
if (!g.apiKey) throw new ConfigError("required option '--api-key <sk_>' not specified");
|
|
323
|
+
setProfile(name, { apiKey: g.apiKey, apiBaseUrl: g.api, label: opts.label });
|
|
324
|
+
success(`Saved profile "${name}"`);
|
|
325
|
+
});
|
|
326
|
+
config.command("use <profile>").description("Set the default profile").action((name) => {
|
|
327
|
+
useProfile(name);
|
|
328
|
+
success(`Default profile is now "${name}"`);
|
|
329
|
+
});
|
|
330
|
+
config.command("remove <profile>").description("Delete a profile").action((name) => {
|
|
331
|
+
removeProfile(name);
|
|
332
|
+
success(`Removed profile "${name}"`);
|
|
333
|
+
});
|
|
334
|
+
config.command("list").description("List profiles (keys masked)").option("--json", "Output JSON").action((opts, cmd) => {
|
|
335
|
+
const g = cmd.optsWithGlobals();
|
|
336
|
+
const cfg = loadConfig();
|
|
337
|
+
const rows = Object.entries(cfg.profiles).map(([name, p]) => ({
|
|
338
|
+
name,
|
|
339
|
+
default: name === cfg.defaultProfile ? "*" : "",
|
|
340
|
+
apiBaseUrl: p.apiBaseUrl,
|
|
341
|
+
apiKey: maskKey(p.apiKey),
|
|
342
|
+
label: p.label ?? ""
|
|
343
|
+
}));
|
|
344
|
+
renderList(rows, [
|
|
345
|
+
{ header: "NAME", get: (r) => r.name },
|
|
346
|
+
{ header: "DEFAULT", get: (r) => r.default },
|
|
347
|
+
{ header: "API", get: (r) => r.apiBaseUrl },
|
|
348
|
+
{ header: "KEY", get: (r) => r.apiKey },
|
|
349
|
+
{ header: "LABEL", get: (r) => r.label }
|
|
350
|
+
], { json: Boolean(g.json) });
|
|
351
|
+
});
|
|
352
|
+
}
|
|
411
353
|
|
|
412
|
-
// src/
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if (!isHelpOrVersion) {
|
|
418
|
-
printBanner({ version: CLI_VERSION });
|
|
354
|
+
// src/commands/whoami.ts
|
|
355
|
+
function modeFromKey(key) {
|
|
356
|
+
if (key.startsWith("sk_live_") || key.startsWith("pk_live_")) return "live";
|
|
357
|
+
if (key.startsWith("sk_test_") || key.startsWith("pk_test_")) return "test";
|
|
358
|
+
return "unknown";
|
|
419
359
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
360
|
+
function storedPrefix(key) {
|
|
361
|
+
const hex = key.replace(/^(sk|pk)_(test|live)_/, "");
|
|
362
|
+
return `${key.slice(0, 2)}_${hex.slice(0, 16)}`;
|
|
363
|
+
}
|
|
364
|
+
function register2(program2) {
|
|
365
|
+
program2.command("whoami").description("Show the active key context (environment + granted scopes)").action(async (_opts, cmd) => {
|
|
366
|
+
const g = cmd.optsWithGlobals();
|
|
367
|
+
const auth = resolveAuth(g);
|
|
368
|
+
const info = {
|
|
369
|
+
apiBaseUrl: auth.apiBaseUrl,
|
|
370
|
+
mode: modeFromKey(auth.apiKey),
|
|
371
|
+
keyPrefix: storedPrefix(auth.apiKey)
|
|
372
|
+
};
|
|
373
|
+
try {
|
|
374
|
+
const keys = await callThrottle(auth, "/api/v1/api-keys");
|
|
375
|
+
const first = keys[0];
|
|
376
|
+
if (first) {
|
|
377
|
+
info.applicationId = first.applicationId;
|
|
378
|
+
info.environmentId = first.environmentId;
|
|
379
|
+
if (first.environmentSlug) info.environment = first.environmentSlug;
|
|
380
|
+
if (first.environmentKind) info.environmentKind = first.environmentKind;
|
|
381
|
+
}
|
|
382
|
+
const mine = keys.find((k) => k.keyPrefix === info.keyPrefix);
|
|
383
|
+
info.scopes = mine ? mine.scopes : "(this key not found in the catalog)";
|
|
384
|
+
info.authenticated = true;
|
|
385
|
+
} catch (err) {
|
|
386
|
+
if (!isThrottleError(err)) throw err;
|
|
387
|
+
info.authenticated = err.statusCode === 401 ? false : true;
|
|
388
|
+
if (err.statusCode !== 401) {
|
|
389
|
+
info.note = `key valid; environment/scopes unavailable to api keys (${err.code})`;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
renderObject(info, { json: Boolean(g.json) });
|
|
428
393
|
});
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/commands/orders.ts
|
|
397
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
398
|
+
|
|
399
|
+
// src/lib/confirm.ts
|
|
400
|
+
import prompts from "prompts";
|
|
401
|
+
var ConfirmError = class extends Error {
|
|
402
|
+
exitCode;
|
|
403
|
+
constructor(m, code = 1) {
|
|
404
|
+
super(m);
|
|
405
|
+
this.exitCode = code;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
async function confirmAction(opts) {
|
|
409
|
+
if (opts.yes) return;
|
|
410
|
+
if (opts.json || !process.stdin.isTTY) {
|
|
411
|
+
throw new ConfirmError("This action needs confirmation. Re-run with --yes to proceed non-interactively.", 2);
|
|
412
|
+
}
|
|
413
|
+
const res = await prompts({ type: "confirm", name: "ok", message: opts.message, initial: false }, { onCancel: () => {
|
|
414
|
+
throw new ConfirmError("Aborted.", 1);
|
|
415
|
+
} });
|
|
416
|
+
if (!res.ok) throw new ConfirmError("Aborted.", 1);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/commands/fulfillments.ts
|
|
420
|
+
var base = (orderId) => `/api/v1/orders/${orderId}/fulfillments`;
|
|
421
|
+
function registerFulfillments(orders) {
|
|
422
|
+
const f = orders.command("fulfillments").description("Order fulfillments");
|
|
423
|
+
f.command("list <orderId>").action(async (orderId, _o, cmd) => {
|
|
424
|
+
const g = cmd.optsWithGlobals();
|
|
425
|
+
const data = await callThrottle(resolveAuth(g), base(orderId));
|
|
426
|
+
renderList(data, [{ header: "ID", get: (x) => x.id }, { header: "STATUS", get: (x) => x.status }, { header: "TYPE", get: (x) => x.type }], { json: Boolean(g.json) });
|
|
436
427
|
});
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
await embedConfigShow({ apiBaseUrl: opts.api, apiKey: opts.apiKey });
|
|
441
|
-
});
|
|
442
|
-
embedConfig.command("set").description("Update the embed config (any combination of --origins, --primary, --logo, --merchant-name)").option("--api <url>", "Throttle API base URL", process.env.THROTTLE_API_URL ?? DEFAULT_API_URL).option("--api-key <sk_*>", "Throttle API key (else $THROTTLE_API_KEY else interactive prompt)").option(
|
|
443
|
-
"--origins <list>",
|
|
444
|
-
"Comma-separated parent origins (e.g. https://shop.example.com,https://staging.example.com). No paths, no trailing slash, no wildcards."
|
|
445
|
-
).option("--primary <hex>", "Primary brand color in #rrggbb form").option("--logo <url>", "Logo URL (https)").option("--merchant-name <name>", "Merchant display name").action(async (opts) => {
|
|
446
|
-
await embedConfigSet({
|
|
447
|
-
apiBaseUrl: opts.api,
|
|
448
|
-
apiKey: opts.apiKey,
|
|
449
|
-
origins: opts.origins,
|
|
450
|
-
primary: opts.primary,
|
|
451
|
-
logo: opts.logo,
|
|
452
|
-
merchantName: opts.merchantName
|
|
428
|
+
f.command("get <orderId> <id>").action(async (orderId, id, _o, cmd) => {
|
|
429
|
+
const g = cmd.optsWithGlobals();
|
|
430
|
+
renderObject(await callThrottle(resolveAuth(g), `${base(orderId)}/${id}`), { json: Boolean(g.json) });
|
|
453
431
|
});
|
|
454
|
-
|
|
432
|
+
f.command("create <orderId>").option("--items <json>", "JSON array of items").action(async (orderId, opts, cmd) => {
|
|
433
|
+
const g = cmd.optsWithGlobals();
|
|
434
|
+
await confirmAction({ message: `Create a fulfillment on ${orderId}?`, yes: g.yes, json: g.json });
|
|
435
|
+
const body = opts.items ? { items: JSON.parse(opts.items) } : {};
|
|
436
|
+
renderObject(await callThrottle(resolveAuth(g), base(orderId), { method: "POST", body: JSON.stringify(body) }), { json: Boolean(g.json) });
|
|
437
|
+
});
|
|
438
|
+
const transition = (name, verb, suffix, msg) => f.command(`${name} <orderId> <id>`).action(async (orderId, id, _o, cmd) => {
|
|
439
|
+
const g = cmd.optsWithGlobals();
|
|
440
|
+
await confirmAction({ message: msg(orderId, id), yes: g.yes, json: g.json });
|
|
441
|
+
const r = await callThrottle(resolveAuth(g), `${base(orderId)}/${id}/${suffix}`, { method: verb });
|
|
442
|
+
g.json ? renderObject(r, { json: true }) : success(`Fulfillment ${id} \u2192 ${name}`);
|
|
443
|
+
});
|
|
444
|
+
transition("processing", "POST", "processing", (o, i) => `Mark fulfillment ${i} processing?`);
|
|
445
|
+
transition("complete", "POST", "complete", (o, i) => `Complete fulfillment ${i}?`);
|
|
446
|
+
transition("cancel", "POST", "cancel", (o, i) => `Cancel fulfillment ${i}?`);
|
|
447
|
+
f.command("ship <orderId> <id>").requiredOption("--carrier <c>").requiredOption("--tracking <t>").action(async (orderId, id, opts, cmd) => {
|
|
448
|
+
const g = cmd.optsWithGlobals();
|
|
449
|
+
await confirmAction({ message: `Mark fulfillment ${id} shipped (${opts.carrier} ${opts.tracking})?`, yes: g.yes, json: g.json });
|
|
450
|
+
const r = await callThrottle(resolveAuth(g), `${base(orderId)}/${id}/shipment`, { method: "PATCH", body: JSON.stringify({ carrier: opts.carrier, trackingNumber: opts.tracking }) });
|
|
451
|
+
g.json ? renderObject(r, { json: true }) : success(`Fulfillment ${id} shipped`);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/lib/qs.ts
|
|
456
|
+
function qs(params) {
|
|
457
|
+
const s = new URLSearchParams();
|
|
458
|
+
for (const [k, v] of Object.entries(params)) if (v !== void 0) s.set(k, v);
|
|
459
|
+
const str = s.toString();
|
|
460
|
+
return str ? `?${str}` : "";
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/commands/orders.ts
|
|
464
|
+
function register3(program2) {
|
|
465
|
+
const orders = program2.command("orders").description("Orders + order detail");
|
|
466
|
+
orders.command("list").description("List orders").option("--status <s>").option("--payment-status <s>").option("--type <t>").option("--source <s>").option("-q, --query <text>").option("--limit <n>").option("--cursor <c>").action(async (opts, cmd) => {
|
|
467
|
+
const g = cmd.optsWithGlobals();
|
|
468
|
+
const auth = resolveAuth(g);
|
|
469
|
+
const path = "/api/v1/orders" + qs({ status: opts.status, paymentStatus: opts.paymentStatus, type: opts.type, source: opts.source, q: opts.query, limit: opts.limit, cursor: opts.cursor });
|
|
470
|
+
const data = await callThrottle(auth, path);
|
|
471
|
+
renderList(data, [
|
|
472
|
+
{ header: "ID", get: (o) => o.id },
|
|
473
|
+
{ header: "NUMBER", get: (o) => o.orderNumber },
|
|
474
|
+
{ header: "STATUS", get: (o) => o.status },
|
|
475
|
+
{ header: "PAYMENT", get: (o) => o.paymentStatus },
|
|
476
|
+
{ header: "TOTAL", get: (o) => `${(o.total / 100).toFixed(2)} ${o.currency}` }
|
|
477
|
+
], { json: Boolean(g.json) });
|
|
478
|
+
});
|
|
479
|
+
orders.command("get <id>").description("Show an order").action(async (id, _o, cmd) => {
|
|
480
|
+
const g = cmd.optsWithGlobals();
|
|
481
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/orders/${id}`), { json: Boolean(g.json) });
|
|
482
|
+
});
|
|
483
|
+
orders.command("cancel <id>").description("Cancel an order").action(async (id, _o, cmd) => {
|
|
484
|
+
const g = cmd.optsWithGlobals();
|
|
485
|
+
await confirmAction({ message: `Cancel order ${id}?`, yes: g.yes, json: g.json });
|
|
486
|
+
const r = await callThrottle(resolveAuth(g), `/api/v1/orders/${id}/cancel`, { method: "POST" });
|
|
487
|
+
g.json ? renderObject(r, { json: true }) : success(`Order ${id} cancelled`);
|
|
488
|
+
});
|
|
489
|
+
orders.command("export").description("Export orders as CSV (respects filters)").option("--status <s>").option("--payment-status <s>").option("--type <t>").option("--source <s>").option("-o, --out <file>", "Write to file instead of stdout").action(async (opts, cmd) => {
|
|
490
|
+
const g = cmd.optsWithGlobals();
|
|
491
|
+
const path = "/api/v1/orders/export.csv" + qs({ status: opts.status, paymentStatus: opts.paymentStatus, type: opts.type, source: opts.source });
|
|
492
|
+
const csv = await callThrottleText(resolveAuth(g), path);
|
|
493
|
+
if (opts.out) {
|
|
494
|
+
writeFileSync2(opts.out, csv);
|
|
495
|
+
success(`Wrote ${opts.out}`);
|
|
496
|
+
} else process.stdout.write(csv);
|
|
497
|
+
});
|
|
498
|
+
orders.command("line-items <id>").description("Edit order line items (JSON for add/update; comma ids for remove)").option("--add <json>", 'JSON array: [{"name","unitPrice","quantity"}]').option("--update <json>", 'JSON array: [{"lineItemId","quantity"?,"unitPrice"?}]').option("--remove <ids>", "Comma-separated line item ids").action(async (id, opts, cmd) => {
|
|
499
|
+
const g = cmd.optsWithGlobals();
|
|
500
|
+
const body = {};
|
|
501
|
+
if (opts.add) body.add = JSON.parse(opts.add);
|
|
502
|
+
if (opts.update) body.update = JSON.parse(opts.update);
|
|
503
|
+
if (opts.remove) body.remove = String(opts.remove).split(",").map((s) => s.trim());
|
|
504
|
+
await confirmAction({ message: `Edit line items on ${id} (may charge/refund the delta)?`, yes: g.yes, json: g.json });
|
|
505
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/orders/${id}/line-items`, { method: "PATCH", body: JSON.stringify(body) }), { json: Boolean(g.json) });
|
|
506
|
+
});
|
|
507
|
+
registerFulfillments(orders);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/commands/returns.ts
|
|
511
|
+
function register4(program2) {
|
|
512
|
+
const r = program2.command("returns").description("Returns / RMA (order detail \u2192 Returns)");
|
|
513
|
+
r.command("list").requiredOption("--order <orderId>").action(async (opts, cmd) => {
|
|
514
|
+
const g = cmd.optsWithGlobals();
|
|
515
|
+
const data = await callThrottle(resolveAuth(g), `/api/v1/orders/${opts.order}/returns`);
|
|
516
|
+
renderList(data, [{ header: "ID", get: (x) => x.id }, { header: "STATUS", get: (x) => x.status }, { header: "REFUND", get: (x) => x.refundAmount }], { json: Boolean(g.json) });
|
|
517
|
+
});
|
|
518
|
+
r.command("open").requiredOption("--order <orderId>").requiredOption("--items <json>", "JSON array of {lineItemId, quantity}").option("--reason <r>").option("--restock").action(async (opts, cmd) => {
|
|
519
|
+
const g = cmd.optsWithGlobals();
|
|
520
|
+
await confirmAction({ message: `Open a return on order ${opts.order}?`, yes: g.yes, json: g.json });
|
|
521
|
+
const body = { items: JSON.parse(opts.items), reason: opts.reason, restock: Boolean(opts.restock) };
|
|
522
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/orders/${opts.order}/returns`, { method: "POST", body: JSON.stringify(body) }), { json: Boolean(g.json) });
|
|
523
|
+
});
|
|
524
|
+
r.command("get <id>").action(async (id, _o, cmd) => {
|
|
525
|
+
const g = cmd.optsWithGlobals();
|
|
526
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/returns/${id}`), { json: Boolean(g.json) });
|
|
527
|
+
});
|
|
528
|
+
r.command("transition <id>").requiredOption("--action <a>", "approve|receive|complete|reject|cancel").action(async (id, opts, cmd) => {
|
|
529
|
+
const g = cmd.optsWithGlobals();
|
|
530
|
+
await confirmAction({ message: `Transition return ${id} \u2192 ${opts.action}?`, yes: g.yes, json: g.json });
|
|
531
|
+
const res = await callThrottle(resolveAuth(g), `/api/v1/returns/${id}/transition`, { method: "POST", body: JSON.stringify({ action: opts.action }) });
|
|
532
|
+
g.json ? renderObject(res, { json: true }) : success(`Return ${id} \u2192 ${opts.action}`);
|
|
533
|
+
});
|
|
534
|
+
r.command("exchange <id>").requiredOption("--items <json>", "JSON array of replacement items").action(async (id, opts, cmd) => {
|
|
535
|
+
const g = cmd.optsWithGlobals();
|
|
536
|
+
await confirmAction({ message: `Complete return ${id} as an exchange (may charge/refund the delta)?`, yes: g.yes, json: g.json });
|
|
537
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/returns/${id}/exchange`, { method: "POST", body: JSON.stringify({ items: JSON.parse(opts.items) }) }), { json: Boolean(g.json) });
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/commands/payments.ts
|
|
542
|
+
function register5(program2) {
|
|
543
|
+
const p = program2.command("payments").description("Payment actions (order detail + invoices)");
|
|
544
|
+
p.command("get <id>").action(async (id, _o, cmd) => {
|
|
545
|
+
const g = cmd.optsWithGlobals();
|
|
546
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/payments/${id}`), { json: Boolean(g.json) });
|
|
547
|
+
});
|
|
548
|
+
p.command("refund <id>").option("--amount <cents>", "Amount in minor units (default: full)").option("--reason <r>").action(async (id, opts, cmd) => {
|
|
549
|
+
const g = cmd.optsWithGlobals();
|
|
550
|
+
await confirmAction({ message: `Refund ${opts.amount ? opts.amount + " minor units" : "the full balance"} on payment ${id}?`, yes: g.yes, json: g.json });
|
|
551
|
+
const body = {};
|
|
552
|
+
if (opts.amount) body.amount = Number(opts.amount);
|
|
553
|
+
if (opts.reason) body.reason = opts.reason;
|
|
554
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/payments/${id}/refund`, { method: "POST", body: JSON.stringify(body) }), { json: Boolean(g.json) });
|
|
555
|
+
});
|
|
556
|
+
p.command("capture <id>").option("--amount <cents>", "Amount to capture (default: full authorized)").action(async (id, opts, cmd) => {
|
|
557
|
+
const g = cmd.optsWithGlobals();
|
|
558
|
+
await confirmAction({ message: `Capture ${opts.amount ? opts.amount + " minor units" : "the full authorized amount"} on payment ${id}?`, yes: g.yes, json: g.json });
|
|
559
|
+
const body = opts.amount ? { amount: Number(opts.amount) } : {};
|
|
560
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/payments/${id}/capture`, { method: "POST", body: JSON.stringify(body) }), { json: Boolean(g.json) });
|
|
561
|
+
});
|
|
562
|
+
p.command("resend-invoice <id>").description("Re-send the invoice email for a payment").action(async (id, _o, cmd) => {
|
|
563
|
+
const g = cmd.optsWithGlobals();
|
|
564
|
+
const r = await callThrottle(resolveAuth(g), `/api/v1/payments/${id}/invoice/resend`, { method: "POST" });
|
|
565
|
+
g.json ? renderObject(r, { json: true }) : success(`Invoice resent for payment ${id}`);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/commands/customers.ts
|
|
570
|
+
function register6(program2) {
|
|
571
|
+
const c = program2.command("customers").description("Customers");
|
|
572
|
+
c.command("list").option("-q, --query <text>").option("--limit <n>").option("--cursor <c>").action(async (opts, cmd) => {
|
|
573
|
+
const g = cmd.optsWithGlobals();
|
|
574
|
+
const data = await callThrottle(resolveAuth(g), "/api/v1/customers" + qs({ q: opts.query, limit: opts.limit, cursor: opts.cursor }));
|
|
575
|
+
renderList(data, [{ header: "ID", get: (x) => x.id }, { header: "EMAIL", get: (x) => x.email }, { header: "NAME", get: (x) => [x.firstName, x.lastName].filter(Boolean).join(" ") }, { header: "EXTERNAL", get: (x) => x.externalId }], { json: Boolean(g.json) });
|
|
576
|
+
});
|
|
577
|
+
c.command("get <id>").action(async (id, _o, cmd) => {
|
|
578
|
+
const g = cmd.optsWithGlobals();
|
|
579
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/customers/${id}`), { json: Boolean(g.json) });
|
|
580
|
+
});
|
|
581
|
+
c.command("create").requiredOption("--email <e>").option("--name <n>").option("--external-id <x>").action(async (opts, cmd) => {
|
|
582
|
+
const g = cmd.optsWithGlobals();
|
|
583
|
+
const body = { email: opts.email };
|
|
584
|
+
if (opts.name) body.firstName = opts.name;
|
|
585
|
+
if (opts.externalId) body.externalId = opts.externalId;
|
|
586
|
+
renderObject(await callThrottle(resolveAuth(g), "/api/v1/customers", { method: "POST", body: JSON.stringify(body) }), { json: Boolean(g.json) });
|
|
587
|
+
});
|
|
588
|
+
c.command("update <id>").option("--email <e>").option("--name <n>").option("--external-id <x>").action(async (id, opts, cmd) => {
|
|
589
|
+
const g = cmd.optsWithGlobals();
|
|
590
|
+
const body = {};
|
|
591
|
+
if (opts.email) body.email = opts.email;
|
|
592
|
+
if (opts.name) body.firstName = opts.name;
|
|
593
|
+
if (opts.externalId !== void 0) body.externalId = opts.externalId;
|
|
594
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/customers/${id}`, { method: "PATCH", body: JSON.stringify(body) }), { json: Boolean(g.json) });
|
|
595
|
+
});
|
|
596
|
+
const addr = c.command("addresses").description("Customer addresses");
|
|
597
|
+
addr.command("list <id>").action(async (id, _o, cmd) => {
|
|
598
|
+
const g = cmd.optsWithGlobals();
|
|
599
|
+
const data = await callThrottle(resolveAuth(g), `/api/v1/customers/${id}/addresses`);
|
|
600
|
+
renderList(data, [{ header: "ID", get: (x) => x.id }, { header: "LINE1", get: (x) => x.line1 }, { header: "CITY", get: (x) => x.city }, { header: "COUNTRY", get: (x) => x.country }], { json: Boolean(g.json) });
|
|
601
|
+
});
|
|
602
|
+
addr.command("add <id>").requiredOption("--json-body <json>", "Address JSON").action(async (id, opts, cmd) => {
|
|
603
|
+
const g = cmd.optsWithGlobals();
|
|
604
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/customers/${id}/addresses`, { method: "POST", body: opts.jsonBody }), { json: Boolean(g.json) });
|
|
605
|
+
});
|
|
606
|
+
addr.command("update <id> <addrId>").requiredOption("--json-body <json>", "Address JSON").action(async (id, addrId, opts, cmd) => {
|
|
607
|
+
const g = cmd.optsWithGlobals();
|
|
608
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/customers/${id}/addresses/${addrId}`, { method: "PATCH", body: opts.jsonBody }), { json: Boolean(g.json) });
|
|
609
|
+
});
|
|
610
|
+
addr.command("remove <id> <addrId>").action(async (id, addrId, _o, cmd) => {
|
|
611
|
+
const g = cmd.optsWithGlobals();
|
|
612
|
+
await confirmAction({ message: `Delete address ${addrId} on customer ${id}?`, yes: g.yes, json: g.json });
|
|
613
|
+
await callThrottle(resolveAuth(g), `/api/v1/customers/${id}/addresses/${addrId}`, { method: "DELETE" });
|
|
614
|
+
success(`Address ${addrId} removed`);
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/commands/subscriptions.ts
|
|
619
|
+
function register7(program2) {
|
|
620
|
+
const s = program2.command("subscriptions").description("Subscriptions");
|
|
621
|
+
s.command("list").option("--customer <id>").option("--limit <n>").option("--cursor <c>").action(async (opts, cmd) => {
|
|
622
|
+
const g = cmd.optsWithGlobals();
|
|
623
|
+
const data = await callThrottle(resolveAuth(g), "/api/v1/subscriptions" + qs({ customerId: opts.customer, limit: opts.limit, cursor: opts.cursor }));
|
|
624
|
+
renderList(data, [{ header: "ID", get: (x) => x.id }, { header: "STATUS", get: (x) => x.status }, { header: "PLAN", get: (x) => x.planName ?? x.planReference }, { header: "AMOUNT", get: (x) => x.amount }, { header: "RENEWS", get: (x) => x.currentPeriodEnd }], { json: Boolean(g.json) });
|
|
625
|
+
});
|
|
626
|
+
s.command("get <id>").action(async (id, _o, cmd) => {
|
|
627
|
+
const g = cmd.optsWithGlobals();
|
|
628
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/subscriptions/${id}`), { json: Boolean(g.json) });
|
|
629
|
+
});
|
|
630
|
+
s.command("cancel <id>").option("--at-period-end", "Cancel at the period boundary instead of now").action(async (id, opts, cmd) => {
|
|
631
|
+
const g = cmd.optsWithGlobals();
|
|
632
|
+
await confirmAction({ message: `Cancel subscription ${id}${opts.atPeriodEnd ? " at period end" : " now"}?`, yes: g.yes, json: g.json });
|
|
633
|
+
const res = await callThrottle(resolveAuth(g), `/api/v1/subscriptions/${id}/cancel`, { method: "POST", body: JSON.stringify({ atPeriodEnd: Boolean(opts.atPeriodEnd) }) });
|
|
634
|
+
g.json ? renderObject(res, { json: true }) : success(`Subscription ${id} cancelled`);
|
|
635
|
+
});
|
|
636
|
+
for (const verb of ["pause", "resume"]) {
|
|
637
|
+
s.command(`${verb} <id>`).action(async (id, _o, cmd) => {
|
|
638
|
+
const g = cmd.optsWithGlobals();
|
|
639
|
+
await confirmAction({ message: `${verb[0].toUpperCase() + verb.slice(1)} subscription ${id}?`, yes: g.yes, json: g.json });
|
|
640
|
+
const res = await callThrottle(resolveAuth(g), `/api/v1/subscriptions/${id}/${verb}`, { method: "POST" });
|
|
641
|
+
g.json ? renderObject(res, { json: true }) : success(`Subscription ${id} ${verb}d`);
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
s.command("change-plan <id>").requiredOption("--amount <cents>").requiredOption("--interval <i>").option("--plan-reference <r>").option("--plan-name <n>").option("--effective <e>", "now|period_end", "now").action(async (id, opts, cmd) => {
|
|
645
|
+
const g = cmd.optsWithGlobals();
|
|
646
|
+
await confirmAction({ message: `Change plan on ${id} \u2192 ${opts.amount}/${opts.interval} (${opts.effective})?`, yes: g.yes, json: g.json });
|
|
647
|
+
const body = { amount: Number(opts.amount), interval: opts.interval, effective: opts.effective };
|
|
648
|
+
if (opts.planReference) body.planReference = opts.planReference;
|
|
649
|
+
if (opts.planName) body.planName = opts.planName;
|
|
650
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/subscriptions/${id}/change-plan`, { method: "POST", body: JSON.stringify(body) }), { json: Boolean(g.json) });
|
|
651
|
+
});
|
|
652
|
+
s.command("cancel-pending-change <id>").description("Cancel a scheduled plan change").action(async (id, _o, cmd) => {
|
|
653
|
+
const g = cmd.optsWithGlobals();
|
|
654
|
+
await callThrottle(resolveAuth(g), `/api/v1/subscriptions/${id}/pending-change`, { method: "DELETE" });
|
|
655
|
+
success(`Cleared pending change on ${id}`);
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/commands/discounts.ts
|
|
660
|
+
function register8(program2) {
|
|
661
|
+
const d = program2.command("discounts").description("Discount codes");
|
|
662
|
+
d.command("list").action(async (_o, cmd) => {
|
|
663
|
+
const g = cmd.optsWithGlobals();
|
|
664
|
+
const data = await callThrottle(resolveAuth(g), "/api/v1/discounts");
|
|
665
|
+
renderList(data, [{ header: "ID", get: (x) => x.id }, { header: "CODE", get: (x) => x.code }, { header: "TYPE", get: (x) => x.type }, { header: "VALUE", get: (x) => x.value }, { header: "ACTIVE", get: (x) => x.active }], { json: Boolean(g.json) });
|
|
666
|
+
});
|
|
667
|
+
d.command("create").requiredOption("--json-body <json>", "Discount JSON (code, type, value, \u2026)").action(async (opts, cmd) => {
|
|
668
|
+
const g = cmd.optsWithGlobals();
|
|
669
|
+
renderObject(await callThrottle(resolveAuth(g), "/api/v1/discounts", { method: "POST", body: opts.jsonBody }), { json: Boolean(g.json) });
|
|
670
|
+
});
|
|
671
|
+
d.command("toggle <id>").option("--active", "Enable").option("--inactive", "Disable").action(async (id, opts, cmd) => {
|
|
672
|
+
const g = cmd.optsWithGlobals();
|
|
673
|
+
if (Boolean(opts.active) === Boolean(opts.inactive)) {
|
|
674
|
+
const e = new Error("Pass exactly one of --active or --inactive");
|
|
675
|
+
e.exitCode = 2;
|
|
676
|
+
throw e;
|
|
677
|
+
}
|
|
678
|
+
const active = Boolean(opts.active);
|
|
679
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/discounts/${id}`, { method: "PATCH", body: JSON.stringify({ active }) }), { json: Boolean(g.json) });
|
|
680
|
+
});
|
|
681
|
+
d.command("delete <id>").action(async (id, _o, cmd) => {
|
|
682
|
+
const g = cmd.optsWithGlobals();
|
|
683
|
+
await confirmAction({ message: `Delete discount ${id}?`, yes: g.yes, json: g.json });
|
|
684
|
+
await callThrottle(resolveAuth(g), `/api/v1/discounts/${id}`, { method: "DELETE" });
|
|
685
|
+
success(`Discount ${id} deleted`);
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/commands/webhooks.ts
|
|
690
|
+
function register9(program2) {
|
|
691
|
+
const w = program2.command("webhooks").description("Webhook endpoints + deliveries");
|
|
692
|
+
const ep = w.command("endpoints").description("Manage endpoints");
|
|
693
|
+
ep.command("list").action(async (_o, cmd) => {
|
|
694
|
+
const g = cmd.optsWithGlobals();
|
|
695
|
+
const data = await callThrottle(resolveAuth(g), "/api/v1/webhook-endpoints");
|
|
696
|
+
renderList(data, [{ header: "ID", get: (x) => x.id }, { header: "URL", get: (x) => x.url }, { header: "ACTIVE", get: (x) => x.isActive }, { header: "EVENTS", get: (x) => (x.enabledEvents ?? []).length }], { json: Boolean(g.json) });
|
|
697
|
+
});
|
|
698
|
+
ep.command("create").requiredOption("--url <u>").requiredOption("--events <list>", "Comma-separated event types").action(async (opts, cmd) => {
|
|
699
|
+
const g = cmd.optsWithGlobals();
|
|
700
|
+
const body = { url: opts.url, enabledEvents: String(opts.events).split(",").map((s) => s.trim()) };
|
|
701
|
+
renderObject(await callThrottle(resolveAuth(g), "/api/v1/webhook-endpoints", { method: "POST", body: JSON.stringify(body) }), { json: Boolean(g.json) });
|
|
702
|
+
});
|
|
703
|
+
ep.command("update <id>").option("--url <u>").option("--events <list>").option("--active <bool>").action(async (id, opts, cmd) => {
|
|
704
|
+
const g = cmd.optsWithGlobals();
|
|
705
|
+
const body = {};
|
|
706
|
+
if (opts.url) body.url = opts.url;
|
|
707
|
+
if (opts.events) body.enabledEvents = String(opts.events).split(",").map((s) => s.trim());
|
|
708
|
+
if (opts.active !== void 0) body.isActive = opts.active === "true";
|
|
709
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/webhook-endpoints/${id}`, { method: "PATCH", body: JSON.stringify(body) }), { json: Boolean(g.json) });
|
|
710
|
+
});
|
|
711
|
+
ep.command("delete <id>").action(async (id, _o, cmd) => {
|
|
712
|
+
const g = cmd.optsWithGlobals();
|
|
713
|
+
await confirmAction({ message: `Delete webhook endpoint ${id}?`, yes: g.yes, json: g.json });
|
|
714
|
+
await callThrottle(resolveAuth(g), `/api/v1/webhook-endpoints/${id}`, { method: "DELETE" });
|
|
715
|
+
success(`Endpoint ${id} deleted`);
|
|
716
|
+
});
|
|
717
|
+
ep.command("send-test <id>").action(async (id, _o, cmd) => {
|
|
718
|
+
const g = cmd.optsWithGlobals();
|
|
719
|
+
const r = await callThrottle(resolveAuth(g), `/api/v1/webhook-endpoints/${id}/test`, { method: "POST" });
|
|
720
|
+
g.json ? renderObject(r, { json: true }) : success(`Test event queued to ${id}`);
|
|
721
|
+
});
|
|
722
|
+
ep.command("rotate-secret <id>").action(async (id, _o, cmd) => {
|
|
723
|
+
const g = cmd.optsWithGlobals();
|
|
724
|
+
await confirmAction({ message: `Rotate the signing secret for ${id}? (old secret stops validating)`, yes: g.yes, json: g.json });
|
|
725
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/webhook-endpoints/${id}/rotate-secret`, { method: "POST" }), { json: Boolean(g.json) });
|
|
726
|
+
});
|
|
727
|
+
const del = w.command("deliveries").description("Inspect + replay deliveries");
|
|
728
|
+
del.command("list").option("--endpoint <id>").option("--status <s>").action(async (opts, cmd) => {
|
|
729
|
+
const g = cmd.optsWithGlobals();
|
|
730
|
+
const data = await callThrottle(resolveAuth(g), "/api/v1/webhook-deliveries" + qs({ endpointId: opts.endpoint, status: opts.status }));
|
|
731
|
+
renderList(data, [{ header: "ID", get: (x) => x.id }, { header: "EVENT", get: (x) => x.eventType }, { header: "STATUS", get: (x) => x.status }, { header: "ATTEMPTS", get: (x) => x.attemptCount }], { json: Boolean(g.json) });
|
|
732
|
+
});
|
|
733
|
+
del.command("get <id>").action(async (id, _o, cmd) => {
|
|
734
|
+
const g = cmd.optsWithGlobals();
|
|
735
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/webhook-deliveries/${id}`), { json: Boolean(g.json) });
|
|
736
|
+
});
|
|
737
|
+
del.command("replay <id>").action(async (id, _o, cmd) => {
|
|
738
|
+
const g = cmd.optsWithGlobals();
|
|
739
|
+
await confirmAction({ message: `Replay delivery ${id}?`, yes: g.yes, json: g.json });
|
|
740
|
+
const r = await callThrottle(resolveAuth(g), `/api/v1/webhook-deliveries/${id}/replay`, { method: "POST" });
|
|
741
|
+
g.json ? renderObject(r, { json: true }) : success(`Delivery ${id} replayed`);
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/commands/events.ts
|
|
746
|
+
function register10(program2) {
|
|
747
|
+
const e = program2.command("events").description("Emitted events + catalog");
|
|
748
|
+
e.command("list").option("--type <t>").option("--limit <n>").option("--cursor <c>").action(async (opts, cmd) => {
|
|
749
|
+
const g = cmd.optsWithGlobals();
|
|
750
|
+
const data = await callThrottle(resolveAuth(g), "/api/v1/events" + qs({ type: opts.type, limit: opts.limit, cursor: opts.cursor }));
|
|
751
|
+
renderList(data, [{ header: "ID", get: (x) => x.id }, { header: "TYPE", get: (x) => x.type }, { header: "ENTITY", get: (x) => x.entityId }, { header: "CREATED", get: (x) => x.createdAt }], { json: Boolean(g.json) });
|
|
752
|
+
});
|
|
753
|
+
e.command("get <id>").action(async (id, _o, cmd) => {
|
|
754
|
+
const g = cmd.optsWithGlobals();
|
|
755
|
+
renderObject(await callThrottle(resolveAuth(g), `/api/v1/events/${id}`), { json: Boolean(g.json) });
|
|
756
|
+
});
|
|
757
|
+
e.command("types").description("List the event-type catalog").action(async (_o, cmd) => {
|
|
758
|
+
const g = cmd.optsWithGlobals();
|
|
759
|
+
const data = await callThrottle(resolveAuth(g), "/api/v1/event-types");
|
|
760
|
+
renderList(data, [{ header: "TYPE", get: (x) => typeof x === "string" ? x : x.type }, { header: "SCOPE", get: (x) => typeof x === "string" ? "" : x.readScope }], { json: Boolean(g.json) });
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/commands/embed.ts
|
|
765
|
+
function register11(program2) {
|
|
766
|
+
const e = program2.command("embed").description("Embed allowlist + branding");
|
|
767
|
+
e.command("show").action(async (_o, cmd) => {
|
|
768
|
+
const g = cmd.optsWithGlobals();
|
|
769
|
+
renderObject(await callThrottle(resolveAuth(g), "/api/v1/embed-config"), { json: Boolean(g.json) });
|
|
770
|
+
});
|
|
771
|
+
e.command("set").option("--origins <csv>", "Comma-separated parent origins").option("--primary <hex>", "Primary brand color #rrggbb").option("--logo <url>").option("--merchant-name <name>").action(async (opts, cmd) => {
|
|
772
|
+
const g = cmd.optsWithGlobals();
|
|
773
|
+
const body = {};
|
|
774
|
+
if (opts.origins) body.allowedOrigins = String(opts.origins).split(",").map((s) => s.trim()).filter(Boolean);
|
|
775
|
+
if (opts.primary) body.primaryColor = opts.primary;
|
|
776
|
+
if (opts.logo) body.logoUrl = opts.logo;
|
|
777
|
+
if (opts.merchantName) body.merchantName = opts.merchantName;
|
|
778
|
+
if (Object.keys(body).length === 0) {
|
|
779
|
+
const err = new Error("Pass at least one of --origins, --primary, --logo, --merchant-name");
|
|
780
|
+
err.exitCode = 2;
|
|
781
|
+
throw err;
|
|
782
|
+
}
|
|
783
|
+
renderObject(await callThrottle(resolveAuth(g), "/api/v1/embed-config", { method: "PUT", body: JSON.stringify(body) }), { json: Boolean(g.json) });
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/index.ts
|
|
788
|
+
initCliSentry();
|
|
789
|
+
var program = new Command();
|
|
790
|
+
program.name("throttle").description("Throttle command-line tool").version(CLI_VERSION, "-v, --version", "Print the CLI version").option("--json", "Machine-readable JSON output (implies non-interactive)").option("-q, --quiet", "Suppress the banner").option("--profile <name>", "Config profile to use").option("--api-key <sk_>", "API key (overrides profile / $THROTTLE_API_KEY)").option("--api <url>", "API base URL (overrides profile / $THROTTLE_API_URL)").option("-y, --yes", "Skip confirmation prompts");
|
|
791
|
+
var args = process.argv.slice(2);
|
|
792
|
+
var bannerless = args.includes("--json") || args.includes("-q") || args.includes("--quiet") || args.length === 0 || args.includes("-h") || args.includes("--help") || args.includes("-v") || args.includes("--version");
|
|
793
|
+
if (!bannerless) printBanner({ version: CLI_VERSION });
|
|
794
|
+
for (const reg of [register, register2, register3, register4, register5, register6, register7, register8, register9, register10, register11]) reg(program);
|
|
455
795
|
program.parseAsync().catch(async (err) => {
|
|
796
|
+
const code = handleError(err);
|
|
456
797
|
captureCliException(err);
|
|
457
|
-
console.error(err);
|
|
458
798
|
await flushCliSentry();
|
|
459
|
-
process.exit(
|
|
799
|
+
process.exit(code);
|
|
460
800
|
});
|
|
461
801
|
//# sourceMappingURL=cli.js.map
|