@uniwebpay/cli 0.1.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.
Files changed (2) hide show
  1. package/dist/index.js +1310 -0
  2. package/package.json +51 -0
package/dist/index.js ADDED
@@ -0,0 +1,1310 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/config.ts
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
8
+ import { homedir } from "os";
9
+ import { join } from "path";
10
+ var CONFIG_DIR = join(homedir(), ".uniweb");
11
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
12
+ function getConfig() {
13
+ if (!existsSync(CONFIG_FILE)) return {};
14
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
15
+ }
16
+ function saveConfig(config) {
17
+ if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
18
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
19
+ }
20
+ function getSecret() {
21
+ return process.env.UNIWEB_SECRET || getConfig().secret;
22
+ }
23
+ function setSecret(secret, options) {
24
+ const config = getConfig();
25
+ config.secret = secret;
26
+ config.apiUrl = options?.apiUrl ?? config.apiUrl ?? getApiUrl();
27
+ config.payUrl = options?.payUrl ?? config.payUrl ?? getPayUrl();
28
+ saveConfig(config);
29
+ }
30
+ function getApiUrl() {
31
+ return process.env.UNIWEB_API_URL || getConfig().apiUrl || "https://apiskill.uniwebpay.com";
32
+ }
33
+ function getPayUrl() {
34
+ return process.env.UNIWEB_PAY_URL || getConfig().payUrl || "https://skill.uniwebpay.com";
35
+ }
36
+ var _overrideFormat;
37
+ function setOutputFormat(format) {
38
+ _overrideFormat = format;
39
+ }
40
+ function getOutputFormat() {
41
+ if (_overrideFormat) return _overrideFormat;
42
+ const env = process.env.UNIWEB_OUTPUT;
43
+ if (env === "json" || env === "human") return env;
44
+ return getConfig().outputFormat || "human";
45
+ }
46
+
47
+ // src/api-client.ts
48
+ async function apiRequest(method, path, body) {
49
+ const secret = getSecret();
50
+ if (!secret) {
51
+ console.error("No API key found. Set UNIWEB_SECRET or run `uniweb wallet create`");
52
+ process.exit(1);
53
+ }
54
+ const url = `${getApiUrl()}/v1${path}`;
55
+ const res = await fetch(url, {
56
+ method,
57
+ headers: {
58
+ "Content-Type": "application/json",
59
+ "Authorization": `Bearer ${secret}`
60
+ },
61
+ body: body ? JSON.stringify(body) : void 0
62
+ });
63
+ const data = await res.json();
64
+ if (!res.ok) {
65
+ const msg = data.error?.message || "Unknown error";
66
+ throw new Error(`API Error (${res.status}): ${msg}`);
67
+ }
68
+ return data;
69
+ }
70
+ async function apiRequestNoAuth(method, path, body) {
71
+ const url = `${getApiUrl()}/v1${path}`;
72
+ const res = await fetch(url, {
73
+ method,
74
+ headers: { "Content-Type": "application/json" },
75
+ body: body ? JSON.stringify(body) : void 0
76
+ });
77
+ const data = await res.json();
78
+ if (!res.ok) {
79
+ const msg = data.error?.message || "Unknown error";
80
+ throw new Error(`API Error (${res.status}): ${msg}`);
81
+ }
82
+ return data;
83
+ }
84
+
85
+ // src/utils/output.ts
86
+ function isJsonOutput() {
87
+ return getOutputFormat() === "json";
88
+ }
89
+ function output(data, humanFormatter) {
90
+ if (isJsonOutput()) {
91
+ console.log(JSON.stringify(data, null, 2));
92
+ } else if (humanFormatter) {
93
+ console.log(humanFormatter(data));
94
+ } else {
95
+ console.log(JSON.stringify(data, null, 2));
96
+ }
97
+ }
98
+ function success(message) {
99
+ if (!isJsonOutput()) {
100
+ console.log(`\u2713 ${message}`);
101
+ }
102
+ }
103
+ function info(message) {
104
+ if (!isJsonOutput()) {
105
+ console.log(message);
106
+ }
107
+ }
108
+ function writeInfo(message) {
109
+ if (isJsonOutput()) {
110
+ process.stderr.write(message);
111
+ } else {
112
+ process.stdout.write(message);
113
+ }
114
+ }
115
+ function jsonOnly(data) {
116
+ if (isJsonOutput()) {
117
+ output(data);
118
+ }
119
+ }
120
+ function table(headers, rows) {
121
+ const colWidths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] || "").length)));
122
+ const top = "\u250C" + colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u252C") + "\u2510";
123
+ const bot = "\u2514" + colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u2534") + "\u2518";
124
+ const row = (cells) => "\u2502" + cells.map((c, i) => ` ${(c || "").padEnd(colWidths[i])} `).join("\u2502") + "\u2502";
125
+ console.log(top);
126
+ console.log(row(headers));
127
+ console.log("\u251C" + colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u253C") + "\u2524");
128
+ rows.forEach((r) => console.log(row(r)));
129
+ console.log(bot);
130
+ }
131
+ function fmtAmount(cents, currency) {
132
+ if (cents == null) return "N/A";
133
+ const n = typeof cents === "string" ? parseInt(cents, 10) : cents;
134
+ if (isNaN(n)) return String(cents);
135
+ const dec = (n / 100).toFixed(2);
136
+ return currency ? `${dec} ${currency}` : dec;
137
+ }
138
+ function fmtDate(ts) {
139
+ if (ts == null || ts === "") return "N/A";
140
+ const n = typeof ts === "string" ? parseInt(ts, 10) : ts;
141
+ if (isNaN(n) || n === 0) return "N/A";
142
+ const d = new Date(n);
143
+ const pad = (v) => String(v).padStart(2, "0");
144
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
145
+ }
146
+
147
+ // src/commands/wallet.ts
148
+ function registerWalletCommands(program2) {
149
+ const wallet = program2.command("wallet").description("Manage your wallet");
150
+ wallet.command("create").description("Create a new wallet").action(async () => {
151
+ try {
152
+ const data = await apiRequestNoAuth("POST", "/wallets");
153
+ if (data.secret) {
154
+ setSecret(data.secret, {
155
+ apiUrl: data.apiUrl || getApiUrl(),
156
+ payUrl: data.payUrl
157
+ });
158
+ success("Wallet created! API key saved to ~/.uniweb/config.json");
159
+ }
160
+ output(data, (d) => {
161
+ const w = d.wallet || d;
162
+ return [
163
+ `Wallet ID: ${w.id}`,
164
+ `Status: ${w.status || "active"}`,
165
+ `Currency: ${w.currency || "SGD"}`
166
+ ].join("\n");
167
+ });
168
+ } catch (err) {
169
+ console.error(`Error: ${err.message}`);
170
+ process.exit(1);
171
+ }
172
+ });
173
+ wallet.command("info").description("Get current wallet info").action(async () => {
174
+ try {
175
+ const data = await apiRequest("GET", "/wallets/current");
176
+ output(data, (d) => {
177
+ return [
178
+ `Wallet ID: ${d.id}`,
179
+ `Status: ${d.status || "N/A"}`,
180
+ `Currency: ${d.currency || "N/A"}`,
181
+ `Available: ${fmtAmount(d.availableBalance, d.currency)}`,
182
+ `Pending: ${fmtAmount(d.pendingBalance, d.currency)}`,
183
+ `Withdrawn: ${fmtAmount(d.withdrawnBalance, d.currency)}`,
184
+ `Webhook: ${d.webhookUrl || "not configured"}`,
185
+ `Created: ${fmtDate(d.createdAt)}`
186
+ ].join("\n");
187
+ });
188
+ } catch (err) {
189
+ console.error(`Error: ${err.message}`);
190
+ process.exit(1);
191
+ }
192
+ });
193
+ wallet.command("claim").description("Generate a claim link for your wallet").action(async () => {
194
+ try {
195
+ const data = await apiRequest("POST", "/wallets/current/claim_link");
196
+ output(data, (d) => {
197
+ return [
198
+ `Claim URL: ${d.claimUrl || "N/A"}`,
199
+ `Claim Token: ${d.claimToken || "N/A"}`,
200
+ `Expires At: ${d.expiresAt ? fmtDate(d.expiresAt) : "N/A"}`
201
+ ].join("\n");
202
+ });
203
+ } catch (err) {
204
+ console.error(`Error: ${err.message}`);
205
+ process.exit(1);
206
+ }
207
+ });
208
+ }
209
+
210
+ // src/commands/product.ts
211
+ function registerProductCommands(program2) {
212
+ const product = program2.command("product").description("Manage products");
213
+ product.command("create <name>").description("Create a new product").option("-d, --description <description>", "Product description").option("-w, --webhook-url <url>", "Webhook URL for this product").action(async (name, opts) => {
214
+ try {
215
+ const body = { name };
216
+ if (opts.description) body.description = opts.description;
217
+ if (opts.webhookUrl) body.webhookUrl = opts.webhookUrl;
218
+ const data = await apiRequest("POST", "/products", body);
219
+ success("Product created");
220
+ output(data, (d) => `Product ID: ${d.id}
221
+ Name: ${d.name}${d.webhookUrl ? "\nWebhook: " + d.webhookUrl : ""}`);
222
+ } catch (err) {
223
+ console.error(`Error: ${err.message}`);
224
+ process.exit(1);
225
+ }
226
+ });
227
+ product.command("list").description("List all products").action(async () => {
228
+ try {
229
+ const data = await apiRequest("GET", "/products");
230
+ output(data, (d) => {
231
+ const items = d.data || d.products || d;
232
+ if (!Array.isArray(items) || items.length === 0) return "No products found.";
233
+ table(
234
+ ["ID", "Name", "Active", "Created"],
235
+ items.map((p) => [p.id, p.name, p.active ? "yes" : "no", fmtDate(p.createdAt)])
236
+ );
237
+ return "";
238
+ });
239
+ } catch (err) {
240
+ console.error(`Error: ${err.message}`);
241
+ process.exit(1);
242
+ }
243
+ });
244
+ product.command("get <id>").description("Get a product by ID").action(async (id) => {
245
+ try {
246
+ const data = await apiRequest("GET", `/products/${id}`);
247
+ output(data, (d) => [
248
+ `Product ID: ${d.id}`,
249
+ `Name: ${d.name}`,
250
+ `Description: ${d.description || "N/A"}`,
251
+ `Active: ${d.active ? "yes" : "no"}`,
252
+ `Webhook: ${d.webhookUrl || "N/A (using wallet default)"}`,
253
+ `Created: ${fmtDate(d.createdAt)}`
254
+ ].join("\n"));
255
+ } catch (err) {
256
+ console.error(`Error: ${err.message}`);
257
+ process.exit(1);
258
+ }
259
+ });
260
+ product.command("update <id>").description("Update a product").option("-n, --name <name>", "New product name").option("-d, --description <description>", "New product description").option("-w, --webhook-url <url>", "Webhook URL for this product").action(async (id, opts) => {
261
+ try {
262
+ const body = {};
263
+ if (opts.name) body.name = opts.name;
264
+ if (opts.description) body.description = opts.description;
265
+ if (opts.webhookUrl) body.webhookUrl = opts.webhookUrl;
266
+ const data = await apiRequest("PATCH", `/products/${id}`, body);
267
+ success("Product updated");
268
+ jsonOnly(data ?? { ok: true, id });
269
+ } catch (err) {
270
+ console.error(`Error: ${err.message}`);
271
+ process.exit(1);
272
+ }
273
+ });
274
+ product.command("delete <id>").description("Archive a product").action(async (id) => {
275
+ try {
276
+ const data = await apiRequest("DELETE", `/products/${id}`);
277
+ success(`Product ${id} archived`);
278
+ jsonOnly(data ?? { ok: true, id });
279
+ } catch (err) {
280
+ console.error(`Error: ${err.message}`);
281
+ process.exit(1);
282
+ }
283
+ });
284
+ }
285
+
286
+ // src/commands/price.ts
287
+ function registerPriceCommands(program2) {
288
+ const price = program2.command("price").description("Manage prices");
289
+ price.command("create <productId>").description("Create a new price for a product").requiredOption("-a, --amount <amount>", "Price amount in cents (e.g. 1000 = $10.00)").requiredOption("-t, --type <type>", "Price type (one_time or recurring)").option("-c, --currency <currency>", "Currency code", "SGD").option("-i, --interval <interval>", "Billing interval (month, year) \u2014 required for recurring").action(async (productId, opts) => {
290
+ try {
291
+ const body = {
292
+ productId,
293
+ unitAmount: parseInt(opts.amount, 10),
294
+ type: opts.type,
295
+ currency: opts.currency.toUpperCase()
296
+ };
297
+ if (opts.interval) body.interval = opts.interval;
298
+ const data = await apiRequest("POST", "/prices", body);
299
+ success("Price created");
300
+ output(data, (d) => {
301
+ const payUrl = getPayUrl();
302
+ return [
303
+ `Price ID: ${d.id}`,
304
+ `Amount: ${fmtAmount(d.unitAmount ?? d.amount, d.currency)}${d.interval ? "/" + d.interval : ""}`,
305
+ `Type: ${d.type}`,
306
+ `Payment URL: ${payUrl}/buy/${d.id}`
307
+ ].join("\n");
308
+ });
309
+ } catch (err) {
310
+ console.error(`Error: ${err.message}`);
311
+ process.exit(1);
312
+ }
313
+ });
314
+ price.command("list").description("List all prices").action(async () => {
315
+ try {
316
+ const data = await apiRequest("GET", "/prices");
317
+ output(data, (d) => {
318
+ const items = d.data || d.prices || d;
319
+ if (!Array.isArray(items) || items.length === 0) return "No prices found.";
320
+ table(
321
+ ["ID", "Amount", "Type", "Interval"],
322
+ items.map((p) => [
323
+ p.id,
324
+ fmtAmount(p.unitAmount ?? p.amount, p.currency),
325
+ p.type,
326
+ p.interval || "-"
327
+ ])
328
+ );
329
+ return "";
330
+ });
331
+ } catch (err) {
332
+ console.error(`Error: ${err.message}`);
333
+ process.exit(1);
334
+ }
335
+ });
336
+ }
337
+
338
+ // src/commands/checkout.ts
339
+ function registerCheckoutCommands(program2) {
340
+ const checkout = program2.command("checkout").description("Manage checkout sessions");
341
+ checkout.command("create").description("Create a new checkout session").requiredOption("-p, --price-id <priceId>", "Price ID to checkout").requiredOption("--success-url <url>", "URL to redirect on success").requiredOption("--cancel-url <url>", "URL to redirect on cancel").option("-m, --mode <mode>", "payment or subscription", "payment").option("-q, --quantity <quantity>", "Quantity", "1").option("--methods <methods>", "Payment methods (comma-separated: card,alipay,paynow,wechat)", "card,alipay,paynow,wechat").option("--customer-id <id>", "Customer ID (optional, auto-created from email at payment time)").option("--trial-days <days>", "Trial period in days (subscription mode)").action(async (opts) => {
342
+ try {
343
+ const body = {
344
+ mode: opts.mode,
345
+ lineItems: [{ priceId: opts.priceId, quantity: parseInt(opts.quantity, 10) }],
346
+ successUrl: opts.successUrl,
347
+ cancelUrl: opts.cancelUrl
348
+ };
349
+ if (opts.mode === "subscription") {
350
+ body.paymentMethodTypes = ["card"];
351
+ } else {
352
+ body.paymentMethodTypes = opts.methods.split(",").map((m) => m.trim());
353
+ }
354
+ if (opts.customerId) body.customerId = opts.customerId;
355
+ if (opts.trialDays) body.trialPeriodDays = parseInt(opts.trialDays, 10);
356
+ const data = await apiRequest("POST", "/checkout/sessions", body);
357
+ success("Checkout session created");
358
+ output(data, (d) => {
359
+ const lines = [
360
+ `Session ID: ${d.id}`,
361
+ `Mode: ${d.mode}`,
362
+ `Checkout URL: ${d.url || d.checkoutUrl || "N/A"}`
363
+ ];
364
+ if (d.mode === "subscription") lines.push(`(Subscription \u2014 customer will be created from email at checkout)`);
365
+ return lines.join("\n");
366
+ });
367
+ } catch (err) {
368
+ console.error(`Error: ${err.message}`);
369
+ process.exit(1);
370
+ }
371
+ });
372
+ checkout.command("get <id>").description("Get a checkout session by ID").option("-w, --watch", 'Poll until status changes from "open" (every 5s)').option("-i, --interval <seconds>", "Poll interval in seconds", "5").action(async (id, opts) => {
373
+ try {
374
+ const formatSession = (d) => {
375
+ return [
376
+ `Session ID: ${d.id}`,
377
+ `Status: ${d.status || "N/A"}`,
378
+ `Mode: ${d.mode || "N/A"}`,
379
+ d.amountTotal != null ? `Amount: ${fmtAmount(d.amountTotal, d.currency)}` : null,
380
+ d.paymentId ? `Payment ID: ${d.paymentId}` : null,
381
+ d.url ? `URL: ${d.url}` : null,
382
+ d.successUrl ? `Success: ${d.successUrl}` : null
383
+ ].filter(Boolean).join("\n");
384
+ };
385
+ if (!opts.watch) {
386
+ const data = await apiRequest("GET", `/checkout/sessions/${id}`);
387
+ output(data, formatSession);
388
+ return;
389
+ }
390
+ const intervalMs = parseInt(opts.interval || "5", 10) * 1e3;
391
+ info(`Watching session ${id} (every ${intervalMs / 1e3}s)...
392
+ `);
393
+ while (true) {
394
+ const data = await apiRequest("GET", `/checkout/sessions/${id}`);
395
+ const status = data.status || "unknown";
396
+ const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
397
+ writeInfo(`\r[${ts}] Status: ${status} `);
398
+ if (status !== "open") {
399
+ info("\n");
400
+ output(data, formatSession);
401
+ break;
402
+ }
403
+ await new Promise((r) => setTimeout(r, intervalMs));
404
+ }
405
+ } catch (err) {
406
+ console.error(`Error: ${err.message}`);
407
+ process.exit(1);
408
+ }
409
+ });
410
+ }
411
+
412
+ // src/commands/subscription.ts
413
+ function registerSubscriptionCommands(program2) {
414
+ const subscription = program2.command("subscription").description("Manage subscriptions");
415
+ subscription.command("list").description("List all subscriptions").action(async () => {
416
+ try {
417
+ const data = await apiRequest("GET", "/subscriptions");
418
+ output(data, (d) => {
419
+ const items = d.data || d.subscriptions || d;
420
+ if (!Array.isArray(items) || items.length === 0) return "No subscriptions found.";
421
+ table(
422
+ ["ID", "Status", "Customer", "Price", "Created"],
423
+ items.map((s) => [s.id, s.status, s.customerId || "N/A", s.priceId || "N/A", fmtDate(s.createdAt)])
424
+ );
425
+ return "";
426
+ });
427
+ } catch (err) {
428
+ console.error(`Error: ${err.message}`);
429
+ process.exit(1);
430
+ }
431
+ });
432
+ subscription.command("get <id>").description("Get a subscription by ID").action(async (id) => {
433
+ try {
434
+ const data = await apiRequest("GET", `/subscriptions/${id}`);
435
+ output(data, (d) => [
436
+ `Subscription ID: ${d.id}`,
437
+ `Status: ${d.status}`,
438
+ `Customer: ${d.customerId || "N/A"}`,
439
+ `Price: ${d.priceId || "N/A"}`,
440
+ `Current Period End: ${fmtDate(d.currentPeriodEnd)}`,
441
+ `Created: ${fmtDate(d.createdAt)}`
442
+ ].join("\n"));
443
+ } catch (err) {
444
+ console.error(`Error: ${err.message}`);
445
+ process.exit(1);
446
+ }
447
+ });
448
+ subscription.command("cancel <id>").description("Cancel a subscription immediately").action(async (id) => {
449
+ try {
450
+ const data = await apiRequest("DELETE", `/subscriptions/${id}`);
451
+ success(`Subscription ${id} cancelled`);
452
+ jsonOnly(data ?? { ok: true, id });
453
+ } catch (err) {
454
+ console.error(`Error: ${err.message}`);
455
+ process.exit(1);
456
+ }
457
+ });
458
+ subscription.command("pause <id>").description("Set subscription to cancel at end of current period").action(async (id) => {
459
+ try {
460
+ const data = await apiRequest("PATCH", `/subscriptions/${id}`, { cancelAtPeriodEnd: true });
461
+ success(`Subscription ${id} will cancel at period end`);
462
+ jsonOnly(data ?? { ok: true, id });
463
+ } catch (err) {
464
+ console.error(`Error: ${err.message}`);
465
+ process.exit(1);
466
+ }
467
+ });
468
+ subscription.command("resume <id>").description("Resume a subscription set to cancel at period end").action(async (id) => {
469
+ try {
470
+ const data = await apiRequest("PATCH", `/subscriptions/${id}`, { cancelAtPeriodEnd: false });
471
+ success(`Subscription ${id} resumed`);
472
+ jsonOnly(data ?? { ok: true, id });
473
+ } catch (err) {
474
+ console.error(`Error: ${err.message}`);
475
+ process.exit(1);
476
+ }
477
+ });
478
+ }
479
+
480
+ // src/commands/customer.ts
481
+ function registerCustomerCommands(program2) {
482
+ const customer = program2.command("customer").description("Manage customers");
483
+ customer.command("create").description("Create a new customer").requiredOption("-e, --email <email>", "Customer email").option("-n, --name <name>", "Customer name").action(async (opts) => {
484
+ try {
485
+ const body = { email: opts.email };
486
+ if (opts.name) body.name = opts.name;
487
+ const data = await apiRequest("POST", "/customers", body);
488
+ success("Customer created");
489
+ output(data, (d) => [
490
+ `Customer ID: ${d.id}`,
491
+ `Email: ${d.email}`,
492
+ `Name: ${d.name || "N/A"}`
493
+ ].join("\n"));
494
+ } catch (err) {
495
+ console.error(`Error: ${err.message}`);
496
+ process.exit(1);
497
+ }
498
+ });
499
+ customer.command("list").description("List all customers").action(async () => {
500
+ try {
501
+ const data = await apiRequest("GET", "/customers");
502
+ output(data, (d) => {
503
+ const items = d.data || d.customers || d;
504
+ if (!Array.isArray(items) || items.length === 0) return "No customers found.";
505
+ table(
506
+ ["ID", "Email", "Name", "Created"],
507
+ items.map((c) => [c.id, c.email, c.name || "", fmtDate(c.createdAt)])
508
+ );
509
+ return "";
510
+ });
511
+ } catch (err) {
512
+ console.error(`Error: ${err.message}`);
513
+ process.exit(1);
514
+ }
515
+ });
516
+ customer.command("get <id>").description("Get a customer by ID").action(async (id) => {
517
+ try {
518
+ const data = await apiRequest("GET", `/customers/${id}`);
519
+ output(data, (d) => [
520
+ `Customer ID: ${d.id}`,
521
+ `Email: ${d.email}`,
522
+ `Name: ${d.name || "N/A"}`,
523
+ `Created: ${fmtDate(d.createdAt)}`
524
+ ].join("\n"));
525
+ } catch (err) {
526
+ console.error(`Error: ${err.message}`);
527
+ process.exit(1);
528
+ }
529
+ });
530
+ customer.command("delete <id>").description("Delete a customer").action(async (id) => {
531
+ try {
532
+ const data = await apiRequest("DELETE", `/customers/${id}`);
533
+ success(`Customer ${id} deleted`);
534
+ jsonOnly(data ?? { ok: true, id });
535
+ } catch (err) {
536
+ console.error(`Error: ${err.message}`);
537
+ process.exit(1);
538
+ }
539
+ });
540
+ customer.command("update <id>").description("Update a customer").option("-e, --email <email>", "New email").option("-n, --name <name>", "New name").action(async (id, opts) => {
541
+ try {
542
+ const body = {};
543
+ if (opts.email) body.email = opts.email;
544
+ if (opts.name) body.name = opts.name;
545
+ if (Object.keys(body).length === 0) {
546
+ console.error("Provide at least --email or --name to update.");
547
+ process.exit(1);
548
+ }
549
+ const data = await apiRequest("PATCH", `/customers/${id}`, body);
550
+ success("Customer updated");
551
+ output(data, (d) => [
552
+ `Customer ID: ${d.id}`,
553
+ `Email: ${d.email}`,
554
+ `Name: ${d.name || "N/A"}`
555
+ ].join("\n"));
556
+ } catch (err) {
557
+ console.error(`Error: ${err.message}`);
558
+ process.exit(1);
559
+ }
560
+ });
561
+ }
562
+
563
+ // src/commands/link.ts
564
+ function registerLinkCommands(program2) {
565
+ const link = program2.command("link").description("Manage payment links");
566
+ link.command("create <amount>").description("Create a new payment link (amount in cents, e.g. 1000 = $10.00)").option("-c, --currency <currency>", "Currency code", "SGD").option("-n, --name <name>", "Link name").option("-d, --description <description>", "Link description").option("--success-url <url>", "URL to redirect after successful payment").option("--cancel-url <url>", "URL to redirect on cancel").option("--methods <methods>", "Payment methods (comma-separated: card,alipay,paynow,wechat)").option("-w, --webhook-url <url>", "Webhook URL for this payment link").action(async (amount, opts) => {
567
+ try {
568
+ const body = {
569
+ amount: parseInt(amount, 10),
570
+ currency: opts.currency
571
+ };
572
+ if (opts.name) body.name = opts.name;
573
+ if (opts.description) body.description = opts.description;
574
+ if (opts.successUrl) body.successUrl = opts.successUrl;
575
+ if (opts.cancelUrl) body.cancelUrl = opts.cancelUrl;
576
+ if (opts.methods) body.paymentMethodTypes = opts.methods.split(",").map((m) => m.trim());
577
+ if (opts.webhookUrl) body.webhookUrl = opts.webhookUrl;
578
+ const data = await apiRequest("POST", "/payment_links", body);
579
+ success("Payment link created");
580
+ output(data, (d) => {
581
+ return [
582
+ `Link ID: ${d.id}`,
583
+ `URL: ${d.url || "N/A"}`,
584
+ `Amount: ${fmtAmount(d.amount, d.currency)}`,
585
+ d.description ? `Desc: ${d.description}` : null
586
+ ].filter(Boolean).join("\n");
587
+ });
588
+ } catch (err) {
589
+ console.error(`Error: ${err.message}`);
590
+ process.exit(1);
591
+ }
592
+ });
593
+ link.command("list").description("List all payment links").action(async () => {
594
+ try {
595
+ const data = await apiRequest("GET", "/payment_links");
596
+ output(data, (d) => {
597
+ const items = d.data || d.paymentLinks || d;
598
+ if (!Array.isArray(items) || items.length === 0) return "No payment links found.";
599
+ table(
600
+ ["ID", "Amount", "Active", "Description", "Created"],
601
+ items.map((l) => [
602
+ l.id,
603
+ fmtAmount(l.amount, l.currency),
604
+ l.active ? "yes" : "no",
605
+ l.description || "",
606
+ fmtDate(l.createdAt)
607
+ ])
608
+ );
609
+ return "";
610
+ });
611
+ } catch (err) {
612
+ console.error(`Error: ${err.message}`);
613
+ process.exit(1);
614
+ }
615
+ });
616
+ link.command("get <id>").description("Get a payment link by ID").action(async (id) => {
617
+ try {
618
+ const data = await apiRequest("GET", `/payment_links/${id}`);
619
+ output(data, (d) => {
620
+ return [
621
+ `Link ID: ${d.id}`,
622
+ `URL: ${d.url || "N/A"}`,
623
+ `Amount: ${fmtAmount(d.amount, d.currency)}`,
624
+ `Active: ${d.active ? "yes" : "no"}`,
625
+ `Desc: ${d.description || "N/A"}`,
626
+ `Webhook: ${d.webhookUrl || "N/A (using wallet default)"}`,
627
+ `Created: ${fmtDate(d.createdAt)}`
628
+ ].join("\n");
629
+ });
630
+ } catch (err) {
631
+ console.error(`Error: ${err.message}`);
632
+ process.exit(1);
633
+ }
634
+ });
635
+ link.command("update <id>").description("Update a payment link").option("-n, --name <name>", "New link name").option("-d, --description <description>", "New description").option("--success-url <url>", "Success redirect URL").option("--cancel-url <url>", "Cancel redirect URL").option("-w, --webhook-url <url>", "Webhook URL").action(async (id, opts) => {
636
+ try {
637
+ const body = {};
638
+ if (opts.name) body.name = opts.name;
639
+ if (opts.description) body.description = opts.description;
640
+ if (opts.successUrl) body.successUrl = opts.successUrl;
641
+ if (opts.cancelUrl) body.cancelUrl = opts.cancelUrl;
642
+ if (opts.webhookUrl) body.webhookUrl = opts.webhookUrl;
643
+ const data = await apiRequest("PATCH", `/payment_links/${id}`, body);
644
+ success("Payment link updated");
645
+ jsonOnly(data ?? { ok: true, id });
646
+ } catch (err) {
647
+ console.error(`Error: ${err.message}`);
648
+ process.exit(1);
649
+ }
650
+ });
651
+ link.command("deactivate <id>").description("Deactivate a payment link").action(async (id) => {
652
+ try {
653
+ const data = await apiRequest("PATCH", `/payment_links/${id}`, { active: false });
654
+ success(`Payment link ${id} deactivated`);
655
+ jsonOnly(data ?? { ok: true, id });
656
+ } catch (err) {
657
+ console.error(`Error: ${err.message}`);
658
+ process.exit(1);
659
+ }
660
+ });
661
+ link.command("activate <id>").description("Activate a payment link").action(async (id) => {
662
+ try {
663
+ const data = await apiRequest("PATCH", `/payment_links/${id}`, { active: true });
664
+ success(`Payment link ${id} activated`);
665
+ jsonOnly(data ?? { ok: true, id });
666
+ } catch (err) {
667
+ console.error(`Error: ${err.message}`);
668
+ process.exit(1);
669
+ }
670
+ });
671
+ }
672
+
673
+ // src/commands/create.ts
674
+ import { createInterface } from "readline";
675
+ function prompt(question) {
676
+ const rl = createInterface({ input: process.stdin, output: isJsonOutput() ? process.stderr : process.stdout });
677
+ return new Promise((resolve) => {
678
+ rl.question(question, (answer) => {
679
+ rl.close();
680
+ resolve(answer.trim());
681
+ });
682
+ });
683
+ }
684
+ function registerCreateCommand(program2) {
685
+ program2.command("create").description("Quick create: product + price + checkout session in one go").action(async () => {
686
+ try {
687
+ const productName = await prompt("Product name: ");
688
+ if (!productName) {
689
+ console.error("Product name is required.");
690
+ process.exit(1);
691
+ }
692
+ const amountStr = await prompt("Amount (in smallest currency unit, e.g. 1000 for $10.00): ");
693
+ const unitAmount = parseInt(amountStr, 10);
694
+ if (isNaN(unitAmount) || unitAmount <= 0) {
695
+ console.error("Invalid amount.");
696
+ process.exit(1);
697
+ }
698
+ const currency = await prompt("Currency (default: SGD): ") || "SGD";
699
+ const type = await prompt("Type (one_time or recurring): ");
700
+ if (type !== "one_time" && type !== "recurring") {
701
+ console.error('Type must be "one_time" or "recurring".');
702
+ process.exit(1);
703
+ }
704
+ let interval;
705
+ if (type === "recurring") {
706
+ interval = await prompt("Billing interval (month or year): ");
707
+ if (interval !== "month" && interval !== "year") {
708
+ console.error('Interval must be "month" or "year".');
709
+ process.exit(1);
710
+ }
711
+ }
712
+ const successUrl = await prompt("Success URL (default: https://example.com/success): ") || "https://example.com/success";
713
+ const cancelUrl = await prompt("Cancel URL (default: https://example.com/cancel): ") || "https://example.com/cancel";
714
+ info("\nCreating product...");
715
+ const product = await apiRequest("POST", "/products", { name: productName });
716
+ success(`Product created: ${product.id}`);
717
+ info("Creating price...");
718
+ const priceBody = {
719
+ productId: product.id,
720
+ unitAmount,
721
+ type,
722
+ currency
723
+ };
724
+ if (interval) priceBody.interval = interval;
725
+ const price = await apiRequest("POST", "/prices", priceBody);
726
+ success(`Price created: ${price.id}`);
727
+ info("Creating checkout session...");
728
+ const mode = type === "recurring" ? "subscription" : "payment";
729
+ const session = await apiRequest("POST", "/checkout/sessions", {
730
+ mode,
731
+ lineItems: [{ priceId: price.id, quantity: 1 }],
732
+ successUrl,
733
+ cancelUrl
734
+ });
735
+ success(`Checkout session created: ${session.id}`);
736
+ const checkoutUrl = session.url || session.checkoutUrl;
737
+ if (checkoutUrl) {
738
+ info(`
739
+ Checkout URL: ${checkoutUrl}`);
740
+ }
741
+ output({
742
+ product: { id: product.id, name: productName },
743
+ price: { id: price.id, unitAmount, currency, type, interval },
744
+ checkout: { id: session.id, url: checkoutUrl }
745
+ });
746
+ } catch (err) {
747
+ console.error(`Error: ${err.message}`);
748
+ process.exit(1);
749
+ }
750
+ });
751
+ }
752
+
753
+ // src/commands/payout.ts
754
+ function registerPayoutCommands(program2) {
755
+ const payout = program2.command("payout").description("Manage payouts");
756
+ payout.command("list").description("List all payouts").action(async () => {
757
+ try {
758
+ const data = await apiRequest("GET", "/payouts");
759
+ output(data, (d) => {
760
+ const items = d.data || d.payouts || d;
761
+ if (!Array.isArray(items) || items.length === 0) return "No payouts found.";
762
+ table(
763
+ ["ID", "Amount", "Status", "Created"],
764
+ items.map((p) => [p.id, fmtAmount(p.amount, p.currency), p.status, fmtDate(p.createdAt)])
765
+ );
766
+ return "";
767
+ });
768
+ } catch (err) {
769
+ console.error(`Error: ${err.message}`);
770
+ process.exit(1);
771
+ }
772
+ });
773
+ payout.command("create").description("Create a new payout").requiredOption("-a, --amount <amount>", "Payout amount in cents (e.g. 1000 = $10.00)").requiredOption("-b, --bank-account-id <bankAccountId>", "Bank account ID").action(async (opts) => {
774
+ try {
775
+ const body = {
776
+ amount: parseInt(opts.amount, 10),
777
+ bankAccountId: opts.bankAccountId
778
+ };
779
+ const data = await apiRequest("POST", "/payouts", body);
780
+ success("Payout created");
781
+ output(data, (d) => [
782
+ `Payout ID: ${d.id}`,
783
+ `Amount: ${fmtAmount(d.amount, d.currency)}`,
784
+ `Status: ${d.status}`
785
+ ].join("\n"));
786
+ } catch (err) {
787
+ console.error(`Error: ${err.message}`);
788
+ process.exit(1);
789
+ }
790
+ });
791
+ payout.command("cancel <id>").description("Cancel a payout").action(async (id) => {
792
+ try {
793
+ const data = await apiRequest("POST", `/payouts/${id}/cancel`);
794
+ success(`Payout ${id} cancelled`);
795
+ jsonOnly(data ?? { ok: true, id });
796
+ } catch (err) {
797
+ console.error(`Error: ${err.message}`);
798
+ process.exit(1);
799
+ }
800
+ });
801
+ payout.command("status").description("Check payout eligibility").action(async () => {
802
+ try {
803
+ const data = await apiRequest("GET", "/payouts/eligibility");
804
+ output(data, (d) => [
805
+ `Eligible: ${d.eligible ?? "N/A"}`,
806
+ `Available Balance: ${fmtAmount(d.availableBalance)}`,
807
+ `Minimum Payout: ${fmtAmount(d.minimumPayout)}`
808
+ ].join("\n"));
809
+ } catch (err) {
810
+ console.error(`Error: ${err.message}`);
811
+ process.exit(1);
812
+ }
813
+ });
814
+ }
815
+
816
+ // src/commands/bank.ts
817
+ import { createInterface as createInterface2 } from "readline";
818
+ function prompt2(question) {
819
+ const rl = createInterface2({ input: process.stdin, output: isJsonOutput() ? process.stderr : process.stdout });
820
+ return new Promise((resolve) => {
821
+ rl.question(question, (answer) => {
822
+ rl.close();
823
+ resolve(answer.trim());
824
+ });
825
+ });
826
+ }
827
+ function registerBankCommands(program2) {
828
+ const bank = program2.command("bank").description("Manage bank accounts");
829
+ bank.command("list").description("List all bank accounts").action(async () => {
830
+ try {
831
+ const data = await apiRequest("GET", "/bank_accounts");
832
+ output(data, (d) => {
833
+ const items = d.data || d.bankAccounts || d;
834
+ if (!Array.isArray(items) || items.length === 0) return "No bank accounts found.";
835
+ table(
836
+ ["ID", "Bank Name", "Last 4", "Default", "Created"],
837
+ items.map((b) => [
838
+ b.id,
839
+ b.bankName || "N/A",
840
+ b.last4 || "N/A",
841
+ b.isDefault ? "yes" : "no",
842
+ fmtDate(b.createdAt)
843
+ ])
844
+ );
845
+ return "";
846
+ });
847
+ } catch (err) {
848
+ console.error(`Error: ${err.message}`);
849
+ process.exit(1);
850
+ }
851
+ });
852
+ bank.command("add").description("Add a new bank account").action(async () => {
853
+ try {
854
+ const accountName = await prompt2("Account holder name: ");
855
+ const bankName = await prompt2("Bank name: ");
856
+ const bankCode = await prompt2("Bank code / routing number (press Enter to skip): ");
857
+ const accountNumber = await prompt2("Account number: ");
858
+ const currency = await prompt2("Currency (default: SGD): ") || "SGD";
859
+ const body = {
860
+ accountName,
861
+ bankName,
862
+ accountNumber,
863
+ currency
864
+ };
865
+ if (bankCode) body.bankCode = bankCode;
866
+ const data = await apiRequest("POST", "/bank_accounts", body);
867
+ success("Bank account added");
868
+ output(data, (d) => [
869
+ `Bank Account ID: ${d.id}`,
870
+ `Bank: ${d.bankName || bankName}`,
871
+ `Last 4: ${d.last4 || "N/A"}`
872
+ ].join("\n"));
873
+ } catch (err) {
874
+ console.error(`Error: ${err.message}`);
875
+ process.exit(1);
876
+ }
877
+ });
878
+ bank.command("remove <id>").description("Remove a bank account").action(async (id) => {
879
+ try {
880
+ const data = await apiRequest("DELETE", `/bank_accounts/${id}`);
881
+ success(`Bank account ${id} removed`);
882
+ jsonOnly(data ?? { ok: true, id });
883
+ } catch (err) {
884
+ console.error(`Error: ${err.message}`);
885
+ process.exit(1);
886
+ }
887
+ });
888
+ bank.command("set-default <id>").description("Set a bank account as default").action(async (id) => {
889
+ try {
890
+ const data = await apiRequest("PATCH", `/bank_accounts/${id}`, { isDefault: true });
891
+ success(`Bank account ${id} set as default`);
892
+ jsonOnly(data ?? { ok: true, id });
893
+ } catch (err) {
894
+ console.error(`Error: ${err.message}`);
895
+ process.exit(1);
896
+ }
897
+ });
898
+ }
899
+
900
+ // src/commands/kyc.ts
901
+ import { createInterface as createInterface3 } from "readline";
902
+ function prompt3(question) {
903
+ const rl = createInterface3({ input: process.stdin, output: isJsonOutput() ? process.stderr : process.stdout });
904
+ return new Promise((resolve) => {
905
+ rl.question(question, (answer) => {
906
+ rl.close();
907
+ resolve(answer.trim());
908
+ });
909
+ });
910
+ }
911
+ function registerKycCommands(program2) {
912
+ const kyc = program2.command("kyc").description("Manage KYC verification");
913
+ kyc.command("submit").description("Submit KYC verification details").action(async () => {
914
+ try {
915
+ const fullName = await prompt3("Full name: ");
916
+ const dateOfBirth = await prompt3("Date of birth (YYYY-MM-DD): ");
917
+ const nationality = await prompt3("Nationality (2-letter code, e.g. SG): ");
918
+ const documentType = await prompt3("Document type (passport / national_id / drivers_license): ");
919
+ const documentNumber = await prompt3("Document number: ");
920
+ const documentFrontUrl = await prompt3("Document front image URL: ");
921
+ const documentBackUrl = await prompt3("Document back image URL (press Enter to skip): ");
922
+ const body = {
923
+ fullName,
924
+ dateOfBirth,
925
+ nationality,
926
+ documentType,
927
+ documentNumber,
928
+ documentFrontUrl
929
+ };
930
+ if (documentBackUrl) body.documentBackUrl = documentBackUrl;
931
+ const data = await apiRequest("POST", "/kyc", body);
932
+ success("KYC submission received");
933
+ output(data, (d) => {
934
+ return [
935
+ `KYC ID: ${d.id || "N/A"}`,
936
+ `Status: ${d.status || "pending"}`
937
+ ].join("\n");
938
+ });
939
+ } catch (err) {
940
+ console.error(`Error: ${err.message}`);
941
+ process.exit(1);
942
+ }
943
+ });
944
+ kyc.command("status").description("Check KYC verification status").action(async () => {
945
+ try {
946
+ const data = await apiRequest("GET", "/kyc/status");
947
+ output(data, (d) => {
948
+ return [
949
+ `Account ID: ${d.accountId || "N/A"}`,
950
+ `KYC Status: ${d.kycStatus || "N/A"}`
951
+ ].join("\n");
952
+ });
953
+ } catch (err) {
954
+ console.error(`Error: ${err.message}`);
955
+ process.exit(1);
956
+ }
957
+ });
958
+ }
959
+
960
+ // src/commands/webhook.ts
961
+ var EVENT_DESCRIPTIONS = {
962
+ "payment.succeeded": "Customer payment completed successfully",
963
+ "payment.failed": "Customer payment failed",
964
+ "refund.succeeded": "Refund completed at the gateway",
965
+ "refund.failed": "Refund attempt rejected by the gateway",
966
+ "checkout.session.completed": "Checkout session marked complete (paymentId set)",
967
+ "checkout.session.expired": "Checkout session expired before payment",
968
+ "subscription.created": "New subscription created (or trial started)",
969
+ "subscription.renewed": "Subscription auto-renewed",
970
+ "subscription.past_due": "Subscription payment overdue; retries in progress",
971
+ "subscription.unpaid": "Subscription marked unpaid; retries exhausted",
972
+ "subscription.canceled": "Subscription cancelled (immediate or end-of-period)",
973
+ "subscription.trial_ending": "Trial period ending soon",
974
+ "payout.requested": "Payout request submitted by merchant",
975
+ "payout.approved": "Payout approved by admin review",
976
+ "payout.settled": "Payout settled to bank account",
977
+ "payout.rejected": "Payout rejected (KYC, AML, balance, etc.)",
978
+ "payout.failed": "Payout failed at bank rail after settlement attempt",
979
+ "account.kyc.approved": "Merchant account KYC approved",
980
+ "account.kyc.rejected": "Merchant account KYC rejected"
981
+ };
982
+ var EVENT_GROUPS = [
983
+ { title: "Payment Events", prefix: "payment." },
984
+ { title: "Refund Events", prefix: "refund." },
985
+ { title: "Checkout Events", prefix: "checkout." },
986
+ { title: "Subscription Events", prefix: "subscription." },
987
+ { title: "Payout Events", prefix: "payout." },
988
+ { title: "Account / KYC Events", prefix: "account." }
989
+ ];
990
+ function registerWebhookCommands(program2) {
991
+ const webhook = program2.command("webhook").description("Manage webhook configuration");
992
+ webhook.command("set <url>").description("Set webhook URL for your wallet (must be HTTPS)").action(async (url) => {
993
+ try {
994
+ if (!url.startsWith("https://")) {
995
+ console.error("Error: webhook URL must start with https://");
996
+ process.exit(1);
997
+ }
998
+ const data = await apiRequest("PATCH", "/wallets/current", {
999
+ webhookUrl: url
1000
+ });
1001
+ success("Webhook configured");
1002
+ output(data, (d) => [
1003
+ ` URL: ${d.webhookUrl}`,
1004
+ d.webhookSecret ? ` Secret: ${d.webhookSecret}` : null,
1005
+ d.webhookSecret ? "\nSave the secret \u2014 it will not be shown again." : null,
1006
+ d.webhookSecret ? "Use client.webhooks.constructEvent(body, signature, secret) in your server." : null
1007
+ ].filter(Boolean).join("\n"));
1008
+ } catch (err) {
1009
+ console.error(`Error: ${err.message}`);
1010
+ process.exit(1);
1011
+ }
1012
+ });
1013
+ webhook.command("info").description("Show current webhook configuration").action(async () => {
1014
+ try {
1015
+ const data = await apiRequest("GET", "/wallets/current");
1016
+ output(data, (d) => d.webhookUrl ? `Webhook URL: ${d.webhookUrl}
1017
+ Status: configured` : "No webhook configured.\nRun: uniweb webhook set <url>");
1018
+ } catch (err) {
1019
+ console.error(`Error: ${err.message}`);
1020
+ process.exit(1);
1021
+ }
1022
+ });
1023
+ webhook.command("remove").description("Remove webhook configuration").action(async () => {
1024
+ try {
1025
+ const data = await apiRequest("PATCH", "/wallets/current", {
1026
+ webhookUrl: null
1027
+ });
1028
+ success("Webhook removed");
1029
+ jsonOnly(data ?? { ok: true, webhookUrl: null });
1030
+ } catch (err) {
1031
+ console.error(`Error: ${err.message}`);
1032
+ process.exit(1);
1033
+ }
1034
+ });
1035
+ webhook.command("roll-secret").description("Generate a new webhook signing secret (invalidates the old one)").action(async () => {
1036
+ try {
1037
+ const wallet = await apiRequest("GET", "/wallets/current");
1038
+ if (!wallet.webhookUrl) {
1039
+ console.error("No webhook configured. Run: uniweb webhook set <url>");
1040
+ process.exit(1);
1041
+ }
1042
+ const data = await apiRequest("PATCH", "/wallets/current", {
1043
+ webhookUrl: wallet.webhookUrl
1044
+ });
1045
+ success("New secret generated");
1046
+ output(data, (d) => [
1047
+ ` URL: ${d.webhookUrl}`,
1048
+ d.webhookSecret ? ` Secret: ${d.webhookSecret}` : null,
1049
+ d.webhookSecret ? "\nSave the new secret \u2014 it will not be shown again." : null,
1050
+ d.webhookSecret ? "The old secret is now invalid." : null
1051
+ ].filter(Boolean).join("\n"));
1052
+ } catch (err) {
1053
+ console.error(`Error: ${err.message}`);
1054
+ process.exit(1);
1055
+ }
1056
+ });
1057
+ webhook.command("events").description("Webhook event types reference").action(() => {
1058
+ const events = Object.keys(EVENT_DESCRIPTIONS);
1059
+ const grouped = EVENT_GROUPS.map((g) => ({
1060
+ title: g.title,
1061
+ events: events.filter((e) => e.startsWith(g.prefix))
1062
+ })).filter((g) => g.events.length > 0);
1063
+ const reference = {
1064
+ events: grouped.reduce((acc, g) => {
1065
+ acc[g.title] = Object.fromEntries(g.events.map((e) => [e, EVENT_DESCRIPTIONS[e]]));
1066
+ return acc;
1067
+ }, {}),
1068
+ payload: {
1069
+ id: "evt_xxx",
1070
+ type: "payment.succeeded",
1071
+ created: 171e7,
1072
+ data: { object: "{ ...full resource... }" }
1073
+ },
1074
+ signatureHeader: "uniweb-Signature: t={timestamp},v1={hmac-sha256-hex}"
1075
+ };
1076
+ const padTo = Math.max(...events.map((e) => e.length));
1077
+ output(reference, () => {
1078
+ const lines = ["uniweb Webhook Event Types", ""];
1079
+ for (const group of grouped) {
1080
+ lines.push(`${group.title}:`);
1081
+ for (const event of group.events) {
1082
+ lines.push(` ${event.padEnd(padTo)} \u2014 ${EVENT_DESCRIPTIONS[event]}`);
1083
+ }
1084
+ lines.push("");
1085
+ }
1086
+ lines.push(
1087
+ "Payload Format:",
1088
+ " {",
1089
+ ' "id": "evt_xxx",',
1090
+ ' "type": "payment.succeeded",',
1091
+ ' "created": 1710000000,',
1092
+ ' "data": { "object": { ...full resource... } }',
1093
+ " }",
1094
+ "",
1095
+ "Signature Header:",
1096
+ " uniweb-Signature: t={timestamp},v1={hmac-sha256-hex}"
1097
+ );
1098
+ return lines.join("\n");
1099
+ });
1100
+ });
1101
+ }
1102
+
1103
+ // src/commands/key.ts
1104
+ function registerKeyCommands(program2) {
1105
+ const key = program2.command("key").description("Manage API keys");
1106
+ key.command("create").description("Create a new API key").requiredOption("-t, --type <type>", "Key type: full or server").option("-n, --name <name>", 'Key name (e.g. "Production Backend")').option("-s, --scopes <scopes>", "Comma-separated scopes for server key (e.g. products.read,products.write,checkout.create)").action(async (opts) => {
1107
+ try {
1108
+ if (opts.type !== "full" && opts.type !== "server") {
1109
+ console.error('Error: type must be "full" or "server"');
1110
+ process.exit(1);
1111
+ }
1112
+ const body = { type: opts.type };
1113
+ if (opts.name) body.name = opts.name;
1114
+ if (opts.scopes && opts.type === "server") {
1115
+ body.scopes = opts.scopes.split(",").map((s) => s.trim());
1116
+ }
1117
+ const data = await apiRequest("POST", "/keys", body);
1118
+ success(`${opts.type === "server" ? "Server" : "Full"} key created`);
1119
+ output(data, (d) => [
1120
+ `Key ID: ${d.id}`,
1121
+ `Secret: ${d.secret}`,
1122
+ `Type: ${d.type}`,
1123
+ d.name ? `Name: ${d.name}` : null,
1124
+ `Scopes: ${Array.isArray(d.scopes) ? d.scopes.join(", ") : d.scopes}`,
1125
+ "",
1126
+ "Save this secret \u2014 it will not be shown again."
1127
+ ].filter(Boolean).join("\n"));
1128
+ } catch (err) {
1129
+ console.error(`Error: ${err.message}`);
1130
+ process.exit(1);
1131
+ }
1132
+ });
1133
+ key.command("list").description("List all API keys").action(async () => {
1134
+ try {
1135
+ const data = await apiRequest("GET", "/keys");
1136
+ output(data, (d) => {
1137
+ const items = d.data || [];
1138
+ if (!Array.isArray(items) || items.length === 0) return "No API keys found.";
1139
+ table(
1140
+ ["ID", "Type", "Name", "Last Used", "Active"],
1141
+ items.map((k) => [
1142
+ k.id,
1143
+ k.type,
1144
+ k.name || "-",
1145
+ k.lastUsedAt ? fmtDate(k.lastUsedAt) : "Never",
1146
+ k.active ? "yes" : "no"
1147
+ ])
1148
+ );
1149
+ return "";
1150
+ });
1151
+ } catch (err) {
1152
+ console.error(`Error: ${err.message}`);
1153
+ process.exit(1);
1154
+ }
1155
+ });
1156
+ key.command("revoke <id>").description("Revoke an API key").action(async (id) => {
1157
+ try {
1158
+ const data = await apiRequest("DELETE", `/keys/${id}`);
1159
+ success(`Key ${id} revoked`);
1160
+ jsonOnly(data ?? { ok: true, id });
1161
+ } catch (err) {
1162
+ console.error(`Error: ${err.message}`);
1163
+ process.exit(1);
1164
+ }
1165
+ });
1166
+ key.command("roll <id>").description("Rotate an API key (new secret, old one stops working)").action(async (id) => {
1167
+ try {
1168
+ const data = await apiRequest("POST", `/keys/${id}/roll`);
1169
+ success("Key rotated");
1170
+ output(data, (d) => [
1171
+ `Key ID: ${d.id}`,
1172
+ `New Secret: ${d.secret}`,
1173
+ "",
1174
+ "Save this secret \u2014 it will not be shown again.",
1175
+ "The old secret has been invalidated."
1176
+ ].join("\n"));
1177
+ } catch (err) {
1178
+ console.error(`Error: ${err.message}`);
1179
+ process.exit(1);
1180
+ }
1181
+ });
1182
+ }
1183
+
1184
+ // src/commands/payment.ts
1185
+ function registerPaymentCommands(program2) {
1186
+ const payment = program2.command("payment").description("Manage payments");
1187
+ payment.command("list").description("List payments").option("-s, --status <status>", "Filter by status (pending, authorized, succeeded, failed, refunded, partially_refunded, abandoned)").option("-c, --customer-id <id>", "Filter by customer ID").action(async (opts) => {
1188
+ try {
1189
+ const params = new URLSearchParams();
1190
+ if (opts.status) params.set("status", opts.status);
1191
+ if (opts.customerId) params.set("customer_id", opts.customerId);
1192
+ const qs = params.toString();
1193
+ const data = await apiRequest("GET", `/payments${qs ? "?" + qs : ""}`);
1194
+ output(data, (d) => {
1195
+ const items = d.data || [];
1196
+ if (!Array.isArray(items) || items.length === 0) return "No payments found.";
1197
+ table(
1198
+ ["ID", "Amount", "Status", "Gateway", "Created"],
1199
+ items.map((p) => [
1200
+ p.id,
1201
+ fmtAmount(p.amount, p.currency),
1202
+ p.status,
1203
+ p.gateway,
1204
+ fmtDate(p.createdAt)
1205
+ ])
1206
+ );
1207
+ return "";
1208
+ });
1209
+ } catch (err) {
1210
+ console.error(`Error: ${err.message}`);
1211
+ process.exit(1);
1212
+ }
1213
+ });
1214
+ payment.command("get <id>").description("Get payment details").option("--gateway", "Query gateway enquiry details").action(async (id, opts) => {
1215
+ try {
1216
+ const data = await apiRequest("GET", `/payments/${id}${opts.gateway ? "?gateway=true" : ""}`);
1217
+ output(data, (d) => [
1218
+ `Payment ID: ${d.id}`,
1219
+ `Amount: ${fmtAmount(d.amount, d.currency)}`,
1220
+ `Status: ${d.status}`,
1221
+ `Gateway: ${d.gateway}`,
1222
+ d.gatewayStatus ? `Gateway Status: ${d.gatewayStatus}` : null,
1223
+ d.gatewayStatusCode ? `Gateway Status Code: ${d.gatewayStatusCode}` : null,
1224
+ d.gatewayStatusMessage ? `Gateway Message: ${d.gatewayStatusMessage}` : null,
1225
+ `Customer: ${d.customerId || "N/A"}`,
1226
+ `Created: ${fmtDate(d.createdAt)}`
1227
+ ].filter(Boolean).join("\n"));
1228
+ } catch (err) {
1229
+ console.error(`Error: ${err.message}`);
1230
+ process.exit(1);
1231
+ }
1232
+ });
1233
+ payment.command("void <id>").description("Void a payment (same-day, before settlement)").action(async (id) => {
1234
+ try {
1235
+ const data = await apiRequest("POST", `/payments/${id}/void`);
1236
+ success(`Payment ${id} voided`);
1237
+ jsonOnly(data ?? { ok: true, id });
1238
+ } catch (err) {
1239
+ console.error(`Error: ${err.message}`);
1240
+ process.exit(1);
1241
+ }
1242
+ });
1243
+ payment.command("refund <id>").description("Refund a payment").option("-a, --amount <amount>", "Refund amount in cents (defaults to full refundable amount)").option("--offline", "Process as offline refund").option("-r, --reason <reason>", "Refund reason").action(async (id, opts) => {
1244
+ try {
1245
+ const body = { paymentId: id };
1246
+ if (opts.amount) body.amount = parseInt(opts.amount, 10);
1247
+ if (opts.offline) body.offlineRefundFlag = true;
1248
+ if (opts.reason) body.reason = opts.reason;
1249
+ const data = await apiRequest("POST", "/refunds", body);
1250
+ success("Refund created");
1251
+ output(data, (d) => [
1252
+ `Refund ID: ${d.id}`,
1253
+ `Amount: ${fmtAmount(d.amount, d.currency)}`,
1254
+ `Status: ${d.status}`
1255
+ ].join("\n"));
1256
+ } catch (err) {
1257
+ console.error(`Error: ${err.message}`);
1258
+ process.exit(1);
1259
+ }
1260
+ });
1261
+ }
1262
+
1263
+ // src/commands/refund.ts
1264
+ function registerRefundCommands(program2) {
1265
+ const refund = program2.command("refund").description("Manage refunds");
1266
+ refund.command("get <id>").description("Get refund status").option("--gateway", "Query gateway enquiry details").action(async (id, opts) => {
1267
+ try {
1268
+ const data = await apiRequest("GET", `/refunds/${id}${opts.gateway ? "?gateway=true" : ""}`);
1269
+ output(data, (d) => [
1270
+ `Refund ID: ${d.id}`,
1271
+ `Payment ID: ${d.paymentId}`,
1272
+ `Amount: ${fmtAmount(d.amount, d.currency)}`,
1273
+ `Status: ${d.status}`,
1274
+ `Gateway: ${d.gateway}`,
1275
+ d.gatewayStatus ? `Gateway Status: ${d.gatewayStatus}` : null,
1276
+ d.gatewayStatusCode ? `Gateway Status Code: ${d.gatewayStatusCode}` : null,
1277
+ d.gatewayStatusMessage ? `Gateway Message: ${d.gatewayStatusMessage}` : null,
1278
+ d.gatewayRequestNo ? `Gateway Request No: ${d.gatewayRequestNo}` : null,
1279
+ d.gatewayTransactionId ? `Gateway Transaction ID: ${d.gatewayTransactionId}` : null,
1280
+ `Created: ${fmtDate(d.createdAt)}`
1281
+ ].filter(Boolean).join("\n"));
1282
+ } catch (err) {
1283
+ console.error(`Error: ${err.message}`);
1284
+ process.exit(1);
1285
+ }
1286
+ });
1287
+ }
1288
+
1289
+ // src/index.ts
1290
+ var program = new Command();
1291
+ program.name("uniweb").description("uniweb CLI - manage payments from the command line").version("0.1.0").option("--json", "Output in JSON format").hook("preAction", (thisCommand) => {
1292
+ const opts = thisCommand.opts();
1293
+ if (opts.json) setOutputFormat("json");
1294
+ });
1295
+ registerWalletCommands(program);
1296
+ registerProductCommands(program);
1297
+ registerPriceCommands(program);
1298
+ registerCheckoutCommands(program);
1299
+ registerSubscriptionCommands(program);
1300
+ registerCustomerCommands(program);
1301
+ registerLinkCommands(program);
1302
+ registerCreateCommand(program);
1303
+ registerPayoutCommands(program);
1304
+ registerBankCommands(program);
1305
+ registerKycCommands(program);
1306
+ registerWebhookCommands(program);
1307
+ registerKeyCommands(program);
1308
+ registerPaymentCommands(program);
1309
+ registerRefundCommands(program);
1310
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@uniwebpay/cli",
3
+ "version": "0.1.0",
4
+ "description": "uniweb CLI - Payment infrastructure for the AI era. Accept cards, e-wallets, and QR payments in Southeast Asia.",
5
+ "type": "module",
6
+ "bin": {
7
+ "uniweb": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "dev": "tsx src/index.ts"
15
+ },
16
+ "keywords": [
17
+ "payment",
18
+ "cli",
19
+ "checkout",
20
+ "subscription",
21
+ "billing",
22
+ "uniweb",
23
+ "ai-native",
24
+ "southeast-asia",
25
+ "singapore",
26
+ "wechat-pay",
27
+ "alipay",
28
+ "paynow"
29
+ ],
30
+ "author": "Glitter Protocol / uniweb contributors",
31
+ "license": "MIT",
32
+ "homepage": "https://vibecash.dev",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/glitternetwork/uniweb.git",
36
+ "directory": "packages/cli"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "dependencies": {
42
+ "commander": "^12.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@uniweb/shared": "workspace:*",
46
+ "tsup": "^8.0.0",
47
+ "tsx": "^4.7.0",
48
+ "typescript": "^5.5.0"
49
+ }
50
+ }
51
+