vibecash 0.1.3 → 0.2.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/dist/index.js +788 -832
- package/package.json +12 -27
- package/README.md +0 -191
package/dist/index.js
CHANGED
|
@@ -1,1012 +1,968 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as Command9 } from "commander";
|
|
5
|
-
|
|
6
|
-
// src/commands/wallet.ts
|
|
7
4
|
import { Command } from "commander";
|
|
8
5
|
|
|
9
6
|
// src/config.ts
|
|
10
7
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
11
8
|
import { homedir } from "os";
|
|
12
9
|
import { join } from "path";
|
|
13
|
-
|
|
14
|
-
// ../shared/src/constants.ts
|
|
15
|
-
var PRODUCT_NAME = "vibecash";
|
|
16
|
-
var DOMAINS = {
|
|
17
|
-
api: `api.${PRODUCT_NAME}.dev`,
|
|
18
|
-
app: `${PRODUCT_NAME}.dev`,
|
|
19
|
-
pay: `pay.${PRODUCT_NAME}.dev`,
|
|
20
|
-
js: `js.${PRODUCT_NAME}.dev`
|
|
21
|
-
};
|
|
22
|
-
var API_BASE_URL = `https://${DOMAINS.api}/v1`;
|
|
23
|
-
var EXPIRATION = {
|
|
24
|
-
checkoutSession: 24 * 60 * 60 * 1e3,
|
|
25
|
-
// 24 hours
|
|
26
|
-
claimToken: 7 * 24 * 60 * 60 * 1e3,
|
|
27
|
-
// 7 days
|
|
28
|
-
portalSession: 60 * 60 * 1e3
|
|
29
|
-
// 1 hour
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// ../shared/src/utils-browser.ts
|
|
33
|
-
function formatAmount(amountCents, currency) {
|
|
34
|
-
const amount = amountCents / 100;
|
|
35
|
-
return new Intl.NumberFormat("en-US", {
|
|
36
|
-
style: "currency",
|
|
37
|
-
currency
|
|
38
|
-
}).format(amount);
|
|
39
|
-
}
|
|
40
|
-
function parseAmountToCents(amount) {
|
|
41
|
-
const parsed = typeof amount === "string" ? parseFloat(amount) : amount;
|
|
42
|
-
return Math.round(parsed * 100);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ../shared/src/utils-node.ts
|
|
46
|
-
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
47
|
-
|
|
48
|
-
// src/config.ts
|
|
49
|
-
var CONFIG_DIR = join(homedir(), `.${PRODUCT_NAME}`);
|
|
10
|
+
var CONFIG_DIR = join(homedir(), ".vibecash");
|
|
50
11
|
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
51
12
|
function getConfig() {
|
|
52
|
-
if (!existsSync(CONFIG_FILE)) {
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
try {
|
|
56
|
-
const content = readFileSync(CONFIG_FILE, "utf-8");
|
|
57
|
-
return JSON.parse(content);
|
|
58
|
-
} catch {
|
|
59
|
-
return {};
|
|
60
|
-
}
|
|
13
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
14
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
61
15
|
}
|
|
62
16
|
function saveConfig(config) {
|
|
63
|
-
if (!existsSync(CONFIG_DIR)) {
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
const existing = getConfig();
|
|
67
|
-
const merged = { ...existing, ...config };
|
|
68
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2));
|
|
17
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
18
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
69
19
|
}
|
|
70
20
|
function getSecret() {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
21
|
+
return process.env.VIBECASH_SECRET || getConfig().secret;
|
|
22
|
+
}
|
|
23
|
+
function setSecret(secret) {
|
|
75
24
|
const config = getConfig();
|
|
76
|
-
|
|
25
|
+
config.secret = secret;
|
|
26
|
+
saveConfig(config);
|
|
77
27
|
}
|
|
78
28
|
function getApiUrl() {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return config.apiUrl || "https://api.vibecash.dev";
|
|
29
|
+
return process.env.VIBECASH_API_URL || getConfig().apiUrl || "https://api.vibecash.dev";
|
|
30
|
+
}
|
|
31
|
+
var _overrideFormat;
|
|
32
|
+
function setOutputFormat(format) {
|
|
33
|
+
_overrideFormat = format;
|
|
85
34
|
}
|
|
86
35
|
function getOutputFormat() {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const config = getConfig();
|
|
92
|
-
return config.outputFormat || "json";
|
|
36
|
+
if (_overrideFormat) return _overrideFormat;
|
|
37
|
+
const env = process.env.VIBECASH_OUTPUT;
|
|
38
|
+
if (env === "json" || env === "human") return env;
|
|
39
|
+
return getConfig().outputFormat || "human";
|
|
93
40
|
}
|
|
94
41
|
|
|
95
42
|
// src/api-client.ts
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
secret
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"Content-Type": "application/json"
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
const response = await fetch(url, {
|
|
117
|
-
method,
|
|
118
|
-
headers,
|
|
119
|
-
body: body ? JSON.stringify(body) : void 0
|
|
120
|
-
});
|
|
121
|
-
const json = await response.json();
|
|
122
|
-
if (!response.ok) {
|
|
123
|
-
const errorMessage = json.error?.message || `API error: ${response.status}`;
|
|
124
|
-
throw new Error(errorMessage);
|
|
125
|
-
}
|
|
126
|
-
return json;
|
|
127
|
-
}
|
|
128
|
-
// Wallets
|
|
129
|
-
async createWallet() {
|
|
130
|
-
return this.request("POST", "/v1/wallets", void 0, false);
|
|
131
|
-
}
|
|
132
|
-
async getWallet() {
|
|
133
|
-
return this.request("GET", "/v1/wallets/current");
|
|
134
|
-
}
|
|
135
|
-
async createClaimLink() {
|
|
136
|
-
return this.request("POST", "/v1/wallets/current/claim_link");
|
|
137
|
-
}
|
|
138
|
-
// Products
|
|
139
|
-
async createProduct(data) {
|
|
140
|
-
return this.request("POST", "/v1/products", data);
|
|
141
|
-
}
|
|
142
|
-
async listProducts() {
|
|
143
|
-
return this.request("GET", "/v1/products");
|
|
144
|
-
}
|
|
145
|
-
async getProduct(id) {
|
|
146
|
-
return this.request("GET", `/v1/products/${id}`);
|
|
147
|
-
}
|
|
148
|
-
// Prices
|
|
149
|
-
async createPrice(data) {
|
|
150
|
-
return this.request("POST", "/v1/prices", {
|
|
151
|
-
productId: data.productId,
|
|
152
|
-
amount: data.amount,
|
|
153
|
-
currency: data.currency,
|
|
154
|
-
type: data.type,
|
|
155
|
-
interval: data.interval,
|
|
156
|
-
intervalCount: data.intervalCount,
|
|
157
|
-
trialPeriodDays: data.trialPeriodDays
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
async listPrices(productId) {
|
|
161
|
-
const query = productId ? `?product_id=${productId}` : "";
|
|
162
|
-
return this.request("GET", `/v1/prices${query}`);
|
|
163
|
-
}
|
|
164
|
-
// Checkout
|
|
165
|
-
async createCheckoutSession(data) {
|
|
166
|
-
return this.request("POST", "/v1/checkout/sessions", data);
|
|
167
|
-
}
|
|
168
|
-
async getCheckoutSession(id) {
|
|
169
|
-
return this.request("GET", `/v1/checkout/sessions/${id}`, void 0, false);
|
|
170
|
-
}
|
|
171
|
-
// Subscriptions
|
|
172
|
-
async listSubscriptions(params) {
|
|
173
|
-
const query = new URLSearchParams();
|
|
174
|
-
if (params?.customerId) query.set("customer_id", params.customerId);
|
|
175
|
-
if (params?.status) query.set("status", params.status);
|
|
176
|
-
const queryStr = query.toString() ? `?${query.toString()}` : "";
|
|
177
|
-
return this.request("GET", `/v1/subscriptions${queryStr}`);
|
|
178
|
-
}
|
|
179
|
-
async getSubscription(id) {
|
|
180
|
-
return this.request("GET", `/v1/subscriptions/${id}`);
|
|
181
|
-
}
|
|
182
|
-
async cancelSubscription(id, immediately = false) {
|
|
183
|
-
return this.request("POST", `/v1/subscriptions/${id}/cancel`, {
|
|
184
|
-
cancel_at_period_end: !immediately
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
async resumeSubscription(id) {
|
|
188
|
-
return this.request("POST", `/v1/subscriptions/${id}/resume`);
|
|
189
|
-
}
|
|
190
|
-
// Customers
|
|
191
|
-
async createCustomer(data) {
|
|
192
|
-
return this.request("POST", "/v1/customers", data);
|
|
193
|
-
}
|
|
194
|
-
async listCustomers() {
|
|
195
|
-
return this.request("GET", "/v1/customers");
|
|
196
|
-
}
|
|
197
|
-
async getCustomer(id) {
|
|
198
|
-
return this.request("GET", `/v1/customers/${id}`);
|
|
199
|
-
}
|
|
200
|
-
async createPortalSession(customerId, returnUrl) {
|
|
201
|
-
return this.request("POST", `/v1/customers/${customerId}/portal_sessions`, {
|
|
202
|
-
return_url: returnUrl
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
// Payment Links
|
|
206
|
-
async createPaymentLink(data) {
|
|
207
|
-
return this.request("POST", "/v1/payment_links", data);
|
|
208
|
-
}
|
|
209
|
-
async listPaymentLinks() {
|
|
210
|
-
return this.request("GET", "/v1/payment_links");
|
|
211
|
-
}
|
|
212
|
-
async getPaymentLink(id) {
|
|
213
|
-
return this.request("GET", `/v1/payment_links/${id}`, void 0, false);
|
|
43
|
+
async function apiRequest(method, path, body) {
|
|
44
|
+
const secret = getSecret();
|
|
45
|
+
if (!secret) {
|
|
46
|
+
console.error("No API key found. Set VIBECASH_SECRET or run `vibecash wallet create`");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const url = `${getApiUrl()}/v1${path}`;
|
|
50
|
+
const res = await fetch(url, {
|
|
51
|
+
method,
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
"Authorization": `Bearer ${secret}`
|
|
55
|
+
},
|
|
56
|
+
body: body ? JSON.stringify(body) : void 0
|
|
57
|
+
});
|
|
58
|
+
const data = await res.json();
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
const msg = data.error?.message || "Unknown error";
|
|
61
|
+
throw new Error(`API Error (${res.status}): ${msg}`);
|
|
214
62
|
}
|
|
215
|
-
|
|
216
|
-
|
|
63
|
+
return data;
|
|
64
|
+
}
|
|
65
|
+
async function apiRequestNoAuth(method, path, body) {
|
|
66
|
+
const url = `${getApiUrl()}/v1${path}`;
|
|
67
|
+
const res = await fetch(url, {
|
|
68
|
+
method,
|
|
69
|
+
headers: { "Content-Type": "application/json" },
|
|
70
|
+
body: body ? JSON.stringify(body) : void 0
|
|
71
|
+
});
|
|
72
|
+
const data = await res.json();
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
const msg = data.error?.message || "Unknown error";
|
|
75
|
+
throw new Error(`API Error (${res.status}): ${msg}`);
|
|
217
76
|
}
|
|
218
|
-
|
|
77
|
+
return data;
|
|
78
|
+
}
|
|
219
79
|
|
|
220
80
|
// src/utils/output.ts
|
|
221
|
-
function formatAmount2(amountCents, currency = "USD") {
|
|
222
|
-
return formatAmount(amountCents, currency);
|
|
223
|
-
}
|
|
224
81
|
function output(data, humanFormatter) {
|
|
225
82
|
const format = getOutputFormat();
|
|
226
|
-
if (format === "
|
|
83
|
+
if (format === "json") {
|
|
84
|
+
console.log(JSON.stringify(data, null, 2));
|
|
85
|
+
} else if (humanFormatter) {
|
|
227
86
|
console.log(humanFormatter(data));
|
|
228
87
|
} else {
|
|
229
88
|
console.log(JSON.stringify(data, null, 2));
|
|
230
89
|
}
|
|
231
90
|
}
|
|
232
|
-
function
|
|
233
|
-
|
|
91
|
+
function success(message) {
|
|
92
|
+
console.log(`\u2713 ${message}`);
|
|
234
93
|
}
|
|
235
|
-
function
|
|
236
|
-
|
|
94
|
+
function table(headers, rows) {
|
|
95
|
+
const colWidths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] || "").length)));
|
|
96
|
+
const top = "\u250C" + colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u252C") + "\u2510";
|
|
97
|
+
const bot = "\u2514" + colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u2534") + "\u2518";
|
|
98
|
+
const row = (cells) => "\u2502" + cells.map((c, i) => ` ${(c || "").padEnd(colWidths[i])} `).join("\u2502") + "\u2502";
|
|
99
|
+
console.log(top);
|
|
100
|
+
console.log(row(headers));
|
|
101
|
+
console.log("\u251C" + colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u253C") + "\u2524");
|
|
102
|
+
rows.forEach((r) => console.log(row(r)));
|
|
103
|
+
console.log(bot);
|
|
237
104
|
}
|
|
238
|
-
function
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
past_due: "\x1B[31m",
|
|
245
|
-
// red
|
|
246
|
-
canceled: "\x1B[90m",
|
|
247
|
-
// gray
|
|
248
|
-
succeeded: "\x1B[32m",
|
|
249
|
-
failed: "\x1B[31m",
|
|
250
|
-
pending: "\x1B[33m",
|
|
251
|
-
open: "\x1B[33m",
|
|
252
|
-
complete: "\x1B[32m",
|
|
253
|
-
expired: "\x1B[90m",
|
|
254
|
-
created: "\x1B[36m",
|
|
255
|
-
// cyan
|
|
256
|
-
claimed: "\x1B[32m",
|
|
257
|
-
verified: "\x1B[32m"
|
|
258
|
-
};
|
|
259
|
-
const reset = "\x1B[0m";
|
|
260
|
-
const color = colors[status] || "";
|
|
261
|
-
return `${color}${status}${reset}`;
|
|
105
|
+
function fmtAmount(cents, currency) {
|
|
106
|
+
if (cents == null) return "N/A";
|
|
107
|
+
const n = typeof cents === "string" ? parseInt(cents, 10) : cents;
|
|
108
|
+
if (isNaN(n)) return String(cents);
|
|
109
|
+
const dec = (n / 100).toFixed(2);
|
|
110
|
+
return currency ? `${dec} ${currency}` : dec;
|
|
262
111
|
}
|
|
263
|
-
function
|
|
264
|
-
if (
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
return rows.map(
|
|
273
|
-
(row) => row.map((cell, i) => cell.padEnd(colWidths[i] + (cell.length - stripAnsi(cell).length))).join(" ")
|
|
274
|
-
).join("\n");
|
|
275
|
-
}
|
|
276
|
-
function stripAnsi(str) {
|
|
277
|
-
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
278
|
-
}
|
|
279
|
-
function error(message) {
|
|
280
|
-
console.error(`\x1B[31mError:\x1B[0m ${message}`);
|
|
281
|
-
process.exit(1);
|
|
282
|
-
}
|
|
283
|
-
function info(message) {
|
|
284
|
-
console.log(`\x1B[36m\u2139\x1B[0m ${message}`);
|
|
112
|
+
function fmtDate(ts) {
|
|
113
|
+
if (ts == null || ts === "") return "N/A";
|
|
114
|
+
const n = typeof ts === "string" ? parseInt(ts, 10) : ts;
|
|
115
|
+
if (isNaN(n) || n === 0) return "N/A";
|
|
116
|
+
const d = new Date(n);
|
|
117
|
+
const pad = (v) => String(v).padStart(2, "0");
|
|
118
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
285
119
|
}
|
|
286
120
|
|
|
287
121
|
// src/commands/wallet.ts
|
|
288
|
-
function
|
|
289
|
-
const wallet =
|
|
122
|
+
function registerWalletCommands(program2) {
|
|
123
|
+
const wallet = program2.command("wallet").description("Manage your wallet");
|
|
290
124
|
wallet.command("create").description("Create a new wallet").action(async () => {
|
|
291
125
|
try {
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
126
|
+
const data = await apiRequestNoAuth("POST", "/wallets");
|
|
127
|
+
if (data.secret) {
|
|
128
|
+
setSecret(data.secret);
|
|
129
|
+
success("Wallet created! API key saved to ~/.vibecash/config.json");
|
|
130
|
+
}
|
|
131
|
+
output(data, (d) => {
|
|
132
|
+
const w = d.wallet || d;
|
|
297
133
|
return [
|
|
298
|
-
`Wallet
|
|
299
|
-
|
|
300
|
-
`
|
|
301
|
-
` Secret: ${d.secret}`,
|
|
302
|
-
` Status: ${formatStatus(d.status)}`,
|
|
303
|
-
``,
|
|
304
|
-
`Your secret has been saved to ~/.aipay/config.json`,
|
|
305
|
-
`You can also set it as an environment variable:`,
|
|
306
|
-
` export AIPAY_SECRET=${d.secret}`
|
|
134
|
+
`Wallet ID: ${w.id}`,
|
|
135
|
+
`Status: ${w.status || "active"}`,
|
|
136
|
+
`Currency: ${w.currency || "SGD"}`
|
|
307
137
|
].join("\n");
|
|
308
138
|
});
|
|
309
139
|
} catch (err) {
|
|
310
|
-
error(
|
|
140
|
+
console.error(`Error: ${err.message}`);
|
|
141
|
+
process.exit(1);
|
|
311
142
|
}
|
|
312
143
|
});
|
|
313
|
-
wallet.command("
|
|
144
|
+
wallet.command("info").description("Get current wallet info").action(async () => {
|
|
314
145
|
try {
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
output(result, (data) => {
|
|
318
|
-
const d = data;
|
|
146
|
+
const data = await apiRequest("GET", "/wallets/current");
|
|
147
|
+
output(data, (d) => {
|
|
319
148
|
return [
|
|
320
|
-
`Wallet:
|
|
321
|
-
|
|
322
|
-
`
|
|
323
|
-
`
|
|
324
|
-
`
|
|
325
|
-
`
|
|
149
|
+
`Wallet ID: ${d.id}`,
|
|
150
|
+
`Status: ${d.status || "N/A"}`,
|
|
151
|
+
`Currency: ${d.currency || "N/A"}`,
|
|
152
|
+
`Available: ${fmtAmount(d.availableBalance, d.currency)}`,
|
|
153
|
+
`Pending: ${fmtAmount(d.pendingBalance, d.currency)}`,
|
|
154
|
+
`Withdrawn: ${fmtAmount(d.withdrawnBalance, d.currency)}`,
|
|
155
|
+
`Webhook: ${d.webhookUrl || "not configured"}`,
|
|
156
|
+
`Created: ${fmtDate(d.createdAt)}`
|
|
326
157
|
].join("\n");
|
|
327
158
|
});
|
|
328
159
|
} catch (err) {
|
|
329
|
-
error(
|
|
160
|
+
console.error(`Error: ${err.message}`);
|
|
161
|
+
process.exit(1);
|
|
330
162
|
}
|
|
331
163
|
});
|
|
332
|
-
wallet.command("claim").description("Generate a claim link for
|
|
164
|
+
wallet.command("claim").description("Generate a claim link for your wallet").action(async () => {
|
|
333
165
|
try {
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
error("This wallet has already been claimed and cannot be claimed again.");
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
const result = await client.createClaimLink();
|
|
341
|
-
output(result, (data) => {
|
|
342
|
-
const d = data;
|
|
343
|
-
return [
|
|
344
|
-
`Claim link generated!`,
|
|
345
|
-
``,
|
|
346
|
-
` URL: ${d.claimUrl}`,
|
|
347
|
-
` Expires: ${formatDate(d.expiresAt)}`,
|
|
348
|
-
``,
|
|
349
|
-
`Share this link to allow someone to claim ownership of this wallet.`
|
|
350
|
-
].join("\n");
|
|
166
|
+
const data = await apiRequest("POST", "/wallets/current/claim_link");
|
|
167
|
+
output(data, (d) => {
|
|
168
|
+
return `Claim Link: ${d.url || d.claimLink || d.link || JSON.stringify(d)}`;
|
|
351
169
|
});
|
|
352
170
|
} catch (err) {
|
|
353
|
-
error(
|
|
171
|
+
console.error(`Error: ${err.message}`);
|
|
172
|
+
process.exit(1);
|
|
354
173
|
}
|
|
355
174
|
});
|
|
356
|
-
return wallet;
|
|
357
175
|
}
|
|
358
176
|
|
|
359
177
|
// src/commands/product.ts
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
output(result, (data) => {
|
|
371
|
-
const d = data;
|
|
372
|
-
return [
|
|
373
|
-
`Product created!`,
|
|
374
|
-
``,
|
|
375
|
-
` ID: ${d.id}`,
|
|
376
|
-
` Name: ${d.name}`,
|
|
377
|
-
d.description ? ` Description: ${d.description}` : null,
|
|
378
|
-
` Active: ${d.active}`,
|
|
379
|
-
` Created: ${formatDate(d.createdAt)}`
|
|
380
|
-
].filter(Boolean).join("\n");
|
|
381
|
-
});
|
|
178
|
+
function registerProductCommands(program2) {
|
|
179
|
+
const product = program2.command("product").description("Manage products");
|
|
180
|
+
product.command("create <name>").description("Create a new product").option("-d, --description <description>", "Product description").action(async (name, opts) => {
|
|
181
|
+
try {
|
|
182
|
+
const body = { name };
|
|
183
|
+
if (opts.description) body.description = opts.description;
|
|
184
|
+
const data = await apiRequest("POST", "/products", body);
|
|
185
|
+
success("Product created");
|
|
186
|
+
output(data, (d) => `Product ID: ${d.id}
|
|
187
|
+
Name: ${d.name}`);
|
|
382
188
|
} catch (err) {
|
|
383
|
-
error(
|
|
189
|
+
console.error(`Error: ${err.message}`);
|
|
190
|
+
process.exit(1);
|
|
384
191
|
}
|
|
385
192
|
});
|
|
386
193
|
product.command("list").description("List all products").action(async () => {
|
|
387
194
|
try {
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
...d.data.map((p) => [p.id, p.name, p.active ? "yes" : "no", formatDate(p.createdAt)])
|
|
398
|
-
];
|
|
399
|
-
return formatTable(rows);
|
|
195
|
+
const data = await apiRequest("GET", "/products");
|
|
196
|
+
output(data, (d) => {
|
|
197
|
+
const items = d.data || d.products || d;
|
|
198
|
+
if (!Array.isArray(items) || items.length === 0) return "No products found.";
|
|
199
|
+
table(
|
|
200
|
+
["ID", "Name", "Created"],
|
|
201
|
+
items.map((p) => [p.id, p.name, fmtDate(p.createdAt)])
|
|
202
|
+
);
|
|
203
|
+
return "";
|
|
400
204
|
});
|
|
401
205
|
} catch (err) {
|
|
402
|
-
error(
|
|
206
|
+
console.error(`Error: ${err.message}`);
|
|
207
|
+
process.exit(1);
|
|
403
208
|
}
|
|
404
209
|
});
|
|
405
|
-
product.command("get").description("Get product
|
|
210
|
+
product.command("get <id>").description("Get a product by ID").action(async (id) => {
|
|
406
211
|
try {
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
212
|
+
const data = await apiRequest("GET", `/products/${id}`);
|
|
213
|
+
output(data, (d) => [
|
|
214
|
+
`Product ID: ${d.id}`,
|
|
215
|
+
`Name: ${d.name}`,
|
|
216
|
+
`Description: ${d.description || "N/A"}`,
|
|
217
|
+
`Created: ${fmtDate(d.createdAt)}`
|
|
218
|
+
].join("\n"));
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error(`Error: ${err.message}`);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
product.command("update <id>").description("Update a product").option("-n, --name <name>", "New product name").option("-d, --description <description>", "New product description").action(async (id, opts) => {
|
|
225
|
+
try {
|
|
226
|
+
const body = {};
|
|
227
|
+
if (opts.name) body.name = opts.name;
|
|
228
|
+
if (opts.description) body.description = opts.description;
|
|
229
|
+
await apiRequest("PATCH", `/products/${id}`, body);
|
|
230
|
+
success("Product updated");
|
|
420
231
|
} catch (err) {
|
|
421
|
-
error(
|
|
232
|
+
console.error(`Error: ${err.message}`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
product.command("delete <id>").description("Delete a product").action(async (id) => {
|
|
237
|
+
try {
|
|
238
|
+
await apiRequest("DELETE", `/products/${id}`);
|
|
239
|
+
success(`Product ${id} deleted`);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.error(`Error: ${err.message}`);
|
|
242
|
+
process.exit(1);
|
|
422
243
|
}
|
|
423
244
|
});
|
|
424
|
-
return product;
|
|
425
245
|
}
|
|
426
246
|
|
|
427
247
|
// src/commands/price.ts
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
intervalCount: parseInt(options.intervalCount, 10),
|
|
451
|
-
trialPeriodDays: options.trialDays ? parseInt(options.trialDays, 10) : void 0
|
|
452
|
-
});
|
|
453
|
-
output(result, (data) => {
|
|
454
|
-
const d = data;
|
|
455
|
-
const interval = d.type === "recurring" && d.interval ? d.intervalCount === 1 ? `/${d.interval}` : `/${d.intervalCount} ${d.interval}s` : "";
|
|
456
|
-
return [
|
|
457
|
-
`Price created!`,
|
|
458
|
-
``,
|
|
459
|
-
` ID: ${d.id}`,
|
|
460
|
-
` Amount: ${formatCurrency(d.amount, d.currency)}${interval}`,
|
|
461
|
-
` Type: ${d.type}`,
|
|
462
|
-
d.interval ? ` Interval: ${d.interval}` : null,
|
|
463
|
-
d.trialPeriodDays ? ` Trial: ${d.trialPeriodDays} days` : null,
|
|
464
|
-
` Created: ${formatDate(d.createdAt)}`
|
|
465
|
-
].filter(Boolean).join("\n");
|
|
466
|
-
});
|
|
467
|
-
} catch (err) {
|
|
468
|
-
error(err instanceof Error ? err.message : "Failed to create price");
|
|
469
|
-
}
|
|
248
|
+
function registerPriceCommands(program2) {
|
|
249
|
+
const price = program2.command("price").description("Manage prices");
|
|
250
|
+
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) => {
|
|
251
|
+
try {
|
|
252
|
+
const body = {
|
|
253
|
+
productId,
|
|
254
|
+
unitAmount: parseInt(opts.amount, 10),
|
|
255
|
+
type: opts.type,
|
|
256
|
+
currency: opts.currency.toUpperCase()
|
|
257
|
+
};
|
|
258
|
+
if (opts.interval) body.interval = opts.interval;
|
|
259
|
+
const data = await apiRequest("POST", "/prices", body);
|
|
260
|
+
success("Price created");
|
|
261
|
+
output(data, (d) => [
|
|
262
|
+
`Price ID: ${d.id}`,
|
|
263
|
+
`Amount: ${fmtAmount(d.unitAmount ?? d.amount, d.currency)}`,
|
|
264
|
+
`Type: ${d.type}`,
|
|
265
|
+
d.interval ? `Interval: ${d.interval}` : null
|
|
266
|
+
].filter(Boolean).join("\n"));
|
|
267
|
+
} catch (err) {
|
|
268
|
+
console.error(`Error: ${err.message}`);
|
|
269
|
+
process.exit(1);
|
|
470
270
|
}
|
|
471
|
-
);
|
|
472
|
-
price.command("list").description("List prices").
|
|
271
|
+
});
|
|
272
|
+
price.command("list").description("List all prices").action(async () => {
|
|
473
273
|
try {
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const rows = [
|
|
482
|
-
["ID", "PRODUCT", "AMOUNT", "TYPE", "INTERVAL"],
|
|
483
|
-
...d.data.map((p) => [
|
|
274
|
+
const data = await apiRequest("GET", "/prices");
|
|
275
|
+
output(data, (d) => {
|
|
276
|
+
const items = d.data || d.prices || d;
|
|
277
|
+
if (!Array.isArray(items) || items.length === 0) return "No prices found.";
|
|
278
|
+
table(
|
|
279
|
+
["ID", "Amount", "Type", "Interval"],
|
|
280
|
+
items.map((p) => [
|
|
484
281
|
p.id,
|
|
485
|
-
p.
|
|
486
|
-
formatCurrency(p.amount, p.currency),
|
|
282
|
+
fmtAmount(p.unitAmount ?? p.amount, p.currency),
|
|
487
283
|
p.type,
|
|
488
284
|
p.interval || "-"
|
|
489
285
|
])
|
|
490
|
-
|
|
491
|
-
return
|
|
286
|
+
);
|
|
287
|
+
return "";
|
|
492
288
|
});
|
|
493
289
|
} catch (err) {
|
|
494
|
-
error(
|
|
290
|
+
console.error(`Error: ${err.message}`);
|
|
291
|
+
process.exit(1);
|
|
495
292
|
}
|
|
496
293
|
});
|
|
497
|
-
return price;
|
|
498
294
|
}
|
|
499
295
|
|
|
500
296
|
// src/commands/checkout.ts
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
};
|
|
524
|
-
if (options.price) {
|
|
525
|
-
params.priceId = options.price;
|
|
526
|
-
} else if (options.amount) {
|
|
527
|
-
params.lineItems = [
|
|
528
|
-
{
|
|
529
|
-
description: options.description,
|
|
530
|
-
amount: parseAmountToCents(options.amount),
|
|
531
|
-
currency: options.currency.toUpperCase(),
|
|
532
|
-
quantity: 1
|
|
533
|
-
}
|
|
534
|
-
];
|
|
535
|
-
}
|
|
536
|
-
const result = await client.createCheckoutSession(params);
|
|
537
|
-
output(result, (data) => {
|
|
538
|
-
const d = data;
|
|
539
|
-
return [
|
|
540
|
-
`Checkout session created!`,
|
|
541
|
-
``,
|
|
542
|
-
` ID: ${d.id}`,
|
|
543
|
-
` Mode: ${d.mode}`,
|
|
544
|
-
` Status: ${formatStatus(d.status)}`,
|
|
545
|
-
d.amountTotal ? ` Amount: ${formatCurrency(d.amountTotal, d.currency || "USD")}` : null,
|
|
546
|
-
` URL: ${d.url}`,
|
|
547
|
-
` Expires: ${formatDate(d.expiresAt)}`
|
|
548
|
-
].filter(Boolean).join("\n");
|
|
549
|
-
});
|
|
550
|
-
} catch (err) {
|
|
551
|
-
error(err instanceof Error ? err.message : "Failed to create checkout session");
|
|
552
|
-
}
|
|
297
|
+
function registerCheckoutCommands(program2) {
|
|
298
|
+
const checkout = program2.command("checkout").description("Manage checkout sessions");
|
|
299
|
+
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").action(async (opts) => {
|
|
300
|
+
try {
|
|
301
|
+
const body = {
|
|
302
|
+
mode: opts.mode,
|
|
303
|
+
lineItems: [{ priceId: opts.priceId, quantity: parseInt(opts.quantity, 10) }],
|
|
304
|
+
successUrl: opts.successUrl,
|
|
305
|
+
cancelUrl: opts.cancelUrl,
|
|
306
|
+
paymentMethodTypes: opts.methods.split(",").map((m) => m.trim())
|
|
307
|
+
};
|
|
308
|
+
const data = await apiRequest("POST", "/checkout/sessions", body);
|
|
309
|
+
success("Checkout session created");
|
|
310
|
+
output(data, (d) => {
|
|
311
|
+
return [
|
|
312
|
+
`Session ID: ${d.id}`,
|
|
313
|
+
`Checkout URL: ${d.url || d.checkoutUrl || "N/A"}`
|
|
314
|
+
].join("\n");
|
|
315
|
+
});
|
|
316
|
+
} catch (err) {
|
|
317
|
+
console.error(`Error: ${err.message}`);
|
|
318
|
+
process.exit(1);
|
|
553
319
|
}
|
|
554
|
-
);
|
|
555
|
-
checkout.command("
|
|
556
|
-
try {
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
320
|
+
});
|
|
321
|
+
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) => {
|
|
322
|
+
try {
|
|
323
|
+
const formatSession = (d) => {
|
|
324
|
+
return [
|
|
325
|
+
`Session ID: ${d.id}`,
|
|
326
|
+
`Status: ${d.status || "N/A"}`,
|
|
327
|
+
`Mode: ${d.mode || "N/A"}`,
|
|
328
|
+
d.amountTotal != null ? `Amount: ${fmtAmount(d.amountTotal, d.currency)}` : null,
|
|
329
|
+
d.paymentId ? `Payment ID: ${d.paymentId}` : null,
|
|
330
|
+
d.url ? `URL: ${d.url}` : null,
|
|
331
|
+
d.successUrl ? `Success: ${d.successUrl}` : null
|
|
332
|
+
].filter(Boolean).join("\n");
|
|
333
|
+
};
|
|
334
|
+
if (!opts.watch) {
|
|
335
|
+
const data = await apiRequest("GET", `/checkout/sessions/${id}`);
|
|
336
|
+
output(data, formatSession);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const intervalMs = parseInt(opts.interval || "5", 10) * 1e3;
|
|
340
|
+
console.log(`Watching session ${id} (every ${intervalMs / 1e3}s)...
|
|
341
|
+
`);
|
|
342
|
+
while (true) {
|
|
343
|
+
const data = await apiRequest("GET", `/checkout/sessions/${id}`);
|
|
344
|
+
const status = data.status || "unknown";
|
|
345
|
+
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
346
|
+
process.stdout.write(`\r[${ts}] Status: ${status} `);
|
|
347
|
+
if (status !== "open") {
|
|
348
|
+
console.log("\n");
|
|
349
|
+
output(data, formatSession);
|
|
350
|
+
break;
|
|
573
351
|
}
|
|
574
|
-
|
|
575
|
-
} else {
|
|
576
|
-
const result = await client.getCheckoutSession(id);
|
|
577
|
-
output(result, formatCheckoutSession);
|
|
352
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
578
353
|
}
|
|
579
354
|
} catch (err) {
|
|
580
|
-
error(
|
|
355
|
+
console.error(`Error: ${err.message}`);
|
|
356
|
+
process.exit(1);
|
|
581
357
|
}
|
|
582
358
|
});
|
|
583
|
-
return checkout;
|
|
584
|
-
}
|
|
585
|
-
function formatCheckoutSession(data) {
|
|
586
|
-
const d = data;
|
|
587
|
-
const lines = [
|
|
588
|
-
`Checkout Session: ${d.session.id}`,
|
|
589
|
-
``,
|
|
590
|
-
` Status: ${formatStatus(d.session.status)}`,
|
|
591
|
-
` Mode: ${d.session.mode}`
|
|
592
|
-
];
|
|
593
|
-
if (d.product) {
|
|
594
|
-
lines.push(` Product: ${d.product.name} (${d.product.id})`);
|
|
595
|
-
}
|
|
596
|
-
if (d.price) {
|
|
597
|
-
lines.push(` Price: ${formatCurrency(d.price.amount, d.price.currency)} (${d.price.id})`);
|
|
598
|
-
} else if (d.session.amountTotal) {
|
|
599
|
-
lines.push(` Amount: ${formatCurrency(d.session.amountTotal, d.session.currency || "USD")}`);
|
|
600
|
-
}
|
|
601
|
-
lines.push(` URL: ${d.session.url}`);
|
|
602
|
-
lines.push(` Created: ${formatDate(d.session.createdAt)}`);
|
|
603
|
-
if (d.session.completedAt) {
|
|
604
|
-
lines.push(` Completed: ${formatDate(d.session.completedAt)}`);
|
|
605
|
-
}
|
|
606
|
-
return lines.join("\n");
|
|
607
359
|
}
|
|
608
360
|
|
|
609
361
|
// src/commands/subscription.ts
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
return "No subscriptions found.";
|
|
624
|
-
}
|
|
625
|
-
const rows = [
|
|
626
|
-
["ID", "CUSTOMER", "STATUS", "PERIOD END", "CANCEL"],
|
|
627
|
-
...d.data.map((s) => [
|
|
628
|
-
s.id,
|
|
629
|
-
s.customerId,
|
|
630
|
-
formatStatus(s.status),
|
|
631
|
-
formatDate(s.currentPeriodEnd),
|
|
632
|
-
s.cancelAtPeriodEnd ? "yes" : "no"
|
|
633
|
-
])
|
|
634
|
-
];
|
|
635
|
-
return formatTable(rows);
|
|
362
|
+
function registerSubscriptionCommands(program2) {
|
|
363
|
+
const subscription = program2.command("subscription").description("Manage subscriptions");
|
|
364
|
+
subscription.command("list").description("List all subscriptions").action(async () => {
|
|
365
|
+
try {
|
|
366
|
+
const data = await apiRequest("GET", "/subscriptions");
|
|
367
|
+
output(data, (d) => {
|
|
368
|
+
const items = d.data || d.subscriptions || d;
|
|
369
|
+
if (!Array.isArray(items) || items.length === 0) return "No subscriptions found.";
|
|
370
|
+
table(
|
|
371
|
+
["ID", "Status", "Customer", "Price", "Created"],
|
|
372
|
+
items.map((s) => [s.id, s.status, s.customerId || "N/A", s.priceId || "N/A", fmtDate(s.createdAt)])
|
|
373
|
+
);
|
|
374
|
+
return "";
|
|
636
375
|
});
|
|
637
376
|
} catch (err) {
|
|
638
|
-
error(
|
|
377
|
+
console.error(`Error: ${err.message}`);
|
|
378
|
+
process.exit(1);
|
|
639
379
|
}
|
|
640
380
|
});
|
|
641
|
-
subscription.command("get").description("Get subscription
|
|
381
|
+
subscription.command("get <id>").description("Get a subscription by ID").action(async (id) => {
|
|
642
382
|
try {
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
` Status: ${formatStatus(d.status)}`,
|
|
653
|
-
` Period Start: ${formatDate(d.currentPeriodStart)}`,
|
|
654
|
-
` Period End: ${formatDate(d.currentPeriodEnd)}`,
|
|
655
|
-
d.trialEnd ? ` Trial End: ${formatDate(d.trialEnd)}` : null,
|
|
656
|
-
` Cancel: ${d.cancelAtPeriodEnd ? "at period end" : "no"}`,
|
|
657
|
-
` Created: ${formatDate(d.createdAt)}`
|
|
658
|
-
].filter(Boolean).join("\n");
|
|
659
|
-
});
|
|
383
|
+
const data = await apiRequest("GET", `/subscriptions/${id}`);
|
|
384
|
+
output(data, (d) => [
|
|
385
|
+
`Subscription ID: ${d.id}`,
|
|
386
|
+
`Status: ${d.status}`,
|
|
387
|
+
`Customer: ${d.customerId || "N/A"}`,
|
|
388
|
+
`Price: ${d.priceId || "N/A"}`,
|
|
389
|
+
`Current Period End: ${fmtDate(d.currentPeriodEnd)}`,
|
|
390
|
+
`Created: ${fmtDate(d.createdAt)}`
|
|
391
|
+
].join("\n"));
|
|
660
392
|
} catch (err) {
|
|
661
|
-
error(
|
|
393
|
+
console.error(`Error: ${err.message}`);
|
|
394
|
+
process.exit(1);
|
|
662
395
|
}
|
|
663
396
|
});
|
|
664
|
-
subscription.command("cancel
|
|
397
|
+
subscription.command("cancel <id>").description("Cancel a subscription").action(async (id) => {
|
|
665
398
|
try {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
output(result, (data) => {
|
|
669
|
-
const d = data;
|
|
670
|
-
const when = options.immediately ? "immediately" : "at period end";
|
|
671
|
-
return [
|
|
672
|
-
`Subscription ${d.id} will be canceled ${when}.`,
|
|
673
|
-
``,
|
|
674
|
-
` Status: ${formatStatus(d.status)}`,
|
|
675
|
-
` Cancel: ${d.cancelAtPeriodEnd ? "at period end" : "immediately"}`,
|
|
676
|
-
d.canceledAt ? ` Canceled At: ${formatDate(d.canceledAt)}` : null
|
|
677
|
-
].filter(Boolean).join("\n");
|
|
678
|
-
});
|
|
399
|
+
await apiRequest("POST", `/subscriptions/${id}/cancel`);
|
|
400
|
+
success(`Subscription ${id} cancelled`);
|
|
679
401
|
} catch (err) {
|
|
680
|
-
error(
|
|
402
|
+
console.error(`Error: ${err.message}`);
|
|
403
|
+
process.exit(1);
|
|
681
404
|
}
|
|
682
405
|
});
|
|
683
|
-
subscription.command("resume").description("Resume a
|
|
406
|
+
subscription.command("resume <id>").description("Resume a cancelled subscription").action(async (id) => {
|
|
684
407
|
try {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
output(result, (data) => {
|
|
688
|
-
const d = data;
|
|
689
|
-
return [
|
|
690
|
-
`Subscription ${d.id} has been resumed.`,
|
|
691
|
-
``,
|
|
692
|
-
` Status: ${formatStatus(d.status)}`,
|
|
693
|
-
` Cancel: ${d.cancelAtPeriodEnd ? "at period end" : "no"}`
|
|
694
|
-
].join("\n");
|
|
695
|
-
});
|
|
408
|
+
await apiRequest("POST", `/subscriptions/${id}/resume`);
|
|
409
|
+
success(`Subscription ${id} resumed`);
|
|
696
410
|
} catch (err) {
|
|
697
|
-
error(
|
|
411
|
+
console.error(`Error: ${err.message}`);
|
|
412
|
+
process.exit(1);
|
|
698
413
|
}
|
|
699
414
|
});
|
|
700
|
-
return subscription;
|
|
701
415
|
}
|
|
702
416
|
|
|
703
417
|
// src/commands/customer.ts
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
`Customer created!`,
|
|
718
|
-
``,
|
|
719
|
-
` ID: ${d.id}`,
|
|
720
|
-
` Email: ${d.email}`,
|
|
721
|
-
d.name ? ` Name: ${d.name}` : null,
|
|
722
|
-
` Created: ${formatDate(d.createdAt)}`
|
|
723
|
-
].filter(Boolean).join("\n");
|
|
724
|
-
});
|
|
418
|
+
function registerCustomerCommands(program2) {
|
|
419
|
+
const customer = program2.command("customer").description("Manage customers");
|
|
420
|
+
customer.command("create").description("Create a new customer").requiredOption("-e, --email <email>", "Customer email").option("-n, --name <name>", "Customer name").action(async (opts) => {
|
|
421
|
+
try {
|
|
422
|
+
const body = { email: opts.email };
|
|
423
|
+
if (opts.name) body.name = opts.name;
|
|
424
|
+
const data = await apiRequest("POST", "/customers", body);
|
|
425
|
+
success("Customer created");
|
|
426
|
+
output(data, (d) => [
|
|
427
|
+
`Customer ID: ${d.id}`,
|
|
428
|
+
`Email: ${d.email}`,
|
|
429
|
+
`Name: ${d.name || "N/A"}`
|
|
430
|
+
].join("\n"));
|
|
725
431
|
} catch (err) {
|
|
726
|
-
error(
|
|
432
|
+
console.error(`Error: ${err.message}`);
|
|
433
|
+
process.exit(1);
|
|
727
434
|
}
|
|
728
435
|
});
|
|
729
436
|
customer.command("list").description("List all customers").action(async () => {
|
|
730
437
|
try {
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
...d.data.map((c) => [c.id, c.email, c.name || "-", formatDate(c.createdAt)])
|
|
741
|
-
];
|
|
742
|
-
return formatTable(rows);
|
|
438
|
+
const data = await apiRequest("GET", "/customers");
|
|
439
|
+
output(data, (d) => {
|
|
440
|
+
const items = d.data || d.customers || d;
|
|
441
|
+
if (!Array.isArray(items) || items.length === 0) return "No customers found.";
|
|
442
|
+
table(
|
|
443
|
+
["ID", "Email", "Name", "Created"],
|
|
444
|
+
items.map((c) => [c.id, c.email, c.name || "", fmtDate(c.createdAt)])
|
|
445
|
+
);
|
|
446
|
+
return "";
|
|
743
447
|
});
|
|
744
448
|
} catch (err) {
|
|
745
|
-
error(
|
|
449
|
+
console.error(`Error: ${err.message}`);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
customer.command("get <id>").description("Get a customer by ID").action(async (id) => {
|
|
454
|
+
try {
|
|
455
|
+
const data = await apiRequest("GET", `/customers/${id}`);
|
|
456
|
+
output(data, (d) => [
|
|
457
|
+
`Customer ID: ${d.id}`,
|
|
458
|
+
`Email: ${d.email}`,
|
|
459
|
+
`Name: ${d.name || "N/A"}`,
|
|
460
|
+
`Created: ${fmtDate(d.createdAt)}`
|
|
461
|
+
].join("\n"));
|
|
462
|
+
} catch (err) {
|
|
463
|
+
console.error(`Error: ${err.message}`);
|
|
464
|
+
process.exit(1);
|
|
746
465
|
}
|
|
747
466
|
});
|
|
748
|
-
customer.command("
|
|
467
|
+
customer.command("portal <id>").description("Create a customer portal session").action(async (id) => {
|
|
749
468
|
try {
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
output(
|
|
753
|
-
|
|
469
|
+
const data = await apiRequest("POST", `/customers/${id}/portal-session`);
|
|
470
|
+
success("Portal session created");
|
|
471
|
+
output(data, (d) => `Portal URL: ${d.url || d.portalUrl || JSON.stringify(d)}`);
|
|
472
|
+
} catch (err) {
|
|
473
|
+
console.error(`Error: ${err.message}`);
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/commands/link.ts
|
|
480
|
+
function registerLinkCommands(program2) {
|
|
481
|
+
const link = program2.command("link").description("Manage payment links");
|
|
482
|
+
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)").action(async (amount, opts) => {
|
|
483
|
+
try {
|
|
484
|
+
const body = {
|
|
485
|
+
amount: parseInt(amount, 10),
|
|
486
|
+
currency: opts.currency
|
|
487
|
+
};
|
|
488
|
+
if (opts.name) body.name = opts.name;
|
|
489
|
+
if (opts.description) body.description = opts.description;
|
|
490
|
+
if (opts.successUrl) body.successUrl = opts.successUrl;
|
|
491
|
+
if (opts.cancelUrl) body.cancelUrl = opts.cancelUrl;
|
|
492
|
+
if (opts.methods) body.paymentMethodTypes = opts.methods.split(",").map((m) => m.trim());
|
|
493
|
+
const data = await apiRequest("POST", "/payment_links", body);
|
|
494
|
+
success("Payment link created");
|
|
495
|
+
output(data, (d) => {
|
|
754
496
|
return [
|
|
755
|
-
`
|
|
756
|
-
|
|
757
|
-
`
|
|
758
|
-
d.
|
|
759
|
-
` Created: ${formatDate(d.createdAt)}`
|
|
497
|
+
`Link ID: ${d.id}`,
|
|
498
|
+
`URL: ${d.url || "N/A"}`,
|
|
499
|
+
`Amount: ${fmtAmount(d.amount, d.currency)}`,
|
|
500
|
+
d.description ? `Desc: ${d.description}` : null
|
|
760
501
|
].filter(Boolean).join("\n");
|
|
761
502
|
});
|
|
762
503
|
} catch (err) {
|
|
763
|
-
error(
|
|
504
|
+
console.error(`Error: ${err.message}`);
|
|
505
|
+
process.exit(1);
|
|
764
506
|
}
|
|
765
507
|
});
|
|
766
|
-
|
|
508
|
+
link.command("list").description("List all payment links").action(async () => {
|
|
767
509
|
try {
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
510
|
+
const data = await apiRequest("GET", "/payment_links");
|
|
511
|
+
output(data, (d) => {
|
|
512
|
+
const items = d.data || d.paymentLinks || d;
|
|
513
|
+
if (!Array.isArray(items) || items.length === 0) return "No payment links found.";
|
|
514
|
+
table(
|
|
515
|
+
["ID", "Amount", "Active", "Description", "Created"],
|
|
516
|
+
items.map((l) => [
|
|
517
|
+
l.id,
|
|
518
|
+
fmtAmount(l.amount, l.currency),
|
|
519
|
+
l.active ? "yes" : "no",
|
|
520
|
+
l.description || "",
|
|
521
|
+
fmtDate(l.createdAt)
|
|
522
|
+
])
|
|
523
|
+
);
|
|
524
|
+
return "";
|
|
525
|
+
});
|
|
526
|
+
} catch (err) {
|
|
527
|
+
console.error(`Error: ${err.message}`);
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
link.command("get <id>").description("Get a payment link by ID").action(async (id) => {
|
|
532
|
+
try {
|
|
533
|
+
const data = await apiRequest("GET", `/payment_links/${id}`);
|
|
534
|
+
output(data, (d) => {
|
|
772
535
|
return [
|
|
773
|
-
`
|
|
774
|
-
|
|
775
|
-
`
|
|
776
|
-
`
|
|
777
|
-
`
|
|
536
|
+
`Link ID: ${d.id}`,
|
|
537
|
+
`URL: ${d.url || "N/A"}`,
|
|
538
|
+
`Amount: ${fmtAmount(d.amount, d.currency)}`,
|
|
539
|
+
`Active: ${d.active ? "yes" : "no"}`,
|
|
540
|
+
`Desc: ${d.description || "N/A"}`,
|
|
541
|
+
`Created: ${fmtDate(d.createdAt)}`
|
|
778
542
|
].join("\n");
|
|
779
543
|
});
|
|
780
544
|
} catch (err) {
|
|
781
|
-
error(
|
|
545
|
+
console.error(`Error: ${err.message}`);
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
link.command("deactivate <id>").description("Deactivate a payment link").action(async (id) => {
|
|
550
|
+
try {
|
|
551
|
+
await apiRequest("PATCH", `/payment_links/${id}`, { active: false });
|
|
552
|
+
success(`Payment link ${id} deactivated`);
|
|
553
|
+
} catch (err) {
|
|
554
|
+
console.error(`Error: ${err.message}`);
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
link.command("activate <id>").description("Activate a payment link").action(async (id) => {
|
|
559
|
+
try {
|
|
560
|
+
await apiRequest("PATCH", `/payment_links/${id}`, { active: true });
|
|
561
|
+
success(`Payment link ${id} activated`);
|
|
562
|
+
} catch (err) {
|
|
563
|
+
console.error(`Error: ${err.message}`);
|
|
564
|
+
process.exit(1);
|
|
782
565
|
}
|
|
783
566
|
});
|
|
784
|
-
return customer;
|
|
785
567
|
}
|
|
786
568
|
|
|
787
569
|
// src/commands/create.ts
|
|
788
|
-
import {
|
|
789
|
-
function
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
product,
|
|
825
|
-
price,
|
|
826
|
-
session
|
|
827
|
-
},
|
|
828
|
-
(data) => {
|
|
829
|
-
const d = data;
|
|
830
|
-
return [
|
|
831
|
-
`Subscription checkout created!`,
|
|
832
|
-
``,
|
|
833
|
-
` Product: ${d.product.name} (${d.product.id})`,
|
|
834
|
-
` Price: ${formatCurrency(d.price.amount, d.price.currency)}/${d.price.interval} (${d.price.id})`,
|
|
835
|
-
``,
|
|
836
|
-
` Session: ${d.session.id}`,
|
|
837
|
-
` Status: ${formatStatus(d.session.status)}`,
|
|
838
|
-
` URL: ${d.session.url}`,
|
|
839
|
-
` Expires: ${formatDate(d.session.expiresAt)}`
|
|
840
|
-
].join("\n");
|
|
841
|
-
}
|
|
842
|
-
);
|
|
843
|
-
} else {
|
|
844
|
-
const session = await client.createCheckoutSession({
|
|
845
|
-
mode: "payment",
|
|
846
|
-
lineItems: [
|
|
847
|
-
{
|
|
848
|
-
description: options.description,
|
|
849
|
-
amount,
|
|
850
|
-
currency: "USD",
|
|
851
|
-
quantity: 1
|
|
852
|
-
}
|
|
853
|
-
],
|
|
854
|
-
successUrl: options.successUrl,
|
|
855
|
-
cancelUrl: options.cancelUrl,
|
|
856
|
-
customerEmail: options.email
|
|
857
|
-
});
|
|
858
|
-
output(session, (data) => {
|
|
859
|
-
const d = data;
|
|
860
|
-
return [
|
|
861
|
-
`Payment checkout created!`,
|
|
862
|
-
``,
|
|
863
|
-
` Session: ${d.id}`,
|
|
864
|
-
` Amount: ${formatCurrency(d.amountTotal || 0, d.currency || "USD")}`,
|
|
865
|
-
` Status: ${formatStatus(d.status)}`,
|
|
866
|
-
` URL: ${d.url}`,
|
|
867
|
-
` Expires: ${formatDate(d.expiresAt)}`
|
|
868
|
-
].join("\n");
|
|
869
|
-
});
|
|
570
|
+
import { createInterface } from "readline";
|
|
571
|
+
function prompt(question) {
|
|
572
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
573
|
+
return new Promise((resolve) => {
|
|
574
|
+
rl.question(question, (answer) => {
|
|
575
|
+
rl.close();
|
|
576
|
+
resolve(answer.trim());
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
function registerCreateCommand(program2) {
|
|
581
|
+
program2.command("create").description("Quick create: product + price + checkout session in one go").action(async () => {
|
|
582
|
+
try {
|
|
583
|
+
const productName = await prompt("Product name: ");
|
|
584
|
+
if (!productName) {
|
|
585
|
+
console.error("Product name is required.");
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
588
|
+
const amountStr = await prompt("Amount (in smallest currency unit, e.g. 1000 for $10.00): ");
|
|
589
|
+
const amount = parseInt(amountStr, 10);
|
|
590
|
+
if (isNaN(amount) || amount <= 0) {
|
|
591
|
+
console.error("Invalid amount.");
|
|
592
|
+
process.exit(1);
|
|
593
|
+
}
|
|
594
|
+
const currency = await prompt("Currency (default: SGD): ") || "SGD";
|
|
595
|
+
const type = await prompt("Type (one_time or recurring): ");
|
|
596
|
+
if (type !== "one_time" && type !== "recurring") {
|
|
597
|
+
console.error('Type must be "one_time" or "recurring".');
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
let interval;
|
|
601
|
+
if (type === "recurring") {
|
|
602
|
+
interval = await prompt("Billing interval (month or year): ");
|
|
603
|
+
if (interval !== "month" && interval !== "year") {
|
|
604
|
+
console.error('Interval must be "month" or "year".');
|
|
605
|
+
process.exit(1);
|
|
870
606
|
}
|
|
871
|
-
} catch (err) {
|
|
872
|
-
error(err instanceof Error ? err.message : "Failed to create checkout");
|
|
873
607
|
}
|
|
608
|
+
const successUrl = await prompt("Success URL (default: https://example.com/success): ") || "https://example.com/success";
|
|
609
|
+
const cancelUrl = await prompt("Cancel URL (default: https://example.com/cancel): ") || "https://example.com/cancel";
|
|
610
|
+
console.log("\nCreating product...");
|
|
611
|
+
const product = await apiRequest("POST", "/products", { name: productName });
|
|
612
|
+
success(`Product created: ${product.id}`);
|
|
613
|
+
console.log("Creating price...");
|
|
614
|
+
const priceBody = {
|
|
615
|
+
productId: product.id,
|
|
616
|
+
amount,
|
|
617
|
+
type,
|
|
618
|
+
currency
|
|
619
|
+
};
|
|
620
|
+
if (interval) priceBody.interval = interval;
|
|
621
|
+
const price = await apiRequest("POST", "/prices", priceBody);
|
|
622
|
+
success(`Price created: ${price.id}`);
|
|
623
|
+
console.log("Creating checkout session...");
|
|
624
|
+
const session = await apiRequest("POST", "/checkout/sessions", {
|
|
625
|
+
priceId: price.id,
|
|
626
|
+
successUrl,
|
|
627
|
+
cancelUrl
|
|
628
|
+
});
|
|
629
|
+
success(`Checkout session created: ${session.id}`);
|
|
630
|
+
const checkoutUrl = session.url || session.checkoutUrl;
|
|
631
|
+
if (checkoutUrl) {
|
|
632
|
+
console.log(`
|
|
633
|
+
Checkout URL: ${checkoutUrl}`);
|
|
634
|
+
}
|
|
635
|
+
output({
|
|
636
|
+
product: { id: product.id, name: productName },
|
|
637
|
+
price: { id: price.id, amount, currency, type, interval },
|
|
638
|
+
checkout: { id: session.id, url: checkoutUrl }
|
|
639
|
+
});
|
|
640
|
+
} catch (err) {
|
|
641
|
+
console.error(`Error: ${err.message}`);
|
|
642
|
+
process.exit(1);
|
|
874
643
|
}
|
|
875
|
-
);
|
|
876
|
-
return create;
|
|
644
|
+
});
|
|
877
645
|
}
|
|
878
646
|
|
|
879
|
-
// src/commands/
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
const d = data;
|
|
898
|
-
return [
|
|
899
|
-
`Payment Link created!`,
|
|
900
|
-
``,
|
|
901
|
-
` ID: ${d.id}`,
|
|
902
|
-
d.name ? ` Name: ${d.name}` : null,
|
|
903
|
-
d.description ? ` Description: ${d.description}` : null,
|
|
904
|
-
` Amount: ${formatAmount2(d.amount, d.currency)}`,
|
|
905
|
-
` URL: ${d.url}`,
|
|
906
|
-
` Active: ${d.active}`,
|
|
907
|
-
` Created: ${formatDate(d.createdAt)}`,
|
|
908
|
-
``,
|
|
909
|
-
`Share this URL with customers - each visitor gets their own checkout session.`
|
|
910
|
-
].filter(Boolean).join("\n");
|
|
911
|
-
});
|
|
912
|
-
} catch (err) {
|
|
913
|
-
error(err instanceof Error ? err.message : "Failed to create payment link");
|
|
914
|
-
}
|
|
647
|
+
// src/commands/payout.ts
|
|
648
|
+
function registerPayoutCommands(program2) {
|
|
649
|
+
const payout = program2.command("payout").description("Manage payouts");
|
|
650
|
+
payout.command("list").description("List all payouts").action(async () => {
|
|
651
|
+
try {
|
|
652
|
+
const data = await apiRequest("GET", "/payouts");
|
|
653
|
+
output(data, (d) => {
|
|
654
|
+
const items = d.data || d.payouts || d;
|
|
655
|
+
if (!Array.isArray(items) || items.length === 0) return "No payouts found.";
|
|
656
|
+
table(
|
|
657
|
+
["ID", "Amount", "Status", "Created"],
|
|
658
|
+
items.map((p) => [p.id, fmtAmount(p.amount, p.currency), p.status, fmtDate(p.createdAt)])
|
|
659
|
+
);
|
|
660
|
+
return "";
|
|
661
|
+
});
|
|
662
|
+
} catch (err) {
|
|
663
|
+
console.error(`Error: ${err.message}`);
|
|
664
|
+
process.exit(1);
|
|
915
665
|
}
|
|
916
|
-
);
|
|
917
|
-
|
|
666
|
+
});
|
|
667
|
+
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) => {
|
|
918
668
|
try {
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
669
|
+
const body = {
|
|
670
|
+
amount: parseInt(opts.amount, 10),
|
|
671
|
+
bankAccountId: opts.bankAccountId
|
|
672
|
+
};
|
|
673
|
+
const data = await apiRequest("POST", "/payouts", body);
|
|
674
|
+
success("Payout created");
|
|
675
|
+
output(data, (d) => [
|
|
676
|
+
`Payout ID: ${d.id}`,
|
|
677
|
+
`Amount: ${fmtAmount(d.amount, d.currency)}`,
|
|
678
|
+
`Status: ${d.status}`
|
|
679
|
+
].join("\n"));
|
|
680
|
+
} catch (err) {
|
|
681
|
+
console.error(`Error: ${err.message}`);
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
payout.command("cancel <id>").description("Cancel a payout").action(async (id) => {
|
|
686
|
+
try {
|
|
687
|
+
await apiRequest("POST", `/payouts/${id}/cancel`);
|
|
688
|
+
success(`Payout ${id} cancelled`);
|
|
689
|
+
} catch (err) {
|
|
690
|
+
console.error(`Error: ${err.message}`);
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
payout.command("status").description("Check payout eligibility").action(async () => {
|
|
695
|
+
try {
|
|
696
|
+
const data = await apiRequest("GET", "/payouts/eligibility");
|
|
697
|
+
output(data, (d) => [
|
|
698
|
+
`Eligible: ${d.eligible ?? "N/A"}`,
|
|
699
|
+
`Available Balance: ${fmtAmount(d.availableBalance)}`,
|
|
700
|
+
`Minimum Payout: ${fmtAmount(d.minimumPayout)}`
|
|
701
|
+
].join("\n"));
|
|
702
|
+
} catch (err) {
|
|
703
|
+
console.error(`Error: ${err.message}`);
|
|
704
|
+
process.exit(1);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// src/commands/bank.ts
|
|
710
|
+
import { createInterface as createInterface2 } from "readline";
|
|
711
|
+
function prompt2(question) {
|
|
712
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
713
|
+
return new Promise((resolve) => {
|
|
714
|
+
rl.question(question, (answer) => {
|
|
715
|
+
rl.close();
|
|
716
|
+
resolve(answer.trim());
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
function registerBankCommands(program2) {
|
|
721
|
+
const bank = program2.command("bank").description("Manage bank accounts");
|
|
722
|
+
bank.command("list").description("List all bank accounts").action(async () => {
|
|
723
|
+
try {
|
|
724
|
+
const data = await apiRequest("GET", "/bank-accounts");
|
|
725
|
+
output(data, (d) => {
|
|
726
|
+
const items = d.data || d.bankAccounts || d;
|
|
727
|
+
if (!Array.isArray(items) || items.length === 0) return "No bank accounts found.";
|
|
728
|
+
table(
|
|
729
|
+
["ID", "Bank Name", "Last 4", "Default", "Created"],
|
|
730
|
+
items.map((b) => [
|
|
731
|
+
b.id,
|
|
732
|
+
b.bankName || "N/A",
|
|
733
|
+
b.last4 || "N/A",
|
|
734
|
+
b.isDefault ? "yes" : "no",
|
|
735
|
+
fmtDate(b.createdAt)
|
|
934
736
|
])
|
|
935
|
-
|
|
936
|
-
return
|
|
737
|
+
);
|
|
738
|
+
return "";
|
|
739
|
+
});
|
|
740
|
+
} catch (err) {
|
|
741
|
+
console.error(`Error: ${err.message}`);
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
bank.command("add").description("Add a new bank account").action(async () => {
|
|
746
|
+
try {
|
|
747
|
+
const accountHolderName = await prompt2("Account holder name: ");
|
|
748
|
+
const bankName = await prompt2("Bank name: ");
|
|
749
|
+
const routingNumber = await prompt2("Routing number: ");
|
|
750
|
+
const accountNumber = await prompt2("Account number: ");
|
|
751
|
+
const accountType = await prompt2("Account type (checking/savings, default: checking): ") || "checking";
|
|
752
|
+
const body = {
|
|
753
|
+
accountHolderName,
|
|
754
|
+
bankName,
|
|
755
|
+
routingNumber,
|
|
756
|
+
accountNumber,
|
|
757
|
+
accountType
|
|
758
|
+
};
|
|
759
|
+
const data = await apiRequest("POST", "/bank-accounts", body);
|
|
760
|
+
success("Bank account added");
|
|
761
|
+
output(data, (d) => [
|
|
762
|
+
`Bank Account ID: ${d.id}`,
|
|
763
|
+
`Bank: ${d.bankName || bankName}`,
|
|
764
|
+
`Last 4: ${d.last4 || "N/A"}`
|
|
765
|
+
].join("\n"));
|
|
766
|
+
} catch (err) {
|
|
767
|
+
console.error(`Error: ${err.message}`);
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
bank.command("remove <id>").description("Remove a bank account").action(async (id) => {
|
|
772
|
+
try {
|
|
773
|
+
await apiRequest("DELETE", `/bank-accounts/${id}`);
|
|
774
|
+
success(`Bank account ${id} removed`);
|
|
775
|
+
} catch (err) {
|
|
776
|
+
console.error(`Error: ${err.message}`);
|
|
777
|
+
process.exit(1);
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
bank.command("set-default <id>").description("Set a bank account as default").action(async (id) => {
|
|
781
|
+
try {
|
|
782
|
+
await apiRequest("PATCH", `/bank-accounts/${id}`, { isDefault: true });
|
|
783
|
+
success(`Bank account ${id} set as default`);
|
|
784
|
+
} catch (err) {
|
|
785
|
+
console.error(`Error: ${err.message}`);
|
|
786
|
+
process.exit(1);
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/commands/kyc.ts
|
|
792
|
+
import { createInterface as createInterface3 } from "readline";
|
|
793
|
+
function prompt3(question) {
|
|
794
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
795
|
+
return new Promise((resolve) => {
|
|
796
|
+
rl.question(question, (answer) => {
|
|
797
|
+
rl.close();
|
|
798
|
+
resolve(answer.trim());
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
function registerKycCommands(program2) {
|
|
803
|
+
const kyc = program2.command("kyc").description("Manage KYC verification");
|
|
804
|
+
kyc.command("submit").description("Submit KYC verification details").action(async () => {
|
|
805
|
+
try {
|
|
806
|
+
const fullName = await prompt3("Full name: ");
|
|
807
|
+
const dateOfBirth = await prompt3("Date of birth (YYYY-MM-DD): ");
|
|
808
|
+
const address = await prompt3("Street address: ");
|
|
809
|
+
const city = await prompt3("City: ");
|
|
810
|
+
const state = await prompt3("State/Province: ");
|
|
811
|
+
const postalCode = await prompt3("Postal code: ");
|
|
812
|
+
const country = await prompt3("Country (2-letter code, e.g. SG): ");
|
|
813
|
+
const body = {
|
|
814
|
+
fullName,
|
|
815
|
+
dateOfBirth,
|
|
816
|
+
address: {
|
|
817
|
+
line1: address,
|
|
818
|
+
city,
|
|
819
|
+
state,
|
|
820
|
+
postalCode,
|
|
821
|
+
country
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
const data = await apiRequest("POST", "/kyc", body);
|
|
825
|
+
success("KYC submission received");
|
|
826
|
+
output(data, (d) => {
|
|
827
|
+
return [
|
|
828
|
+
`KYC ID: ${d.id || "N/A"}`,
|
|
829
|
+
`Status: ${d.status || "pending"}`
|
|
830
|
+
].join("\n");
|
|
937
831
|
});
|
|
938
832
|
} catch (err) {
|
|
939
|
-
error(
|
|
833
|
+
console.error(`Error: ${err.message}`);
|
|
834
|
+
process.exit(1);
|
|
940
835
|
}
|
|
941
836
|
});
|
|
942
|
-
|
|
837
|
+
kyc.command("status").description("Check KYC verification status").action(async () => {
|
|
943
838
|
try {
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
output(result, (data) => {
|
|
947
|
-
const d = data;
|
|
839
|
+
const data = await apiRequest("GET", "/kyc/status");
|
|
840
|
+
output(data, (d) => {
|
|
948
841
|
return [
|
|
949
|
-
`
|
|
950
|
-
|
|
951
|
-
d.
|
|
952
|
-
|
|
953
|
-
` Amount: ${formatAmount2(d.amount, d.currency)}`,
|
|
954
|
-
` URL: ${d.url}`,
|
|
955
|
-
` Active: ${d.active}`,
|
|
956
|
-
d.successUrl ? ` Success URL: ${d.successUrl}` : null,
|
|
957
|
-
` Created: ${formatDate(d.createdAt)}`
|
|
958
|
-
].filter(Boolean).join("\n");
|
|
842
|
+
`Status: ${d.status || "N/A"}`,
|
|
843
|
+
`Verified: ${d.verified ?? "N/A"}`,
|
|
844
|
+
`Requirements: ${d.requirements ? JSON.stringify(d.requirements) : "None"}`
|
|
845
|
+
].join("\n");
|
|
959
846
|
});
|
|
960
847
|
} catch (err) {
|
|
961
|
-
error(
|
|
848
|
+
console.error(`Error: ${err.message}`);
|
|
849
|
+
process.exit(1);
|
|
962
850
|
}
|
|
963
851
|
});
|
|
964
|
-
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/commands/webhook.ts
|
|
855
|
+
function registerWebhookCommands(program2) {
|
|
856
|
+
const webhook = program2.command("webhook").description("Manage webhook configuration");
|
|
857
|
+
webhook.command("set <url>").description("Set webhook URL for your wallet (must be HTTPS)").action(async (url) => {
|
|
965
858
|
try {
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
859
|
+
if (!url.startsWith("https://")) {
|
|
860
|
+
console.error("Error: webhook URL must start with https://");
|
|
861
|
+
process.exit(1);
|
|
862
|
+
}
|
|
863
|
+
const data = await apiRequest("PATCH", "/wallets/current", {
|
|
864
|
+
webhookUrl: url
|
|
971
865
|
});
|
|
866
|
+
success("Webhook configured");
|
|
867
|
+
console.log(` URL: ${data.webhookUrl}`);
|
|
868
|
+
if (data.webhookSecret) {
|
|
869
|
+
console.log(` Secret: ${data.webhookSecret}`);
|
|
870
|
+
console.log("\nSave the secret \u2014 it will not be shown again.");
|
|
871
|
+
console.log("Use client.webhooks.constructEvent(body, signature, secret) in your server.");
|
|
872
|
+
}
|
|
972
873
|
} catch (err) {
|
|
973
|
-
error(
|
|
874
|
+
console.error(`Error: ${err.message}`);
|
|
875
|
+
process.exit(1);
|
|
974
876
|
}
|
|
975
877
|
});
|
|
976
|
-
|
|
878
|
+
webhook.command("info").description("Show current webhook configuration").action(async () => {
|
|
977
879
|
try {
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
880
|
+
const data = await apiRequest("GET", "/wallets/current");
|
|
881
|
+
if (data.webhookUrl) {
|
|
882
|
+
console.log(`Webhook URL: ${data.webhookUrl}`);
|
|
883
|
+
console.log("Status: configured");
|
|
884
|
+
} else {
|
|
885
|
+
console.log("No webhook configured.");
|
|
886
|
+
console.log("Run: vibecash webhook set <url>");
|
|
887
|
+
}
|
|
888
|
+
} catch (err) {
|
|
889
|
+
console.error(`Error: ${err.message}`);
|
|
890
|
+
process.exit(1);
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
webhook.command("remove").description("Remove webhook configuration").action(async () => {
|
|
894
|
+
try {
|
|
895
|
+
await apiRequest("PATCH", "/wallets/current", {
|
|
896
|
+
webhookUrl: null
|
|
983
897
|
});
|
|
898
|
+
success("Webhook removed");
|
|
984
899
|
} catch (err) {
|
|
985
|
-
error(
|
|
900
|
+
console.error(`Error: ${err.message}`);
|
|
901
|
+
process.exit(1);
|
|
986
902
|
}
|
|
987
903
|
});
|
|
988
|
-
|
|
904
|
+
webhook.command("roll-secret").description("Generate a new webhook signing secret (invalidates the old one)").action(async () => {
|
|
905
|
+
try {
|
|
906
|
+
const wallet = await apiRequest("GET", "/wallets/current");
|
|
907
|
+
if (!wallet.webhookUrl) {
|
|
908
|
+
console.error("No webhook configured. Run: vibecash webhook set <url>");
|
|
909
|
+
process.exit(1);
|
|
910
|
+
}
|
|
911
|
+
const data = await apiRequest("PATCH", "/wallets/current", {
|
|
912
|
+
webhookUrl: wallet.webhookUrl
|
|
913
|
+
});
|
|
914
|
+
success("New secret generated");
|
|
915
|
+
console.log(` URL: ${data.webhookUrl}`);
|
|
916
|
+
if (data.webhookSecret) {
|
|
917
|
+
console.log(` Secret: ${data.webhookSecret}`);
|
|
918
|
+
console.log("\nSave the new secret \u2014 it will not be shown again.");
|
|
919
|
+
console.log("The old secret is now invalid.");
|
|
920
|
+
}
|
|
921
|
+
} catch (err) {
|
|
922
|
+
console.error(`Error: ${err.message}`);
|
|
923
|
+
process.exit(1);
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
webhook.command("events").description("Webhook event types reference").action(() => {
|
|
927
|
+
console.log("VibeCash Webhook Event Types\n");
|
|
928
|
+
console.log("Payment Events:");
|
|
929
|
+
console.log(" payment.succeeded \u2014 Customer payment completed successfully");
|
|
930
|
+
console.log(" payment.failed \u2014 Customer payment failed\n");
|
|
931
|
+
console.log("Subscription Events:");
|
|
932
|
+
console.log(" subscription.renewed \u2014 Subscription auto-renewed");
|
|
933
|
+
console.log(" subscription.past_due \u2014 Subscription payment overdue");
|
|
934
|
+
console.log(" subscription.unpaid \u2014 Subscription marked unpaid");
|
|
935
|
+
console.log(" subscription.trial_ending \u2014 Trial period ending soon\n");
|
|
936
|
+
console.log("Checkout Events:");
|
|
937
|
+
console.log(" checkout.session.expired \u2014 Checkout session expired\n");
|
|
938
|
+
console.log("Payload Format:");
|
|
939
|
+
console.log(" {");
|
|
940
|
+
console.log(' "id": "evt_xxx",');
|
|
941
|
+
console.log(' "type": "payment.succeeded",');
|
|
942
|
+
console.log(' "created": 1710000000,');
|
|
943
|
+
console.log(' "data": { "object": { ...full resource... } }');
|
|
944
|
+
console.log(" }\n");
|
|
945
|
+
console.log("Signature Header:");
|
|
946
|
+
console.log(" VibeCash-Signature: t={timestamp},v1={hmac-sha256-hex}");
|
|
947
|
+
});
|
|
989
948
|
}
|
|
990
949
|
|
|
991
950
|
// src/index.ts
|
|
992
|
-
var program = new
|
|
993
|
-
program.name("vibecash").description("VibeCash -
|
|
994
|
-
program.addCommand(createWalletCommand());
|
|
995
|
-
program.addCommand(createProductCommand());
|
|
996
|
-
program.addCommand(createPriceCommand());
|
|
997
|
-
program.addCommand(createCheckoutCommand());
|
|
998
|
-
program.addCommand(createSubscriptionCommand());
|
|
999
|
-
program.addCommand(createCustomerCommand());
|
|
1000
|
-
program.addCommand(createQuickCreateCommand());
|
|
1001
|
-
program.addCommand(createLinkCommand());
|
|
1002
|
-
program.option("--human", "Output in human-readable format");
|
|
1003
|
-
program.option("--json", "Output in JSON format (default)");
|
|
1004
|
-
program.hook("preAction", (thisCommand) => {
|
|
951
|
+
var program = new Command();
|
|
952
|
+
program.name("vibecash").description("VibeCash CLI - manage payments from the command line").version("0.2.1").option("--json", "Output in JSON format").hook("preAction", (thisCommand) => {
|
|
1005
953
|
const opts = thisCommand.opts();
|
|
1006
|
-
if (opts.
|
|
1007
|
-
process.env.AIPAY_OUTPUT = "human";
|
|
1008
|
-
} else if (opts.json) {
|
|
1009
|
-
process.env.AIPAY_OUTPUT = "json";
|
|
1010
|
-
}
|
|
954
|
+
if (opts.json) setOutputFormat("json");
|
|
1011
955
|
});
|
|
956
|
+
registerWalletCommands(program);
|
|
957
|
+
registerProductCommands(program);
|
|
958
|
+
registerPriceCommands(program);
|
|
959
|
+
registerCheckoutCommands(program);
|
|
960
|
+
registerSubscriptionCommands(program);
|
|
961
|
+
registerCustomerCommands(program);
|
|
962
|
+
registerLinkCommands(program);
|
|
963
|
+
registerCreateCommand(program);
|
|
964
|
+
registerPayoutCommands(program);
|
|
965
|
+
registerBankCommands(program);
|
|
966
|
+
registerKycCommands(program);
|
|
967
|
+
registerWebhookCommands(program);
|
|
1012
968
|
program.parse();
|