@usetransactional/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.
- package/README.md +161 -0
- package/dist/bin.js +1567 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.js +1698 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,1567 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path2 from 'path';
|
|
5
|
+
import * as os2 from 'os';
|
|
6
|
+
import open from 'open';
|
|
7
|
+
import ora2 from 'ora';
|
|
8
|
+
import chalk4 from 'chalk';
|
|
9
|
+
import Table from 'cli-table3';
|
|
10
|
+
import yaml from 'yaml';
|
|
11
|
+
import inquirer from 'inquirer';
|
|
12
|
+
|
|
13
|
+
var CONFIG_DIR = path2.join(os2.homedir(), ".transactional");
|
|
14
|
+
var CONFIG_FILE = path2.join(CONFIG_DIR, "config.json");
|
|
15
|
+
var CREDENTIALS_FILE = path2.join(CONFIG_DIR, "credentials.json");
|
|
16
|
+
var DEFAULT_CONFIG = {
|
|
17
|
+
apiUrl: "https://api.usetransactional.com",
|
|
18
|
+
webUrl: "https://usetransactional.com",
|
|
19
|
+
outputFormat: "table",
|
|
20
|
+
color: true
|
|
21
|
+
};
|
|
22
|
+
var currentConfig = { ...DEFAULT_CONFIG };
|
|
23
|
+
var currentCredentials = null;
|
|
24
|
+
function ensureConfigDir() {
|
|
25
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
26
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function loadConfig() {
|
|
30
|
+
ensureConfigDir();
|
|
31
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
32
|
+
try {
|
|
33
|
+
const content = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
34
|
+
const loaded = JSON.parse(content);
|
|
35
|
+
currentConfig = { ...DEFAULT_CONFIG, ...loaded };
|
|
36
|
+
} catch {
|
|
37
|
+
currentConfig = { ...DEFAULT_CONFIG };
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
currentConfig = { ...DEFAULT_CONFIG };
|
|
41
|
+
}
|
|
42
|
+
if (process.env.TRANSACTIONAL_API_URL) {
|
|
43
|
+
currentConfig.apiUrl = process.env.TRANSACTIONAL_API_URL;
|
|
44
|
+
}
|
|
45
|
+
if (process.env.TRANSACTIONAL_WEB_URL) {
|
|
46
|
+
currentConfig.webUrl = process.env.TRANSACTIONAL_WEB_URL;
|
|
47
|
+
}
|
|
48
|
+
if (process.env.NO_COLOR || process.env.TRANSACTIONAL_NO_COLOR) {
|
|
49
|
+
currentConfig.color = false;
|
|
50
|
+
}
|
|
51
|
+
return currentConfig;
|
|
52
|
+
}
|
|
53
|
+
function saveConfig(config) {
|
|
54
|
+
ensureConfigDir();
|
|
55
|
+
currentConfig = { ...currentConfig, ...config };
|
|
56
|
+
const { apiUrl, webUrl, outputFormat, color } = currentConfig;
|
|
57
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ apiUrl, webUrl, outputFormat, color }, null, 2), {
|
|
58
|
+
mode: 384
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function getConfig() {
|
|
62
|
+
return currentConfig;
|
|
63
|
+
}
|
|
64
|
+
function getApiUrl() {
|
|
65
|
+
return currentConfig.apiUrl;
|
|
66
|
+
}
|
|
67
|
+
function getWebUrl() {
|
|
68
|
+
return currentConfig.webUrl;
|
|
69
|
+
}
|
|
70
|
+
function getOutputFormat() {
|
|
71
|
+
return currentConfig.outputFormat;
|
|
72
|
+
}
|
|
73
|
+
function isColorEnabled() {
|
|
74
|
+
return currentConfig.color;
|
|
75
|
+
}
|
|
76
|
+
function loadCredentials() {
|
|
77
|
+
ensureConfigDir();
|
|
78
|
+
if (currentCredentials) {
|
|
79
|
+
return currentCredentials;
|
|
80
|
+
}
|
|
81
|
+
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
82
|
+
try {
|
|
83
|
+
const content = fs.readFileSync(CREDENTIALS_FILE, "utf-8");
|
|
84
|
+
const loaded = JSON.parse(content);
|
|
85
|
+
if (loaded.version === 1) {
|
|
86
|
+
currentCredentials = {
|
|
87
|
+
version: 2,
|
|
88
|
+
user: loaded.user,
|
|
89
|
+
currentOrganization: loaded.currentOrganization
|
|
90
|
+
};
|
|
91
|
+
saveCredentials(currentCredentials);
|
|
92
|
+
} else {
|
|
93
|
+
currentCredentials = loaded;
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
currentCredentials = {
|
|
97
|
+
version: 2
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
currentCredentials = {
|
|
102
|
+
version: 2
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return currentCredentials;
|
|
106
|
+
}
|
|
107
|
+
function saveCredentials(credentials) {
|
|
108
|
+
ensureConfigDir();
|
|
109
|
+
currentCredentials = credentials;
|
|
110
|
+
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), {
|
|
111
|
+
mode: 384
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function getCurrentOrganization() {
|
|
115
|
+
const credentials = loadCredentials();
|
|
116
|
+
return credentials.currentOrganization;
|
|
117
|
+
}
|
|
118
|
+
function setCurrentOrganization(orgSlug) {
|
|
119
|
+
const credentials = loadCredentials();
|
|
120
|
+
credentials.currentOrganization = orgSlug;
|
|
121
|
+
saveCredentials(credentials);
|
|
122
|
+
}
|
|
123
|
+
function getToken() {
|
|
124
|
+
const credentials = loadCredentials();
|
|
125
|
+
if (!credentials.token) {
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
if (credentials.expiresAt && new Date(credentials.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
129
|
+
return void 0;
|
|
130
|
+
}
|
|
131
|
+
return credentials.token;
|
|
132
|
+
}
|
|
133
|
+
function storeToken(token, expiresInSeconds) {
|
|
134
|
+
const credentials = loadCredentials();
|
|
135
|
+
credentials.token = token;
|
|
136
|
+
credentials.expiresAt = new Date(Date.now() + expiresInSeconds * 1e3).toISOString();
|
|
137
|
+
saveCredentials(credentials);
|
|
138
|
+
}
|
|
139
|
+
function isLoggedIn() {
|
|
140
|
+
return !!getToken();
|
|
141
|
+
}
|
|
142
|
+
function clearCredentials() {
|
|
143
|
+
ensureConfigDir();
|
|
144
|
+
currentCredentials = {
|
|
145
|
+
version: 2
|
|
146
|
+
};
|
|
147
|
+
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
148
|
+
fs.unlinkSync(CREDENTIALS_FILE);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function storeUserInfo(user) {
|
|
152
|
+
const credentials = loadCredentials();
|
|
153
|
+
credentials.user = user;
|
|
154
|
+
saveCredentials(credentials);
|
|
155
|
+
}
|
|
156
|
+
function switchOrganization(orgSlug) {
|
|
157
|
+
setCurrentOrganization(orgSlug);
|
|
158
|
+
}
|
|
159
|
+
function getConfigDir() {
|
|
160
|
+
return CONFIG_DIR;
|
|
161
|
+
}
|
|
162
|
+
function getCredentialsFile() {
|
|
163
|
+
return CREDENTIALS_FILE;
|
|
164
|
+
}
|
|
165
|
+
function initConfig() {
|
|
166
|
+
return loadConfig();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/lib/client.ts
|
|
170
|
+
var ApiClient = class {
|
|
171
|
+
apiUrl;
|
|
172
|
+
token;
|
|
173
|
+
orgSlug;
|
|
174
|
+
constructor(orgSlug) {
|
|
175
|
+
this.apiUrl = getApiUrl();
|
|
176
|
+
this.token = getToken();
|
|
177
|
+
this.orgSlug = orgSlug || getCurrentOrganization();
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get headers for API requests
|
|
181
|
+
*/
|
|
182
|
+
getHeaders() {
|
|
183
|
+
const headers = {
|
|
184
|
+
"Content-Type": "application/json",
|
|
185
|
+
"User-Agent": "transactional-cli/0.1.0"
|
|
186
|
+
};
|
|
187
|
+
if (this.token) {
|
|
188
|
+
headers["Authorization"] = `Bearer ${this.token}`;
|
|
189
|
+
}
|
|
190
|
+
if (this.orgSlug) {
|
|
191
|
+
headers["X-Organization-Slug"] = this.orgSlug;
|
|
192
|
+
}
|
|
193
|
+
return headers;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Build URL with query parameters
|
|
197
|
+
*/
|
|
198
|
+
buildUrl(path3, params) {
|
|
199
|
+
const url = new URL(path3, this.apiUrl);
|
|
200
|
+
if (params) {
|
|
201
|
+
for (const [key, value] of Object.entries(params)) {
|
|
202
|
+
if (value !== void 0 && value !== null) {
|
|
203
|
+
url.searchParams.set(key, String(value));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return url.toString();
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Make an HTTP request
|
|
211
|
+
*/
|
|
212
|
+
async request(method, path3, options) {
|
|
213
|
+
try {
|
|
214
|
+
const url = this.buildUrl(path3, options?.params);
|
|
215
|
+
const init = {
|
|
216
|
+
method,
|
|
217
|
+
headers: this.getHeaders()
|
|
218
|
+
};
|
|
219
|
+
if (options?.body) {
|
|
220
|
+
init.body = JSON.stringify(options.body);
|
|
221
|
+
}
|
|
222
|
+
const response = await fetch(url, init);
|
|
223
|
+
if (response.status === 204) {
|
|
224
|
+
return { success: true };
|
|
225
|
+
}
|
|
226
|
+
let data;
|
|
227
|
+
const contentType = response.headers.get("content-type");
|
|
228
|
+
if (contentType?.includes("application/json")) {
|
|
229
|
+
data = await response.json();
|
|
230
|
+
} else {
|
|
231
|
+
data = await response.text();
|
|
232
|
+
}
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
const errorResponse = data;
|
|
235
|
+
return {
|
|
236
|
+
success: false,
|
|
237
|
+
error: {
|
|
238
|
+
code: errorResponse.error?.code || `HTTP_${response.status}`,
|
|
239
|
+
message: errorResponse.error?.message || response.statusText,
|
|
240
|
+
details: errorResponse.error?.details
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (data && typeof data === "object" && "data" in data) {
|
|
245
|
+
return { success: true, data: data.data };
|
|
246
|
+
}
|
|
247
|
+
return { success: true, data };
|
|
248
|
+
} catch (err) {
|
|
249
|
+
if (err instanceof Error) {
|
|
250
|
+
if (err.message.includes("ECONNREFUSED")) {
|
|
251
|
+
return {
|
|
252
|
+
success: false,
|
|
253
|
+
error: {
|
|
254
|
+
code: "CONNECTION_REFUSED",
|
|
255
|
+
message: `Cannot connect to API server. Check your network connection.`
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
success: false,
|
|
261
|
+
error: {
|
|
262
|
+
code: "NETWORK_ERROR",
|
|
263
|
+
message: err.message
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
success: false,
|
|
269
|
+
error: {
|
|
270
|
+
code: "UNKNOWN_ERROR",
|
|
271
|
+
message: "An unknown error occurred"
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* GET request
|
|
278
|
+
*/
|
|
279
|
+
async get(path3, params) {
|
|
280
|
+
return this.request("GET", path3, { params });
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* POST request
|
|
284
|
+
*/
|
|
285
|
+
async post(path3, body) {
|
|
286
|
+
return this.request("POST", path3, { body });
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* PUT request
|
|
290
|
+
*/
|
|
291
|
+
async put(path3, body) {
|
|
292
|
+
return this.request("PUT", path3, { body });
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* PATCH request
|
|
296
|
+
*/
|
|
297
|
+
async patch(path3, body) {
|
|
298
|
+
return this.request("PATCH", path3, { body });
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* DELETE request
|
|
302
|
+
*/
|
|
303
|
+
async delete(path3) {
|
|
304
|
+
return this.request("DELETE", path3);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
function getApiClient(orgSlug) {
|
|
308
|
+
return new ApiClient(orgSlug);
|
|
309
|
+
}
|
|
310
|
+
async function requestDeviceCode(sessionType = "CLI") {
|
|
311
|
+
const apiUrl = getApiUrl();
|
|
312
|
+
const response = await fetch(`${apiUrl}/cli/device-code`, {
|
|
313
|
+
method: "POST",
|
|
314
|
+
headers: {
|
|
315
|
+
"Content-Type": "application/json",
|
|
316
|
+
"User-Agent": "transactional-cli/0.1.0"
|
|
317
|
+
},
|
|
318
|
+
body: JSON.stringify({
|
|
319
|
+
sessionType,
|
|
320
|
+
clientInfo: {
|
|
321
|
+
deviceName: process.env.HOSTNAME || "Unknown",
|
|
322
|
+
osName: process.platform,
|
|
323
|
+
osVersion: process.version,
|
|
324
|
+
hostname: process.env.HOSTNAME || "Unknown",
|
|
325
|
+
clientVersion: "0.1.0"
|
|
326
|
+
}
|
|
327
|
+
})
|
|
328
|
+
});
|
|
329
|
+
if (!response.ok) {
|
|
330
|
+
const errorData = await response.json().catch(() => ({}));
|
|
331
|
+
const errorMsg = typeof errorData.error === "string" ? errorData.error : errorData.error?.message || "Failed to get device code";
|
|
332
|
+
throw new Error(errorMsg);
|
|
333
|
+
}
|
|
334
|
+
return response.json();
|
|
335
|
+
}
|
|
336
|
+
async function pollForToken(deviceCode, interval, expiresIn, onPoll) {
|
|
337
|
+
const apiUrl = getApiUrl();
|
|
338
|
+
const startTime = Date.now();
|
|
339
|
+
const expiresAt = startTime + expiresIn * 1e3;
|
|
340
|
+
while (Date.now() < expiresAt) {
|
|
341
|
+
if (onPoll) onPoll();
|
|
342
|
+
const response = await fetch(`${apiUrl}/cli/token`, {
|
|
343
|
+
method: "POST",
|
|
344
|
+
headers: {
|
|
345
|
+
"Content-Type": "application/json",
|
|
346
|
+
"User-Agent": "transactional-cli/0.1.0"
|
|
347
|
+
},
|
|
348
|
+
body: JSON.stringify({ deviceCode })
|
|
349
|
+
});
|
|
350
|
+
if (response.ok) {
|
|
351
|
+
return response.json();
|
|
352
|
+
}
|
|
353
|
+
const errorData = await response.json();
|
|
354
|
+
switch (errorData.error) {
|
|
355
|
+
case "authorization_pending":
|
|
356
|
+
break;
|
|
357
|
+
case "slow_down":
|
|
358
|
+
interval = Math.min(interval + 5, 60);
|
|
359
|
+
break;
|
|
360
|
+
case "expired_token":
|
|
361
|
+
throw new Error("Login timed out. Please try again.");
|
|
362
|
+
case "access_denied":
|
|
363
|
+
throw new Error("Authorization denied.");
|
|
364
|
+
default:
|
|
365
|
+
throw new Error(errorData.error_description || "Login failed");
|
|
366
|
+
}
|
|
367
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1e3));
|
|
368
|
+
}
|
|
369
|
+
throw new Error("Login timed out. Please try again.");
|
|
370
|
+
}
|
|
371
|
+
async function login(sessionType = "CLI", callbacks) {
|
|
372
|
+
try {
|
|
373
|
+
const deviceCodeResponse = await requestDeviceCode(sessionType);
|
|
374
|
+
const { deviceCode, userCode, expiresIn, interval } = deviceCodeResponse;
|
|
375
|
+
const webUrl = getWebUrl();
|
|
376
|
+
const verificationUrl = `${webUrl}/cli/authorize?user_code=${userCode}`;
|
|
377
|
+
if (callbacks?.onDeviceCode) {
|
|
378
|
+
callbacks.onDeviceCode(userCode, verificationUrl);
|
|
379
|
+
}
|
|
380
|
+
if (callbacks?.onBrowserOpen) {
|
|
381
|
+
callbacks.onBrowserOpen();
|
|
382
|
+
}
|
|
383
|
+
await open(verificationUrl);
|
|
384
|
+
const tokenResponse = await pollForToken(
|
|
385
|
+
deviceCode,
|
|
386
|
+
interval,
|
|
387
|
+
expiresIn,
|
|
388
|
+
callbacks?.onPolling
|
|
389
|
+
);
|
|
390
|
+
storeToken(tokenResponse.token, tokenResponse.expiresIn);
|
|
391
|
+
storeUserInfo({
|
|
392
|
+
id: tokenResponse.user.id,
|
|
393
|
+
email: tokenResponse.user.email,
|
|
394
|
+
name: tokenResponse.user.name
|
|
395
|
+
});
|
|
396
|
+
return {
|
|
397
|
+
success: true,
|
|
398
|
+
data: {
|
|
399
|
+
user: tokenResponse.user,
|
|
400
|
+
organizations: tokenResponse.organizations
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
} catch (err) {
|
|
404
|
+
return {
|
|
405
|
+
success: false,
|
|
406
|
+
error: {
|
|
407
|
+
code: "LOGIN_FAILED",
|
|
408
|
+
message: err instanceof Error ? err.message : "Login failed"
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function logout() {
|
|
414
|
+
clearCredentials();
|
|
415
|
+
}
|
|
416
|
+
async function whoami(orgSlug) {
|
|
417
|
+
const client = getApiClient(orgSlug);
|
|
418
|
+
return client.get("/cli/whoami");
|
|
419
|
+
}
|
|
420
|
+
async function listOrganizations() {
|
|
421
|
+
const client = getApiClient();
|
|
422
|
+
return client.get("/cli/organizations");
|
|
423
|
+
}
|
|
424
|
+
function formatOutput(data, format) {
|
|
425
|
+
const outputFormat = format || getOutputFormat();
|
|
426
|
+
switch (outputFormat) {
|
|
427
|
+
case "json":
|
|
428
|
+
return JSON.stringify(data, null, 2);
|
|
429
|
+
case "yaml":
|
|
430
|
+
return yaml.stringify(data);
|
|
431
|
+
case "table":
|
|
432
|
+
default:
|
|
433
|
+
return formatAsTable(data);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function formatAsTable(data) {
|
|
437
|
+
if (!data) {
|
|
438
|
+
return "No data";
|
|
439
|
+
}
|
|
440
|
+
if (Array.isArray(data)) {
|
|
441
|
+
if (data.length === 0) {
|
|
442
|
+
return "No results";
|
|
443
|
+
}
|
|
444
|
+
const columns = Object.keys(data[0]);
|
|
445
|
+
const table = new Table({
|
|
446
|
+
head: columns.map((c) => isColorEnabled() ? chalk4.bold(c) : c),
|
|
447
|
+
style: {
|
|
448
|
+
head: isColorEnabled() ? ["cyan"] : [],
|
|
449
|
+
border: isColorEnabled() ? ["gray"] : []
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
for (const row of data) {
|
|
453
|
+
table.push(columns.map((col) => formatValue(row[col])));
|
|
454
|
+
}
|
|
455
|
+
return table.toString();
|
|
456
|
+
}
|
|
457
|
+
if (typeof data === "object") {
|
|
458
|
+
const table = new Table({
|
|
459
|
+
style: {
|
|
460
|
+
border: isColorEnabled() ? ["gray"] : []
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
for (const [key, value] of Object.entries(data)) {
|
|
464
|
+
const formattedKey = isColorEnabled() ? chalk4.bold(key) : key;
|
|
465
|
+
table.push({ [formattedKey]: formatValue(value) });
|
|
466
|
+
}
|
|
467
|
+
return table.toString();
|
|
468
|
+
}
|
|
469
|
+
return String(data);
|
|
470
|
+
}
|
|
471
|
+
function formatValue(value) {
|
|
472
|
+
if (value === null || value === void 0) {
|
|
473
|
+
return isColorEnabled() ? chalk4.gray("-") : "-";
|
|
474
|
+
}
|
|
475
|
+
if (typeof value === "boolean") {
|
|
476
|
+
if (isColorEnabled()) {
|
|
477
|
+
return value ? chalk4.green("Yes") : chalk4.red("No");
|
|
478
|
+
}
|
|
479
|
+
return value ? "Yes" : "No";
|
|
480
|
+
}
|
|
481
|
+
if (value instanceof Date) {
|
|
482
|
+
return value.toISOString();
|
|
483
|
+
}
|
|
484
|
+
if (Array.isArray(value)) {
|
|
485
|
+
return value.join(", ");
|
|
486
|
+
}
|
|
487
|
+
if (typeof value === "object") {
|
|
488
|
+
return JSON.stringify(value);
|
|
489
|
+
}
|
|
490
|
+
return String(value);
|
|
491
|
+
}
|
|
492
|
+
function printSuccess(message) {
|
|
493
|
+
if (isColorEnabled()) {
|
|
494
|
+
console.log(chalk4.green("\u2713"), message);
|
|
495
|
+
} else {
|
|
496
|
+
console.log("[OK]", message);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function printError(message, details) {
|
|
500
|
+
if (isColorEnabled()) {
|
|
501
|
+
console.error(chalk4.red("\u2717"), message);
|
|
502
|
+
} else {
|
|
503
|
+
console.error("[ERROR]", message);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function printKeyValue(key, value) {
|
|
507
|
+
const formattedValue = formatValue(value);
|
|
508
|
+
if (isColorEnabled()) {
|
|
509
|
+
console.log(`${chalk4.gray(key + ":")} ${formattedValue}`);
|
|
510
|
+
} else {
|
|
511
|
+
console.log(`${key}: ${formattedValue}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function print(data, format) {
|
|
515
|
+
console.log(formatOutput(data, format));
|
|
516
|
+
}
|
|
517
|
+
async function selectOrganization(organizations) {
|
|
518
|
+
const { orgSlug } = await inquirer.prompt([
|
|
519
|
+
{
|
|
520
|
+
type: "list",
|
|
521
|
+
name: "orgSlug",
|
|
522
|
+
message: "Select an organization:",
|
|
523
|
+
choices: organizations.map((org) => ({
|
|
524
|
+
name: `${org.name} (${org.slug}) - ${org.role}`,
|
|
525
|
+
value: org.slug
|
|
526
|
+
}))
|
|
527
|
+
}
|
|
528
|
+
]);
|
|
529
|
+
return orgSlug;
|
|
530
|
+
}
|
|
531
|
+
async function confirm(message, defaultValue = false) {
|
|
532
|
+
const { confirmed } = await inquirer.prompt([
|
|
533
|
+
{
|
|
534
|
+
type: "confirm",
|
|
535
|
+
name: "confirmed",
|
|
536
|
+
message,
|
|
537
|
+
default: defaultValue
|
|
538
|
+
}
|
|
539
|
+
]);
|
|
540
|
+
return confirmed;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/commands/auth.ts
|
|
544
|
+
function createLoginCommand() {
|
|
545
|
+
return new Command("login").description("Authenticate with Transactional").option("--mcp", "Login for MCP server use").option("-f, --force", "Force new login even if already logged in").action(async (options) => {
|
|
546
|
+
if (isLoggedIn() && !options.force) {
|
|
547
|
+
const currentOrg = getCurrentOrganization();
|
|
548
|
+
if (currentOrg) {
|
|
549
|
+
printInfo(`Already logged in. Current organization: ${currentOrg}`);
|
|
550
|
+
} else {
|
|
551
|
+
printInfo('Already logged in. Use "transactional org use <slug>" to select an organization.');
|
|
552
|
+
}
|
|
553
|
+
printInfo('Use "transactional login --force" to get a new token.');
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const spinner = ora2();
|
|
557
|
+
const result = await login(options.mcp ? "MCP" : "CLI", {
|
|
558
|
+
onDeviceCode: (userCode, verificationUrl) => {
|
|
559
|
+
console.log();
|
|
560
|
+
console.log(chalk4.bold("To complete authentication:"));
|
|
561
|
+
console.log();
|
|
562
|
+
console.log(` 1. Visit: ${chalk4.cyan(verificationUrl)}`);
|
|
563
|
+
console.log(` 2. Verify this code matches: ${chalk4.bold.yellow(formatUserCode(userCode))}`);
|
|
564
|
+
console.log(` 3. Click "Authorize" in your browser`);
|
|
565
|
+
console.log();
|
|
566
|
+
},
|
|
567
|
+
onBrowserOpen: () => {
|
|
568
|
+
spinner.start("Opening browser...");
|
|
569
|
+
spinner.succeed("Browser opened");
|
|
570
|
+
spinner.start("Waiting for authorization...");
|
|
571
|
+
},
|
|
572
|
+
onPolling: () => {
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
if (!result.success || !result.data) {
|
|
576
|
+
spinner.fail("Login failed");
|
|
577
|
+
printError(result.error?.message || "Unknown error");
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
spinner.succeed("Authorization received!");
|
|
581
|
+
console.log();
|
|
582
|
+
printSuccess("Login successful!");
|
|
583
|
+
console.log();
|
|
584
|
+
printKeyValue("User", result.data.user.email);
|
|
585
|
+
if (result.data.organizations.length > 0) {
|
|
586
|
+
console.log();
|
|
587
|
+
printInfo(`You have access to ${result.data.organizations.length} organization(s).`);
|
|
588
|
+
printInfo('Use "transactional org list" to see them, or "transactional org use <slug>" to select one.');
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
function formatUserCode(code) {
|
|
593
|
+
if (code.length === 8) {
|
|
594
|
+
return `${code.slice(0, 4)}-${code.slice(4)}`;
|
|
595
|
+
}
|
|
596
|
+
return code;
|
|
597
|
+
}
|
|
598
|
+
function createLogoutCommand() {
|
|
599
|
+
return new Command("logout").description("Log out from all organizations").action(async () => {
|
|
600
|
+
if (!isLoggedIn()) {
|
|
601
|
+
printInfo("You are not logged in.");
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
logout();
|
|
605
|
+
printSuccess("Logged out from all organizations.");
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
function createWhoamiCommand() {
|
|
609
|
+
return new Command("whoami").description("Show current user and organization info").option("-o, --org <slug>", "Organization slug").option("--json", "Output as JSON").action(async (options) => {
|
|
610
|
+
if (!isLoggedIn()) {
|
|
611
|
+
printError('Not logged in. Use "transactional login" to authenticate.');
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
const result = await whoami(options.org);
|
|
615
|
+
if (!result.success || !result.data) {
|
|
616
|
+
printError(result.error?.message || "Failed to get user info");
|
|
617
|
+
process.exit(1);
|
|
618
|
+
}
|
|
619
|
+
if (options.json) {
|
|
620
|
+
print(result.data, "json");
|
|
621
|
+
} else {
|
|
622
|
+
const { user, organization, session } = result.data;
|
|
623
|
+
console.log();
|
|
624
|
+
printHeading("User");
|
|
625
|
+
printKeyValue("ID", user.id);
|
|
626
|
+
printKeyValue("Email", user.email);
|
|
627
|
+
if (user.name) printKeyValue("Name", user.name);
|
|
628
|
+
console.log();
|
|
629
|
+
printHeading("Organization");
|
|
630
|
+
if (organization) {
|
|
631
|
+
printKeyValue("ID", String(organization.id));
|
|
632
|
+
printKeyValue("Name", organization.name);
|
|
633
|
+
printKeyValue("Slug", organization.slug);
|
|
634
|
+
printKeyValue("Role", organization.role);
|
|
635
|
+
} else {
|
|
636
|
+
printInfo('No organization selected. Use "transactional org use <slug>" to select one.');
|
|
637
|
+
}
|
|
638
|
+
console.log();
|
|
639
|
+
printHeading("Session");
|
|
640
|
+
printKeyValue("ID", String(session.id));
|
|
641
|
+
printKeyValue("Type", session.type);
|
|
642
|
+
printKeyValue("Created", session.createdAt);
|
|
643
|
+
if (session.expiresAt) printKeyValue("Expires", session.expiresAt);
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
function createSwitchCommand() {
|
|
648
|
+
return new Command("switch").description('Switch to a different organization (alias for "org use")').argument("[slug]", "Organization slug to switch to").action(async (slug) => {
|
|
649
|
+
if (!isLoggedIn()) {
|
|
650
|
+
printError('Not logged in. Use "transactional login" to authenticate.');
|
|
651
|
+
process.exit(1);
|
|
652
|
+
}
|
|
653
|
+
const spinner = ora2("Fetching organizations...").start();
|
|
654
|
+
const orgsResult = await listOrganizations();
|
|
655
|
+
if (!orgsResult.success || !orgsResult.data) {
|
|
656
|
+
spinner.fail("Failed to fetch organizations");
|
|
657
|
+
printError(orgsResult.error?.message || "Unknown error");
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
const orgs = orgsResult.data;
|
|
661
|
+
spinner.stop();
|
|
662
|
+
if (orgs.length === 0) {
|
|
663
|
+
printError("No organizations found.");
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
let targetSlug = slug;
|
|
667
|
+
if (!targetSlug) {
|
|
668
|
+
if (orgs.length === 1) {
|
|
669
|
+
targetSlug = orgs[0].slug;
|
|
670
|
+
printInfo(`Only one organization available: ${targetSlug}`);
|
|
671
|
+
} else {
|
|
672
|
+
targetSlug = await selectOrganization(orgs);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const validOrg = orgs.find((o) => o.slug === targetSlug);
|
|
676
|
+
if (!validOrg) {
|
|
677
|
+
printError(`Organization "${targetSlug}" not found.`);
|
|
678
|
+
printInfo("Available organizations: " + orgs.map((o) => o.slug).join(", "));
|
|
679
|
+
process.exit(1);
|
|
680
|
+
}
|
|
681
|
+
switchOrganization(targetSlug);
|
|
682
|
+
printSuccess(`Switched to organization: ${validOrg.name} (${targetSlug})`);
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
function createOrgsCommand() {
|
|
686
|
+
const orgCmd = new Command("org").description("Manage organizations");
|
|
687
|
+
orgCmd.command("list").description("List all organizations you have access to").option("--json", "Output as JSON").action(async (options) => {
|
|
688
|
+
if (!isLoggedIn()) {
|
|
689
|
+
printError('Not logged in. Use "transactional login" to authenticate.');
|
|
690
|
+
process.exit(1);
|
|
691
|
+
}
|
|
692
|
+
const spinner = ora2("Fetching organizations...").start();
|
|
693
|
+
const result = await listOrganizations();
|
|
694
|
+
if (!result.success || !result.data) {
|
|
695
|
+
spinner.fail("Failed to fetch organizations");
|
|
696
|
+
printError(result.error?.message || "Unknown error");
|
|
697
|
+
process.exit(1);
|
|
698
|
+
}
|
|
699
|
+
spinner.stop();
|
|
700
|
+
const orgs = result.data;
|
|
701
|
+
const currentOrg = getCurrentOrganization();
|
|
702
|
+
if (orgs.length === 0) {
|
|
703
|
+
printInfo("No organizations found.");
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const orgInfos = orgs.map((org) => ({
|
|
707
|
+
slug: org.slug,
|
|
708
|
+
name: org.name,
|
|
709
|
+
role: org.role,
|
|
710
|
+
current: org.slug === currentOrg ? "*" : ""
|
|
711
|
+
}));
|
|
712
|
+
if (options.json) {
|
|
713
|
+
print(orgInfos, "json");
|
|
714
|
+
} else {
|
|
715
|
+
print(orgInfos);
|
|
716
|
+
console.log();
|
|
717
|
+
printInfo(`Current organization: ${currentOrg || "(none selected)"}`);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
orgCmd.command("use").description("Set the current organization for CLI commands").argument("<slug>", "Organization slug to use").action(async (slug) => {
|
|
721
|
+
if (!isLoggedIn()) {
|
|
722
|
+
printError('Not logged in. Use "transactional login" to authenticate.');
|
|
723
|
+
process.exit(1);
|
|
724
|
+
}
|
|
725
|
+
const spinner = ora2("Verifying organization...").start();
|
|
726
|
+
const result = await listOrganizations();
|
|
727
|
+
if (!result.success || !result.data) {
|
|
728
|
+
spinner.fail("Failed to verify organization");
|
|
729
|
+
printError(result.error?.message || "Unknown error");
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
const org = result.data.find((o) => o.slug === slug);
|
|
733
|
+
if (!org) {
|
|
734
|
+
spinner.fail("Organization not found");
|
|
735
|
+
printError(`Organization "${slug}" not found.`);
|
|
736
|
+
printInfo('Use "transactional org list" to see available organizations.');
|
|
737
|
+
process.exit(1);
|
|
738
|
+
}
|
|
739
|
+
spinner.stop();
|
|
740
|
+
switchOrganization(slug);
|
|
741
|
+
printSuccess(`Now using organization: ${org.name} (${slug})`);
|
|
742
|
+
});
|
|
743
|
+
orgCmd.command("current").description("Show the current organization").action(() => {
|
|
744
|
+
const currentOrg = getCurrentOrganization();
|
|
745
|
+
if (currentOrg) {
|
|
746
|
+
printKeyValue("Current organization", currentOrg);
|
|
747
|
+
} else {
|
|
748
|
+
printInfo('No organization selected. Use "transactional org use <slug>" to select one.');
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
return orgCmd;
|
|
752
|
+
}
|
|
753
|
+
function printInfo(message) {
|
|
754
|
+
if (isColorEnabled()) {
|
|
755
|
+
console.log(chalk4.blue("\u2139"), message);
|
|
756
|
+
} else {
|
|
757
|
+
console.log("[INFO]", message);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function printHeading(title) {
|
|
761
|
+
if (isColorEnabled()) {
|
|
762
|
+
console.log(chalk4.bold.underline(title));
|
|
763
|
+
} else {
|
|
764
|
+
console.log(`=== ${title} ===`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function createEmailCommand() {
|
|
768
|
+
const emailCmd = new Command("email").description("Email management commands");
|
|
769
|
+
emailCmd.command("send").description("Send a single email").requiredOption("-f, --from <email>", "Sender email address").requiredOption("-t, --to <email>", "Recipient email address").option("-s, --subject <text>", "Email subject").option("--html <content>", "HTML body").option("--text <content>", "Plain text body").option("--template <id>", "Template ID").option("--template-alias <alias>", "Template alias").option("--model <json>", "Template model (JSON)").option("--cc <emails>", "CC recipients (comma-separated)").option("--bcc <emails>", "BCC recipients (comma-separated)").option("--reply-to <email>", "Reply-to address").option("--tag <tag>", "Message tag").option("--stream <id>", "Stream ID").option("-o, --org <slug>", "Organization slug").option("--json", "Output as JSON").action(async (options) => {
|
|
770
|
+
requireLogin();
|
|
771
|
+
const spinner = ora2("Sending email...").start();
|
|
772
|
+
try {
|
|
773
|
+
const sendOptions = {
|
|
774
|
+
from: options.from,
|
|
775
|
+
to: options.to,
|
|
776
|
+
subject: options.subject,
|
|
777
|
+
htmlBody: options.html,
|
|
778
|
+
textBody: options.text,
|
|
779
|
+
templateId: options.template ? parseInt(options.template, 10) : void 0,
|
|
780
|
+
templateAlias: options.templateAlias,
|
|
781
|
+
templateModel: options.model ? JSON.parse(options.model) : void 0,
|
|
782
|
+
cc: options.cc ? options.cc.split(",").map((e) => e.trim()) : void 0,
|
|
783
|
+
bcc: options.bcc ? options.bcc.split(",").map((e) => e.trim()) : void 0,
|
|
784
|
+
replyTo: options.replyTo,
|
|
785
|
+
tag: options.tag,
|
|
786
|
+
streamId: options.stream ? parseInt(options.stream, 10) : void 0
|
|
787
|
+
};
|
|
788
|
+
const client = getApiClient(options.org);
|
|
789
|
+
const result = await client.post("/email", sendOptions);
|
|
790
|
+
if (!result.success || !result.data) {
|
|
791
|
+
spinner.fail("Failed to send email");
|
|
792
|
+
printError(result.error?.message || "Unknown error");
|
|
793
|
+
process.exit(1);
|
|
794
|
+
}
|
|
795
|
+
spinner.succeed("Email sent successfully!");
|
|
796
|
+
if (options.json) {
|
|
797
|
+
print(result.data, "json");
|
|
798
|
+
} else {
|
|
799
|
+
printKeyValue("Message ID", result.data.messageId);
|
|
800
|
+
printKeyValue("To", result.data.to);
|
|
801
|
+
printKeyValue("Submitted At", result.data.submittedAt);
|
|
802
|
+
}
|
|
803
|
+
} catch (err) {
|
|
804
|
+
spinner.fail("Failed to send email");
|
|
805
|
+
printError(err instanceof Error ? err.message : "Unknown error");
|
|
806
|
+
process.exit(1);
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
emailCmd.command("batch <file>").description("Send batch emails from a JSON file").option("--dry-run", "Validate without sending").option("-o, --org <slug>", "Organization slug").option("--json", "Output as JSON").action(async (file, options) => {
|
|
810
|
+
requireLogin();
|
|
811
|
+
if (!fs.existsSync(file)) {
|
|
812
|
+
printError(`File not found: ${file}`);
|
|
813
|
+
process.exit(1);
|
|
814
|
+
}
|
|
815
|
+
let emails;
|
|
816
|
+
try {
|
|
817
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
818
|
+
emails = JSON.parse(content);
|
|
819
|
+
} catch (err) {
|
|
820
|
+
printError(`Failed to parse JSON file: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
821
|
+
process.exit(1);
|
|
822
|
+
}
|
|
823
|
+
if (!Array.isArray(emails)) {
|
|
824
|
+
printError("File must contain an array of email objects");
|
|
825
|
+
process.exit(1);
|
|
826
|
+
}
|
|
827
|
+
if (options.dryRun) {
|
|
828
|
+
printSuccess(`Validated ${emails.length} emails (dry run)`);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
const spinner = ora2(`Sending ${emails.length} emails...`).start();
|
|
832
|
+
try {
|
|
833
|
+
const client = getApiClient(options.org);
|
|
834
|
+
const result = await client.post("/email/batch", { messages: emails });
|
|
835
|
+
if (!result.success || !result.data) {
|
|
836
|
+
spinner.fail("Failed to send batch emails");
|
|
837
|
+
printError(result.error?.message || "Unknown error");
|
|
838
|
+
process.exit(1);
|
|
839
|
+
}
|
|
840
|
+
spinner.succeed(`Sent ${result.data.length} emails successfully!`);
|
|
841
|
+
if (options.json) {
|
|
842
|
+
print(result.data, "json");
|
|
843
|
+
}
|
|
844
|
+
} catch (err) {
|
|
845
|
+
spinner.fail("Failed to send batch emails");
|
|
846
|
+
printError(err instanceof Error ? err.message : "Unknown error");
|
|
847
|
+
process.exit(1);
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
const templates = emailCmd.command("templates").description("Manage email templates");
|
|
851
|
+
templates.command("list").description("List email templates").option("--server <id>", "Filter by server ID").option("--status <status>", "Filter by status (DRAFT, ACTIVE, ARCHIVED)").option("--limit <n>", "Max results", "50").option("-o, --org <slug>", "Organization slug").option("--json", "Output as JSON").action(async (options) => {
|
|
852
|
+
requireLogin();
|
|
853
|
+
const client = getApiClient(options.org);
|
|
854
|
+
const result = await client.get("/templates", {
|
|
855
|
+
serverId: options.server ? parseInt(options.server, 10) : void 0,
|
|
856
|
+
status: options.status,
|
|
857
|
+
limit: parseInt(options.limit, 10)
|
|
858
|
+
});
|
|
859
|
+
if (!result.success || !result.data) {
|
|
860
|
+
printError(result.error?.message || "Failed to list templates");
|
|
861
|
+
process.exit(1);
|
|
862
|
+
}
|
|
863
|
+
if (options.json) {
|
|
864
|
+
print(result.data, "json");
|
|
865
|
+
} else {
|
|
866
|
+
const data = result.data.map((t) => ({
|
|
867
|
+
id: t.id,
|
|
868
|
+
name: t.name,
|
|
869
|
+
alias: t.alias || "-",
|
|
870
|
+
status: t.status,
|
|
871
|
+
updated: t.updatedAt
|
|
872
|
+
}));
|
|
873
|
+
print(data);
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
templates.command("get <id>").description("Get template details").option("-o, --org <slug>", "Organization slug").option("--json", "Output as JSON").action(async (id, options) => {
|
|
877
|
+
requireLogin();
|
|
878
|
+
const client = getApiClient(options.org);
|
|
879
|
+
const result = await client.get(`/templates/${id}`);
|
|
880
|
+
if (!result.success || !result.data) {
|
|
881
|
+
printError(result.error?.message || "Failed to get template");
|
|
882
|
+
process.exit(1);
|
|
883
|
+
}
|
|
884
|
+
print(result.data, options.json ? "json" : void 0);
|
|
885
|
+
});
|
|
886
|
+
templates.command("create").description("Create a new template").requiredOption("--name <name>", "Template name").requiredOption("--subject <subject>", "Email subject").requiredOption("--server <id>", "Server ID").option("--alias <alias>", "Template alias").option("--html <content>", "HTML body").option("--text <content>", "Plain text body").option("-o, --org <slug>", "Organization slug").option("--json", "Output as JSON").action(async (options) => {
|
|
887
|
+
requireLogin();
|
|
888
|
+
const spinner = ora2("Creating template...").start();
|
|
889
|
+
const client = getApiClient(options.org);
|
|
890
|
+
const result = await client.post("/templates", {
|
|
891
|
+
name: options.name,
|
|
892
|
+
subject: options.subject,
|
|
893
|
+
serverId: parseInt(options.server, 10),
|
|
894
|
+
alias: options.alias,
|
|
895
|
+
htmlBody: options.html,
|
|
896
|
+
textBody: options.text
|
|
897
|
+
});
|
|
898
|
+
if (!result.success || !result.data) {
|
|
899
|
+
spinner.fail("Failed to create template");
|
|
900
|
+
printError(result.error?.message || "Unknown error");
|
|
901
|
+
process.exit(1);
|
|
902
|
+
}
|
|
903
|
+
spinner.succeed("Template created!");
|
|
904
|
+
if (options.json) {
|
|
905
|
+
print(result.data, "json");
|
|
906
|
+
} else {
|
|
907
|
+
printKeyValue("ID", result.data.id);
|
|
908
|
+
printKeyValue("Name", result.data.name);
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
templates.command("update <id>").description("Update a template").option("--name <name>", "Template name").option("--subject <subject>", "Email subject").option("--alias <alias>", "Template alias").option("--html <content>", "HTML body").option("--text <content>", "Plain text body").option("-o, --org <slug>", "Organization slug").option("--json", "Output as JSON").action(async (id, options) => {
|
|
912
|
+
requireLogin();
|
|
913
|
+
const spinner = ora2("Updating template...").start();
|
|
914
|
+
const client = getApiClient(options.org);
|
|
915
|
+
const result = await client.patch(`/templates/${id}`, {
|
|
916
|
+
name: options.name,
|
|
917
|
+
subject: options.subject,
|
|
918
|
+
alias: options.alias,
|
|
919
|
+
htmlBody: options.html,
|
|
920
|
+
textBody: options.text
|
|
921
|
+
});
|
|
922
|
+
if (!result.success || !result.data) {
|
|
923
|
+
spinner.fail("Failed to update template");
|
|
924
|
+
printError(result.error?.message || "Unknown error");
|
|
925
|
+
process.exit(1);
|
|
926
|
+
}
|
|
927
|
+
spinner.succeed("Template updated!");
|
|
928
|
+
if (options.json) {
|
|
929
|
+
print(result.data, "json");
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
templates.command("delete <id>").description("Delete a template").option("-o, --org <slug>", "Organization slug").action(async (id, options) => {
|
|
933
|
+
requireLogin();
|
|
934
|
+
const shouldDelete = await confirm(`Are you sure you want to delete template ${id}?`, false);
|
|
935
|
+
if (!shouldDelete) {
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const spinner = ora2("Deleting template...").start();
|
|
939
|
+
const client = getApiClient(options.org);
|
|
940
|
+
const result = await client.delete(`/templates/${id}`);
|
|
941
|
+
if (!result.success) {
|
|
942
|
+
spinner.fail("Failed to delete template");
|
|
943
|
+
printError(result.error?.message || "Unknown error");
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
spinner.succeed("Template deleted!");
|
|
947
|
+
});
|
|
948
|
+
const domains = emailCmd.command("domains").description("Manage email domains");
|
|
949
|
+
domains.command("list").description("List email domains").option("-o, --org <slug>", "Organization slug").option("--json", "Output as JSON").action(async (options) => {
|
|
950
|
+
requireLogin();
|
|
951
|
+
const client = getApiClient(options.org);
|
|
952
|
+
const result = await client.get("/domains");
|
|
953
|
+
if (!result.success || !result.data) {
|
|
954
|
+
printError(result.error?.message || "Failed to list domains");
|
|
955
|
+
process.exit(1);
|
|
956
|
+
}
|
|
957
|
+
if (options.json) {
|
|
958
|
+
print(result.data.domains, "json");
|
|
959
|
+
} else {
|
|
960
|
+
const data = result.data.domains.map((d) => ({
|
|
961
|
+
id: d.id,
|
|
962
|
+
domain: d.name,
|
|
963
|
+
status: d.status,
|
|
964
|
+
dkimVerified: d.dkimVerified ? "Yes" : "No",
|
|
965
|
+
spfVerified: d.spfVerified ? "Yes" : "No"
|
|
966
|
+
}));
|
|
967
|
+
print(data);
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
domains.command("add <domain>").description("Add a domain").option("-o, --org <slug>", "Organization slug").option("--json", "Output as JSON").action(async (domain, options) => {
|
|
971
|
+
requireLogin();
|
|
972
|
+
const spinner = ora2("Adding domain...").start();
|
|
973
|
+
const client = getApiClient(options.org);
|
|
974
|
+
const result = await client.post("/domains", { domain });
|
|
975
|
+
if (!result.success || !result.data) {
|
|
976
|
+
spinner.fail("Failed to add domain");
|
|
977
|
+
printError(result.error?.message || "Unknown error");
|
|
978
|
+
process.exit(1);
|
|
979
|
+
}
|
|
980
|
+
spinner.succeed("Domain added!");
|
|
981
|
+
console.log("\nDNS Records to configure:");
|
|
982
|
+
for (const record of result.data.dnsRecords) {
|
|
983
|
+
console.log(`
|
|
984
|
+
${record.type}:`);
|
|
985
|
+
console.log(` Name: ${record.name}`);
|
|
986
|
+
console.log(` Value: ${record.value}`);
|
|
987
|
+
}
|
|
988
|
+
if (options.json) {
|
|
989
|
+
print(result.data, "json");
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
domains.command("verify <id>").description("Verify a domain").option("-o, --org <slug>", "Organization slug").option("--json", "Output as JSON").action(async (id, options) => {
|
|
993
|
+
requireLogin();
|
|
994
|
+
const spinner = ora2("Verifying domain...").start();
|
|
995
|
+
const client = getApiClient(options.org);
|
|
996
|
+
const result = await client.post(`/domains/${id}/verify`);
|
|
997
|
+
if (!result.success || !result.data) {
|
|
998
|
+
spinner.fail("Domain verification failed");
|
|
999
|
+
printError(result.error?.message || "Unknown error");
|
|
1000
|
+
process.exit(1);
|
|
1001
|
+
}
|
|
1002
|
+
if (result.data.status === "VERIFIED") {
|
|
1003
|
+
spinner.succeed("Domain verified!");
|
|
1004
|
+
} else {
|
|
1005
|
+
spinner.info("Verification in progress");
|
|
1006
|
+
console.log("\nVerification Status:");
|
|
1007
|
+
const checkmark = (verified) => verified ? "\u2713" : "\u2717";
|
|
1008
|
+
console.log(` ${checkmark(result.data.dkimVerified)} DKIM`);
|
|
1009
|
+
console.log(` ${checkmark(result.data.spfVerified)} SPF`);
|
|
1010
|
+
console.log(` ${checkmark(result.data.returnPathVerified)} Return-Path`);
|
|
1011
|
+
console.log(` ${checkmark(result.data.dmarcVerified)} DMARC`);
|
|
1012
|
+
if (result.data.verificationError) {
|
|
1013
|
+
console.log(`
|
|
1014
|
+
Error: ${result.data.verificationError}`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
if (options.json) {
|
|
1018
|
+
print(result.data, "json");
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
domains.command("delete <id>").description("Delete a domain").option("-o, --org <slug>", "Organization slug").action(async (id, options) => {
|
|
1022
|
+
requireLogin();
|
|
1023
|
+
const shouldDelete = await confirm(`Are you sure you want to delete domain ${id}?`, false);
|
|
1024
|
+
if (!shouldDelete) {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
const spinner = ora2("Deleting domain...").start();
|
|
1028
|
+
const client = getApiClient(options.org);
|
|
1029
|
+
const result = await client.delete(`/domains/${id}`);
|
|
1030
|
+
if (!result.success) {
|
|
1031
|
+
spinner.fail("Failed to delete domain");
|
|
1032
|
+
printError(result.error?.message || "Unknown error");
|
|
1033
|
+
process.exit(1);
|
|
1034
|
+
}
|
|
1035
|
+
spinner.succeed("Domain deleted!");
|
|
1036
|
+
});
|
|
1037
|
+
const senders = emailCmd.command("senders").description("Manage email senders");
|
|
1038
|
+
senders.command("list").description("List email senders").option("-o, --org <slug>", "Organization slug").option("--json", "Output as JSON").action(async (options) => {
|
|
1039
|
+
requireLogin();
|
|
1040
|
+
const client = getApiClient(options.org);
|
|
1041
|
+
const result = await client.get("/senders");
|
|
1042
|
+
if (!result.success || !result.data) {
|
|
1043
|
+
printError(result.error?.message || "Failed to list senders");
|
|
1044
|
+
process.exit(1);
|
|
1045
|
+
}
|
|
1046
|
+
if (options.json) {
|
|
1047
|
+
print(result.data.senders, "json");
|
|
1048
|
+
} else {
|
|
1049
|
+
const data = result.data.senders.map((s) => ({
|
|
1050
|
+
id: s.id,
|
|
1051
|
+
email: s.email,
|
|
1052
|
+
name: s.name || "-",
|
|
1053
|
+
status: s.status
|
|
1054
|
+
}));
|
|
1055
|
+
print(data);
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
senders.command("add <email>").description("Add an email sender").option("--name <name>", "Sender name").option("-o, --org <slug>", "Organization slug").action(async (emailAddr, options) => {
|
|
1059
|
+
requireLogin();
|
|
1060
|
+
const spinner = ora2("Adding sender...").start();
|
|
1061
|
+
const client = getApiClient(options.org);
|
|
1062
|
+
const result = await client.post("/senders", {
|
|
1063
|
+
email: emailAddr,
|
|
1064
|
+
name: options.name
|
|
1065
|
+
});
|
|
1066
|
+
if (!result.success || !result.data) {
|
|
1067
|
+
spinner.fail("Failed to add sender");
|
|
1068
|
+
printError(result.error?.message || "Unknown error");
|
|
1069
|
+
process.exit(1);
|
|
1070
|
+
}
|
|
1071
|
+
spinner.succeed("Sender added! Check your email for verification link.");
|
|
1072
|
+
});
|
|
1073
|
+
senders.command("delete <id>").description("Delete an email sender").option("-o, --org <slug>", "Organization slug").action(async (id, options) => {
|
|
1074
|
+
requireLogin();
|
|
1075
|
+
const shouldDelete = await confirm(`Are you sure you want to delete sender ${id}?`, false);
|
|
1076
|
+
if (!shouldDelete) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const spinner = ora2("Deleting sender...").start();
|
|
1080
|
+
const client = getApiClient(options.org);
|
|
1081
|
+
const result = await client.delete(`/senders/${id}`);
|
|
1082
|
+
if (!result.success) {
|
|
1083
|
+
spinner.fail("Failed to delete sender");
|
|
1084
|
+
printError(result.error?.message || "Unknown error");
|
|
1085
|
+
process.exit(1);
|
|
1086
|
+
}
|
|
1087
|
+
spinner.succeed("Sender deleted!");
|
|
1088
|
+
});
|
|
1089
|
+
const suppressions = emailCmd.command("suppressions").description("Manage email suppressions");
|
|
1090
|
+
suppressions.command("list").description("List email suppressions").option("-o, --org <slug>", "Organization slug").option("--server <id>", "Filter by server ID").option("--json", "Output as JSON").action(async (options) => {
|
|
1091
|
+
requireLogin();
|
|
1092
|
+
const client = getApiClient(options.org);
|
|
1093
|
+
const params = new URLSearchParams();
|
|
1094
|
+
if (options.server) {
|
|
1095
|
+
params.set("serverId", options.server);
|
|
1096
|
+
}
|
|
1097
|
+
const queryString = params.toString();
|
|
1098
|
+
const url = queryString ? `/suppressions?${queryString}` : "/suppressions";
|
|
1099
|
+
const result = await client.get(url);
|
|
1100
|
+
if (!result.success || !result.data) {
|
|
1101
|
+
printError(result.error?.message || "Failed to list suppressions");
|
|
1102
|
+
process.exit(1);
|
|
1103
|
+
}
|
|
1104
|
+
if (options.json) {
|
|
1105
|
+
print(result.data.data, "json");
|
|
1106
|
+
} else {
|
|
1107
|
+
const data = result.data.data.map((s) => ({
|
|
1108
|
+
id: s.id,
|
|
1109
|
+
email: s.email,
|
|
1110
|
+
reason: s.reason,
|
|
1111
|
+
created: s.createdAt
|
|
1112
|
+
}));
|
|
1113
|
+
print(data);
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
suppressions.command("add <email>").description("Add email to suppression list").option("-o, --org <slug>", "Organization slug").requiredOption("--server <id>", "Server ID to add suppression to").option("--reason <reason>", "Suppression reason (HARD_BOUNCE, SPAM_COMPLAINT, MANUAL, UNSUBSCRIBE)", "MANUAL").option("--notes <notes>", "Optional notes").action(async (emailAddr, options) => {
|
|
1117
|
+
requireLogin();
|
|
1118
|
+
const spinner = ora2("Adding to suppression list...").start();
|
|
1119
|
+
const client = getApiClient(options.org);
|
|
1120
|
+
const result = await client.post("/suppressions", {
|
|
1121
|
+
email: emailAddr,
|
|
1122
|
+
serverId: parseInt(options.server, 10),
|
|
1123
|
+
reason: options.reason,
|
|
1124
|
+
notes: options.notes
|
|
1125
|
+
});
|
|
1126
|
+
if (!result.success) {
|
|
1127
|
+
spinner.fail("Failed to add suppression");
|
|
1128
|
+
printError(result.error?.message || "Unknown error");
|
|
1129
|
+
process.exit(1);
|
|
1130
|
+
}
|
|
1131
|
+
spinner.succeed("Email added to suppression list!");
|
|
1132
|
+
});
|
|
1133
|
+
suppressions.command("remove <id>").description("Remove suppression by ID").option("-o, --org <slug>", "Organization slug").action(async (id, options) => {
|
|
1134
|
+
requireLogin();
|
|
1135
|
+
const shouldRemove = await confirm(
|
|
1136
|
+
`Are you sure you want to remove suppression #${id}?`,
|
|
1137
|
+
false
|
|
1138
|
+
);
|
|
1139
|
+
if (!shouldRemove) {
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
const spinner = ora2("Removing from suppression list...").start();
|
|
1143
|
+
const client = getApiClient(options.org);
|
|
1144
|
+
const result = await client.delete(`/suppressions/${id}`);
|
|
1145
|
+
if (!result.success) {
|
|
1146
|
+
spinner.fail("Failed to remove suppression");
|
|
1147
|
+
printError(result.error?.message || "Unknown error");
|
|
1148
|
+
process.exit(1);
|
|
1149
|
+
}
|
|
1150
|
+
spinner.succeed("Suppression removed!");
|
|
1151
|
+
});
|
|
1152
|
+
emailCmd.command("stats").description("Get email statistics").option("--period <period>", "Period (day, week, month)", "week").option("--server <id>", "Filter by server ID").option("--stream <id>", "Filter by stream ID").option("-o, --org <slug>", "Organization slug").option("--json", "Output as JSON").action(async (options) => {
|
|
1153
|
+
requireLogin();
|
|
1154
|
+
const client = getApiClient(options.org);
|
|
1155
|
+
const result = await client.get("/stats/outbound", {
|
|
1156
|
+
period: options.period,
|
|
1157
|
+
serverId: options.server ? parseInt(options.server, 10) : void 0,
|
|
1158
|
+
streamId: options.stream ? parseInt(options.stream, 10) : void 0
|
|
1159
|
+
});
|
|
1160
|
+
if (!result.success || !result.data) {
|
|
1161
|
+
printError(result.error?.message || "Failed to get stats");
|
|
1162
|
+
process.exit(1);
|
|
1163
|
+
}
|
|
1164
|
+
if (options.json) {
|
|
1165
|
+
print(result.data, "json");
|
|
1166
|
+
} else {
|
|
1167
|
+
const data = result.data;
|
|
1168
|
+
console.log(`
|
|
1169
|
+
Email Statistics (${data.period})
|
|
1170
|
+
`);
|
|
1171
|
+
printKeyValue("Sent", data.sent);
|
|
1172
|
+
printKeyValue("Delivered", data.delivered);
|
|
1173
|
+
printKeyValue("Bounced", data.bounced);
|
|
1174
|
+
printKeyValue("Complaints", data.complained);
|
|
1175
|
+
printKeyValue("Opened", data.opened);
|
|
1176
|
+
printKeyValue("Clicked", data.clicked);
|
|
1177
|
+
console.log("\nRates:");
|
|
1178
|
+
printKeyValue("Delivery Rate", `${(data.deliveryRate * 100).toFixed(2)}%`);
|
|
1179
|
+
printKeyValue("Open Rate", `${(data.openRate * 100).toFixed(2)}%`);
|
|
1180
|
+
printKeyValue("Click Rate", `${(data.clickRate * 100).toFixed(2)}%`);
|
|
1181
|
+
printKeyValue("Bounce Rate", `${(data.bounceRate * 100).toFixed(2)}%`);
|
|
1182
|
+
printKeyValue("Complaint Rate", `${(data.complaintRate * 100).toFixed(4)}%`);
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
return emailCmd;
|
|
1186
|
+
}
|
|
1187
|
+
function requireLogin() {
|
|
1188
|
+
if (!isLoggedIn()) {
|
|
1189
|
+
printError('Not logged in. Use "transactional login" to authenticate.');
|
|
1190
|
+
process.exit(1);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
function createConfigCommand() {
|
|
1194
|
+
const configCmd = new Command("config").description("Manage CLI configuration");
|
|
1195
|
+
configCmd.command("show").description("Show current configuration").option("--json", "Output as JSON").action((options) => {
|
|
1196
|
+
const config = getConfig();
|
|
1197
|
+
if (options.json) {
|
|
1198
|
+
print(config, "json");
|
|
1199
|
+
} else {
|
|
1200
|
+
console.log();
|
|
1201
|
+
printHeading2("Current Configuration");
|
|
1202
|
+
printKeyValue("API URL", config.apiUrl);
|
|
1203
|
+
printKeyValue("Web URL", config.webUrl);
|
|
1204
|
+
printKeyValue("Output Format", config.outputFormat);
|
|
1205
|
+
printKeyValue("Color", config.color ? "enabled" : "disabled");
|
|
1206
|
+
console.log();
|
|
1207
|
+
printHeading2("File Locations");
|
|
1208
|
+
printKeyValue("Config Directory", getConfigDir());
|
|
1209
|
+
printKeyValue("Credentials File", getCredentialsFile());
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
configCmd.command("set <key> <value>").description("Set a configuration value").action((key, value) => {
|
|
1213
|
+
const validKeys = ["apiUrl", "webUrl", "outputFormat", "color"];
|
|
1214
|
+
if (!validKeys.includes(key)) {
|
|
1215
|
+
console.error(`Invalid key: ${key}`);
|
|
1216
|
+
console.error(`Valid keys: ${validKeys.join(", ")}`);
|
|
1217
|
+
process.exit(1);
|
|
1218
|
+
}
|
|
1219
|
+
if (key === "outputFormat") {
|
|
1220
|
+
const validFormats = ["table", "json", "yaml"];
|
|
1221
|
+
if (!validFormats.includes(value)) {
|
|
1222
|
+
console.error(`Invalid output format: ${value}`);
|
|
1223
|
+
console.error(`Valid formats: ${validFormats.join(", ")}`);
|
|
1224
|
+
process.exit(1);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
if (key === "color") {
|
|
1228
|
+
const validColors = ["true", "false", "yes", "no", "1", "0"];
|
|
1229
|
+
if (!validColors.includes(value.toLowerCase())) {
|
|
1230
|
+
console.error(`Invalid color value: ${value}`);
|
|
1231
|
+
console.error(`Valid values: true, false`);
|
|
1232
|
+
process.exit(1);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
let typedValue = value;
|
|
1236
|
+
if (key === "color") {
|
|
1237
|
+
typedValue = ["true", "yes", "1"].includes(value.toLowerCase());
|
|
1238
|
+
}
|
|
1239
|
+
saveConfig({ [key]: typedValue });
|
|
1240
|
+
printSuccess(`Set ${key} = ${typedValue}`);
|
|
1241
|
+
});
|
|
1242
|
+
configCmd.command("get <key>").description("Get a configuration value").action((key) => {
|
|
1243
|
+
const config = getConfig();
|
|
1244
|
+
const value = config[key];
|
|
1245
|
+
if (value === void 0) {
|
|
1246
|
+
console.error(`Unknown key: ${key}`);
|
|
1247
|
+
process.exit(1);
|
|
1248
|
+
}
|
|
1249
|
+
console.log(value);
|
|
1250
|
+
});
|
|
1251
|
+
configCmd.command("reset").description("Reset configuration to defaults").action(() => {
|
|
1252
|
+
saveConfig({
|
|
1253
|
+
apiUrl: "https://api.usetransactional.com",
|
|
1254
|
+
webUrl: "https://usetransactional.com",
|
|
1255
|
+
outputFormat: "table",
|
|
1256
|
+
color: true
|
|
1257
|
+
});
|
|
1258
|
+
printSuccess("Configuration reset to defaults");
|
|
1259
|
+
});
|
|
1260
|
+
configCmd.command("path").description("Show configuration file paths").action(() => {
|
|
1261
|
+
console.log("Config Directory:", getConfigDir());
|
|
1262
|
+
console.log("Credentials File:", getCredentialsFile());
|
|
1263
|
+
});
|
|
1264
|
+
return configCmd;
|
|
1265
|
+
}
|
|
1266
|
+
function printHeading2(title) {
|
|
1267
|
+
if (isColorEnabled()) {
|
|
1268
|
+
console.log(chalk4.bold.underline(title));
|
|
1269
|
+
} else {
|
|
1270
|
+
console.log(`=== ${title} ===`);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
function getClaudeDesktopConfigPath() {
|
|
1274
|
+
const platform2 = os2.platform();
|
|
1275
|
+
const homeDir = os2.homedir();
|
|
1276
|
+
if (platform2 === "darwin") {
|
|
1277
|
+
return path2.join(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
1278
|
+
} else if (platform2 === "win32") {
|
|
1279
|
+
return path2.join(homeDir, "AppData", "Roaming", "Claude", "claude_desktop_config.json");
|
|
1280
|
+
} else {
|
|
1281
|
+
return path2.join(homeDir, ".config", "claude", "claude_desktop_config.json");
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
function getClaudeCodeConfigPath() {
|
|
1285
|
+
const homeDir = os2.homedir();
|
|
1286
|
+
return path2.join(homeDir, ".claude.json");
|
|
1287
|
+
}
|
|
1288
|
+
function getMcpServerUrl() {
|
|
1289
|
+
const apiUrl = getApiUrl();
|
|
1290
|
+
if (apiUrl.includes("localhost") || apiUrl.includes("127.0.0.1")) {
|
|
1291
|
+
return apiUrl.replace(/\/$/, "") + "/mcp";
|
|
1292
|
+
}
|
|
1293
|
+
return process.env.MCP_SERVER_URL || "https://mcp.usetransactional.com/mcp";
|
|
1294
|
+
}
|
|
1295
|
+
function readJsonConfig(filePath) {
|
|
1296
|
+
try {
|
|
1297
|
+
if (!fs.existsSync(filePath)) {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1301
|
+
return JSON.parse(content);
|
|
1302
|
+
} catch {
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
function writeJsonConfig(filePath, config) {
|
|
1307
|
+
const dir = path2.dirname(filePath);
|
|
1308
|
+
if (!fs.existsSync(dir)) {
|
|
1309
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1310
|
+
}
|
|
1311
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n");
|
|
1312
|
+
}
|
|
1313
|
+
function printWarning(message) {
|
|
1314
|
+
if (isColorEnabled()) {
|
|
1315
|
+
console.log(chalk4.yellow("\u26A0"), message);
|
|
1316
|
+
} else {
|
|
1317
|
+
console.log("[WARN]", message);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
function createMcpCommand() {
|
|
1321
|
+
const mcpCmd = new Command("mcp").description("MCP (Model Context Protocol) integration");
|
|
1322
|
+
mcpCmd.command("setup").description("Show MCP setup instructions").action(() => {
|
|
1323
|
+
const mcpUrl = getMcpServerUrl();
|
|
1324
|
+
console.log("\n\u{1F4E1} Transactional MCP Server Setup\n");
|
|
1325
|
+
console.log("The MCP server allows Claude and other AI assistants to");
|
|
1326
|
+
console.log("interact with your Transactional account.\n");
|
|
1327
|
+
console.log(chalk4.bold("MCP Server URL:"));
|
|
1328
|
+
console.log(` ${chalk4.cyan(mcpUrl)}
|
|
1329
|
+
`);
|
|
1330
|
+
console.log(chalk4.bold("Setup Options:\n"));
|
|
1331
|
+
console.log(chalk4.underline("1. Claude Desktop (Pro/Max/Team/Enterprise)"));
|
|
1332
|
+
console.log(" Go to Settings \u2192 Integrations \u2192 Add Custom Integration");
|
|
1333
|
+
console.log(` Enter URL: ${chalk4.cyan(mcpUrl.replace("/mcp", ""))}`);
|
|
1334
|
+
console.log(" Claude will handle OAuth authorization automatically.\n");
|
|
1335
|
+
console.log(chalk4.underline("2. Claude Desktop (Free/JSON Config)"));
|
|
1336
|
+
console.log(" Run: transactional mcp install --target claude-desktop\n");
|
|
1337
|
+
console.log(chalk4.underline("3. Claude Code"));
|
|
1338
|
+
console.log(" Run: transactional mcp install --target claude-code\n");
|
|
1339
|
+
console.log(chalk4.bold("Available Commands:"));
|
|
1340
|
+
console.log(" transactional mcp install Install MCP config");
|
|
1341
|
+
console.log(" transactional mcp uninstall Remove MCP config");
|
|
1342
|
+
console.log(" transactional mcp status Check MCP server status");
|
|
1343
|
+
console.log(" transactional mcp tools List available MCP tools\n");
|
|
1344
|
+
});
|
|
1345
|
+
mcpCmd.command("config").description("Show MCP server configuration").option("--target <target>", "Target: claude-desktop, claude-code", "claude-desktop").option("--json", "Output as raw JSON").action((options) => {
|
|
1346
|
+
const mcpUrl = getMcpServerUrl();
|
|
1347
|
+
const target = options.target;
|
|
1348
|
+
let config;
|
|
1349
|
+
if (target === "claude-code") {
|
|
1350
|
+
config = {
|
|
1351
|
+
mcpServers: {
|
|
1352
|
+
transactional: {
|
|
1353
|
+
type: "http",
|
|
1354
|
+
url: mcpUrl
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
} else {
|
|
1359
|
+
config = {
|
|
1360
|
+
mcpServers: {
|
|
1361
|
+
transactional: {
|
|
1362
|
+
command: "npx",
|
|
1363
|
+
args: ["mcp-remote", mcpUrl.replace("/mcp", "")]
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
if (options.json) {
|
|
1369
|
+
console.log(JSON.stringify(config, null, 2));
|
|
1370
|
+
} else {
|
|
1371
|
+
console.log("\n\u{1F4CB} MCP Configuration\n");
|
|
1372
|
+
console.log(`Target: ${target}
|
|
1373
|
+
`);
|
|
1374
|
+
console.log("```json");
|
|
1375
|
+
console.log(JSON.stringify(config, null, 2));
|
|
1376
|
+
console.log("```\n");
|
|
1377
|
+
if (target === "claude-desktop") {
|
|
1378
|
+
const configPath = getClaudeDesktopConfigPath();
|
|
1379
|
+
console.log(`Config file: ${configPath}
|
|
1380
|
+
`);
|
|
1381
|
+
console.log(chalk4.yellow("Note: Uses mcp-remote for OAuth support."));
|
|
1382
|
+
console.log("Install mcp-remote: npm install -g mcp-remote\n");
|
|
1383
|
+
} else {
|
|
1384
|
+
const configPath = getClaudeCodeConfigPath();
|
|
1385
|
+
console.log(`Config file: ${configPath}
|
|
1386
|
+
`);
|
|
1387
|
+
}
|
|
1388
|
+
console.log('Run "transactional mcp install" to auto-install.\n');
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
mcpCmd.command("install").description("Install MCP config to Claude Desktop or Claude Code").option("--target <target>", "Target: claude-desktop, claude-code, both", "both").option("--force", "Overwrite existing transactional config").action(async (options) => {
|
|
1392
|
+
const target = options.target;
|
|
1393
|
+
const mcpUrl = getMcpServerUrl();
|
|
1394
|
+
console.log("\n\u{1F4E1} Installing Transactional MCP configuration...\n");
|
|
1395
|
+
const targets = target === "both" ? ["claude-desktop", "claude-code"] : [target];
|
|
1396
|
+
let anyInstalled = false;
|
|
1397
|
+
let anySkipped = false;
|
|
1398
|
+
for (const t of targets) {
|
|
1399
|
+
try {
|
|
1400
|
+
if (t === "claude-desktop") {
|
|
1401
|
+
const configPath = getClaudeDesktopConfigPath();
|
|
1402
|
+
const existingConfig = readJsonConfig(configPath) || { mcpServers: {} };
|
|
1403
|
+
if (existingConfig.mcpServers?.transactional && !options.force) {
|
|
1404
|
+
printWarning(`Claude Desktop: Already configured. Use --force to overwrite.`);
|
|
1405
|
+
console.log(` Config: ${configPath}
|
|
1406
|
+
`);
|
|
1407
|
+
anySkipped = true;
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
existingConfig.mcpServers = {
|
|
1411
|
+
...existingConfig.mcpServers,
|
|
1412
|
+
transactional: {
|
|
1413
|
+
command: "npx",
|
|
1414
|
+
args: ["mcp-remote", mcpUrl.replace("/mcp", "")]
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
writeJsonConfig(configPath, existingConfig);
|
|
1418
|
+
printSuccess(`Claude Desktop: Config installed`);
|
|
1419
|
+
console.log(` Config: ${configPath}
|
|
1420
|
+
`);
|
|
1421
|
+
anyInstalled = true;
|
|
1422
|
+
} else if (t === "claude-code") {
|
|
1423
|
+
const configPath = getClaudeCodeConfigPath();
|
|
1424
|
+
const existingConfig = readJsonConfig(configPath) || {};
|
|
1425
|
+
if (existingConfig.mcpServers?.transactional && !options.force) {
|
|
1426
|
+
printWarning(`Claude Code: Already configured. Use --force to overwrite.`);
|
|
1427
|
+
console.log(` Config: ${configPath}
|
|
1428
|
+
`);
|
|
1429
|
+
anySkipped = true;
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
existingConfig.mcpServers = {
|
|
1433
|
+
...existingConfig.mcpServers,
|
|
1434
|
+
transactional: {
|
|
1435
|
+
type: "http",
|
|
1436
|
+
url: mcpUrl
|
|
1437
|
+
}
|
|
1438
|
+
};
|
|
1439
|
+
writeJsonConfig(configPath, existingConfig);
|
|
1440
|
+
printSuccess(`Claude Code: Config installed`);
|
|
1441
|
+
console.log(` Config: ${configPath}
|
|
1442
|
+
`);
|
|
1443
|
+
anyInstalled = true;
|
|
1444
|
+
}
|
|
1445
|
+
} catch (err) {
|
|
1446
|
+
printError(`Failed to install ${t} config: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
if (anyInstalled) {
|
|
1450
|
+
console.log(chalk4.bold("Next steps:"));
|
|
1451
|
+
console.log("1. Restart Claude Desktop/Code to apply changes");
|
|
1452
|
+
console.log("2. When you use a Transactional tool, Claude will prompt you to authorize");
|
|
1453
|
+
console.log("");
|
|
1454
|
+
} else if (anySkipped) {
|
|
1455
|
+
console.log("To force reinstall, run: transactional mcp install --force\n");
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
mcpCmd.command("uninstall").description("Remove MCP config from Claude Desktop and/or Claude Code").option("--target <target>", "Target: claude-desktop, claude-code, both", "both").action((options) => {
|
|
1459
|
+
const target = options.target;
|
|
1460
|
+
const targets = target === "both" ? ["claude-desktop", "claude-code"] : [target];
|
|
1461
|
+
for (const t of targets) {
|
|
1462
|
+
const spinner = ora2(`Removing MCP config from ${t}...`).start();
|
|
1463
|
+
try {
|
|
1464
|
+
const configPath = t === "claude-desktop" ? getClaudeDesktopConfigPath() : getClaudeCodeConfigPath();
|
|
1465
|
+
if (!fs.existsSync(configPath)) {
|
|
1466
|
+
spinner.info(`No ${t} config found.`);
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
const config = readJsonConfig(configPath);
|
|
1470
|
+
if (!config?.mcpServers?.transactional) {
|
|
1471
|
+
spinner.info(`Transactional not configured in ${t}.`);
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
delete config.mcpServers.transactional;
|
|
1475
|
+
writeJsonConfig(configPath, config);
|
|
1476
|
+
spinner.succeed(`Removed from ${t}`);
|
|
1477
|
+
} catch (err) {
|
|
1478
|
+
spinner.fail(`Failed to remove ${t} config`);
|
|
1479
|
+
printError(err instanceof Error ? err.message : "Unknown error");
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
console.log("");
|
|
1483
|
+
printWarning("Please restart Claude Desktop/Code to apply changes.\n");
|
|
1484
|
+
});
|
|
1485
|
+
mcpCmd.command("status").description("Check MCP server status").action(async () => {
|
|
1486
|
+
const spinner = ora2("Checking MCP server...").start();
|
|
1487
|
+
const mcpUrl = getMcpServerUrl().replace("/mcp", "");
|
|
1488
|
+
try {
|
|
1489
|
+
const response = await fetch(`${mcpUrl}/health`);
|
|
1490
|
+
if (response.ok) {
|
|
1491
|
+
const data = await response.json();
|
|
1492
|
+
spinner.succeed("MCP server is running");
|
|
1493
|
+
console.log("\nServer info:");
|
|
1494
|
+
print(data);
|
|
1495
|
+
console.log("\nOAuth endpoints:");
|
|
1496
|
+
console.log(` Authorization: ${mcpUrl}/mcp/authorize`);
|
|
1497
|
+
console.log(` Token: ${mcpUrl}/mcp/token`);
|
|
1498
|
+
console.log(` Protected Resource Metadata: ${mcpUrl}/.well-known/oauth-protected-resource`);
|
|
1499
|
+
} else {
|
|
1500
|
+
spinner.fail(`MCP server returned ${response.status}`);
|
|
1501
|
+
}
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
spinner.fail("Could not connect to MCP server");
|
|
1504
|
+
printError(err instanceof Error ? err.message : "Unknown error");
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
mcpCmd.command("tools").description("List available MCP tools").action(() => {
|
|
1508
|
+
console.log("\n\u{1F527} Available MCP Tools\n");
|
|
1509
|
+
const tools = [
|
|
1510
|
+
{ category: "Email", tools: [
|
|
1511
|
+
{ name: "transactional_email_send", desc: "Send a single email" },
|
|
1512
|
+
{ name: "transactional_email_batch", desc: "Send multiple emails" },
|
|
1513
|
+
{ name: "transactional_email_stats", desc: "Get email statistics" },
|
|
1514
|
+
{ name: "transactional_templates_list", desc: "List templates" },
|
|
1515
|
+
{ name: "transactional_templates_get", desc: "Get template details" },
|
|
1516
|
+
{ name: "transactional_templates_create", desc: "Create template" },
|
|
1517
|
+
{ name: "transactional_domains_list", desc: "List domains" },
|
|
1518
|
+
{ name: "transactional_domains_add", desc: "Add domain" },
|
|
1519
|
+
{ name: "transactional_senders_list", desc: "List senders" },
|
|
1520
|
+
{ name: "transactional_suppressions_list", desc: "List suppressions" }
|
|
1521
|
+
] },
|
|
1522
|
+
{ category: "Organization", tools: [
|
|
1523
|
+
{ name: "transactional_whoami", desc: "Current user info" },
|
|
1524
|
+
{ name: "transactional_orgs_list", desc: "List organizations" },
|
|
1525
|
+
{ name: "transactional_orgs_switch", desc: "Switch organization" },
|
|
1526
|
+
{ name: "transactional_api_keys_list", desc: "List API keys" },
|
|
1527
|
+
{ name: "transactional_api_keys_create", desc: "Create API key" },
|
|
1528
|
+
{ name: "transactional_members_list", desc: "List members" }
|
|
1529
|
+
] },
|
|
1530
|
+
{ category: "Billing", tools: [
|
|
1531
|
+
{ name: "transactional_billing_usage", desc: "Get usage" },
|
|
1532
|
+
{ name: "transactional_billing_invoices", desc: "List invoices" },
|
|
1533
|
+
{ name: "transactional_billing_plan", desc: "Get plan details" }
|
|
1534
|
+
] }
|
|
1535
|
+
];
|
|
1536
|
+
for (const category of tools) {
|
|
1537
|
+
console.log(`${category.category}:`);
|
|
1538
|
+
for (const tool of category.tools) {
|
|
1539
|
+
console.log(` ${tool.name.padEnd(35)} ${tool.desc}`);
|
|
1540
|
+
}
|
|
1541
|
+
console.log();
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
return mcpCmd;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// src/index.ts
|
|
1548
|
+
initConfig();
|
|
1549
|
+
function createProgram() {
|
|
1550
|
+
const program2 = new Command();
|
|
1551
|
+
program2.name("transactional").description("CLI for Transactional - manage email, SMS, forms, and more").version("0.1.0");
|
|
1552
|
+
program2.addCommand(createLoginCommand());
|
|
1553
|
+
program2.addCommand(createLogoutCommand());
|
|
1554
|
+
program2.addCommand(createWhoamiCommand());
|
|
1555
|
+
program2.addCommand(createSwitchCommand());
|
|
1556
|
+
program2.addCommand(createOrgsCommand());
|
|
1557
|
+
program2.addCommand(createEmailCommand());
|
|
1558
|
+
program2.addCommand(createConfigCommand());
|
|
1559
|
+
program2.addCommand(createMcpCommand());
|
|
1560
|
+
return program2;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// src/bin.ts
|
|
1564
|
+
var program = createProgram();
|
|
1565
|
+
program.parse(process.argv);
|
|
1566
|
+
//# sourceMappingURL=bin.js.map
|
|
1567
|
+
//# sourceMappingURL=bin.js.map
|